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> = { en: shareImageEn, de: shareImageDe, }; export function generateStaticParams() { return LOCALES.map((locale) => ({ locale })); } export async function generateMetadata({ params }: Props): Promise { 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 (
{children}