Merge feat/german-locale: German version, glow effects and search metadata
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>
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user