返回 CodeWhale
layout.tsx
根目录 / web / app / [locale] / layout.tsx
1 import type { Metadata } from "next";
2 import localFont from "next/font/local";
3 import { notFound } from "next/navigation";
4 import { Nav } from "@/components/nav";
5 import { Footer } from "@/components/footer";
6 import { UsageCounting } from "@/components/usage-counting";
7 import { BUILD_FACTS } from "@/lib/facts";
8 import { isValidLocale, localeDirection, locales } from "@/lib/i18n/config";
9 import { getChrome, getHome } from "@/lib/i18n/dictionaries";
10 import { serializeJsonLd } from "@/lib/json-ld";
11 import { buildPageMetadata } from "@/lib/page-meta";
12 import { buildSiteJsonLd } from "@/lib/site-schema";
13 import { metadata as notFoundMetadata } from "./not-found";
14 import "../globals.css";
15
16 // Shannon Sans is the one face, as in the GPUI app (`set_theme`). The pinned
17 // variable font is split by scripts/subset-shannon-sans.py into a Latin face,
18 // the only font preloaded, and an extended face (latin-ext, Greek, Cyrillic,
19 // Devanagari) the browser fetches only when a page contains one of its
20 // glyphs. The unicode ranges must match what that script prints. Code uses
21 // the system monospace stack (tokens-roles.css), so no mono face loads.
22 const sansLatin = localFont({
23 src: "../../public/brand/fonts/ShannonSans-Variable-latin.woff2",
24 weight: "100 900",
25 style: "normal",
26 variable: "--font-shannon-latin",
27 display: "swap",
28 // The extended face's metric-matched fallback sits last in the stack; a
29 // fallback here would claim every non-Latin glyph before the extended face.
30 adjustFontFallback: false,
31 declarations: [
32 {
33 prop: "unicode-range",
34 value:
35 "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2190-2199, U+2212-2215, U+FEFF, U+FFFD",
36 },
37 ],
38 });
39
40 const sansExt = localFont({
41 src: "../../public/brand/fonts/ShannonSans-Variable-ext.woff2",
42 weight: "100 900",
43 style: "normal",
44 variable: "--font-shannon-ext",
45 display: "swap",
46 preload: false,
47 declarations: [
48 {
49 prop: "unicode-range",
50 value:
51 "U+0100-02BA, U+02BD-02C5, U+02C7-02D9, U+02DB, U+02DD-0303, U+0305-0307, U+0309-0328, U+032A-052F, U+0900-097F, U+10FB, U+1AB0-1ACE, U+1C80-1C88, U+1D00-1FFF, U+2070-209C, U+20A0-20AB, U+20AD-20C0, U+20F0, U+2100-2121, U+2123-218F, U+25CC, U+2C60-2C7F, U+2DE0-2E5D, U+A640-A69F, U+A700-A7FF, U+A8FF, U+A92E, U+AB30-AB6B, U+FB00-FB06, U+FE00-FE2F, U+FFFC, U+10780-107BA, U+1DF00-1DF1E",
52 },
53 ],
54 });
55
56 export function generateStaticParams() {
57 return locales.map((locale) => ({ locale }));
58 }
59
60 export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }): Promise<Metadata> {
61 const { locale } = await params;
62 // The layout answers not-found for this; without the guard the home page's
63 // title, canonical, and hreflang stream into that 404 (`/wp-login.php`).
64 // Throwing here instead leaves the page with no title at all.
65 if (!isValidLocale(locale)) return notFoundMetadata;
66 const home = getHome(locale);
67 return buildPageMetadata({
68 path: "/",
69 locale,
70 title: home.metaTitle,
71 description: home.metaDescription,
72 });
73 }
74
75 export default async function LocaleLayout({
76 children,
77 params,
78 }: {
79 children: React.ReactNode;
80 params: Promise<{ locale: string }>;
81 }) {
82 const { locale } = await params;
83 // Dotted paths bypass locale redirection so real files keep resolving.
84 // An unknown path such as /foo.txt (or /foo.txt/faq) still binds `[locale]`
85 // here. Reject it before reading dictionaries or rendering home chrome,
86 // so nonexistent files never become HTTP 200 pages with a fake html lang.
87 if (!isValidLocale(locale)) notFound();
88 const chrome = getChrome(locale);
89 // RTL locales (e.g. ar) set the document direction from the canonical
90 // registry so the browser handles bidirectional layout from the root.
91 const dir = localeDirection(locale);
92 const siteJsonLd = buildSiteJsonLd(locale);
93
94 return (
95 <html
96 lang={locale}
97 dir={dir}
98 className={`${sansLatin.variable} ${sansExt.variable}`}
99 suppressHydrationWarning
100 >
101 <body>
102 <script
103 type="application/ld+json"
104 dangerouslySetInnerHTML={{ __html: serializeJsonLd(siteJsonLd) }}
105 />
106 {/* Site-wide theme, applied before paint so there is no flash. With
107 no pin, the stylesheet's `prefers-color-scheme` rules resolve the
108 OS appearance (and track it live); a stored "light" or "dark"
109 (`cw-theme`, see components/theme-toggle.tsx) pins the scheme.
110 "system", a legacy "auto", or no choice leaves it to the OS. */}
111 <script
112 dangerouslySetInnerHTML={{
113 __html:
114 "(function(){try{var t=localStorage.getItem('cw-theme');if(t==='light'||t==='dark'){document.documentElement.setAttribute('data-theme',t);}}catch(e){}})();",
115 }}
116 />
117 <a href="#main-content" className="skip-link">
118 {chrome.skipToContent}
119 </a>
120 <Nav locale={locale} />
121 <main id="main-content">{children}</main>
122 <Footer locale={locale} />
123 {/* Aggregate usage counting, on by default — see lib/telemetry. The
124 choice lives on the privacy page; every opt-out stays off. */}
125 <UsageCounting appVersion={BUILD_FACTS.version ?? "0.0.0"} />
126 </body>
127 </html>
128 );
129 }
130
130 lines Plain Text