Brings the German main language (English under /en), the binary glow effects with the refined drawings and calls to action, and the search metadata onto the server-side analytics from v2.1.0. Both sides had added apps/web/proxy.ts. It is now one request shell: it picks the language and maps the address (routes.ts), and records the page view server-side (pageview-decision.ts) for every request that is not a redirect, using the public address the visitor asked for. The browser analytics component and the /stats proxy route stay removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
137 lines
4.1 KiB
TypeScript
137 lines
4.1 KiB
TypeScript
import type { Metadata, Viewport } from "next";
|
|
import type { StaticImageData } from "next/image";
|
|
import { Inter, JetBrains_Mono, Newsreader } from "next/font/google";
|
|
import { BitBurst } from "@/src/components/bits/BitBurst";
|
|
import { BitGlow } from "@/src/components/bits/BitGlow";
|
|
import { Footer } from "@/src/components/Footer";
|
|
import { Header } from "@/src/components/Header";
|
|
import { InteractiveElements } from "@/src/components/InteractiveElements";
|
|
import { ContentProvider } from "@/src/components/ContentProvider";
|
|
import { BitCurtain } from "@/src/components/bits/BitCurtain";
|
|
import shareImageDe from "@/src/assets/share/share-de.png";
|
|
import shareImageEn from "@/src/assets/share/share-en.png";
|
|
import { contentFor } from "@/src/content";
|
|
import { INDEXING, SITE_NAME, SITE_URL } from "@/src/content/site";
|
|
import { robotsMeta } from "@/src/domain/indexing";
|
|
import {
|
|
DEFAULT_LOCALE,
|
|
LOCALES,
|
|
parseLocale,
|
|
type Locale,
|
|
} from "@/src/domain/locale";
|
|
import { pathFor } from "@/src/domain/routes";
|
|
import "../globals.css";
|
|
|
|
const inter = Inter({ subsets: ["latin"], variable: "--font-inter" });
|
|
// A real monospaced fallback has almost the same letter width. The default
|
|
// fallback (a resized Arial) wraps the mono labels differently, so the hero
|
|
// jumped when the font arrived.
|
|
const jetbrainsMono = JetBrains_Mono({
|
|
subsets: ["latin"],
|
|
variable: "--font-jetbrains-mono",
|
|
display: "swap",
|
|
adjustFontFallback: false,
|
|
fallback: ["ui-monospace", "Menlo", "Consolas", "monospace"],
|
|
});
|
|
const newsreader = Newsreader({
|
|
subsets: ["latin"],
|
|
variable: "--font-newsreader",
|
|
style: "italic",
|
|
display: "swap",
|
|
});
|
|
|
|
type Props = {
|
|
readonly children: React.ReactNode;
|
|
readonly params: Promise<{ readonly locale: string }>;
|
|
};
|
|
|
|
/** The share image is a screenshot of the hero in that language (see scripts/render-share-image.mjs). */
|
|
const SHARE_IMAGE: Readonly<Record<Locale, StaticImageData>> = {
|
|
en: shareImageEn,
|
|
de: shareImageDe,
|
|
};
|
|
|
|
export function generateStaticParams() {
|
|
return LOCALES.map((locale) => ({ locale }));
|
|
}
|
|
|
|
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|
const locale = parseLocale((await params).locale) ?? DEFAULT_LOCALE;
|
|
const { site } = contentFor(locale);
|
|
const image = SHARE_IMAGE[locale];
|
|
const images = [
|
|
{
|
|
url: image.src,
|
|
width: image.width,
|
|
height: image.height,
|
|
alt: site.shareImageAlt,
|
|
},
|
|
];
|
|
|
|
return {
|
|
metadataBase: new URL(SITE_URL),
|
|
title: {
|
|
default: site.title,
|
|
template: `%s | ${SITE_NAME}`,
|
|
},
|
|
description: site.description,
|
|
applicationName: SITE_NAME,
|
|
authors: [{ name: SITE_NAME, url: SITE_URL }],
|
|
creator: SITE_NAME,
|
|
robots: robotsMeta(INDEXING),
|
|
icons: {
|
|
icon: "/favicon.svg",
|
|
apple: "/apple-touch-icon.png",
|
|
},
|
|
openGraph: {
|
|
type: "website",
|
|
url: pathFor("home", locale),
|
|
siteName: SITE_NAME,
|
|
locale: site.ogLocale,
|
|
alternateLocale: LOCALES.filter((other) => other !== locale).map(
|
|
(other) => contentFor(other).site.ogLocale,
|
|
),
|
|
title: site.title,
|
|
description: site.description,
|
|
images,
|
|
},
|
|
twitter: {
|
|
card: "summary_large_image",
|
|
title: site.title,
|
|
description: site.description,
|
|
images,
|
|
},
|
|
};
|
|
}
|
|
|
|
export const viewport: Viewport = {
|
|
themeColor: "#ffffff",
|
|
colorScheme: "light",
|
|
viewportFit: "cover",
|
|
};
|
|
|
|
export default async function RootLayout({ children, params }: Props) {
|
|
// The proxy only lets the two languages through; anything else is a file-like
|
|
// address that ends in the error page of the main language.
|
|
const locale = parseLocale((await params).locale) ?? DEFAULT_LOCALE;
|
|
|
|
return (
|
|
<html
|
|
lang={locale}
|
|
className={`${inter.variable} ${newsreader.variable} ${jetbrainsMono.variable}`}
|
|
>
|
|
<body className="min-h-screen bg-white">
|
|
<ContentProvider locale={locale} content={contentFor(locale)}>
|
|
<Header />
|
|
<main>{children}</main>
|
|
<Footer />
|
|
<InteractiveElements />
|
|
<BitCurtain />
|
|
</ContentProvider>
|
|
<BitBurst />
|
|
<BitGlow />
|
|
</body>
|
|
</html>
|
|
);
|
|
}
|