import type { Metadata, Viewport } from "next"; import { Inter, Outfit } from "next/font/google"; import { headers } from "next/headers"; import { AuthProvider } from "@/contexts/AuthContext"; import { CheckinDetailsProvider } from "@/contexts/CheckinDetailsContext"; import { ConfigProvider } from "@/contexts/ConfigContext"; import { ThemeProvider } from "@/components/theme-provider"; import { AppShell } from "@/components/layout/app-shell"; import { Toaster } from "@/components/ui/toaster"; import { NonceProvider } from "@/components/nonce-provider"; import { DeviceMemoryInstaller } from "@/components/device-memory-installer"; import { PAGE_TITLE_TEMPLATE } from "@/lib/page-title"; import { publicConfig, runtimeConfig } from "@/lib/runtime-config"; import { SITE_DESCRIPTION } from "@/lib/site-description"; import "./globals.css"; // `--font-inter` is for what cannot take the class: the API reference's Scalar theme. const inter = Inter({ subsets: ["latin"], variable: "--font-inter" }); // The wordmark's face, and nothing else's. Outfit's O, i-dots and single-storey g are // drawn from one circle, so the name reads as part of the three-bubble mark; Inter's O // is a squarer superellipse. SIL Open Font Licence. const outfit = Outfit({ weight: "700", subsets: ["latin"], variable: "--font-outfit", }); // This layout reads `headers()` for the CSP nonce, which under `cacheComponents` is // runtime data outside any Suspense boundary: every route's static shell is empty and // the build fails on it. `false` allows a blocking route, and the root is the placement // because the read is in it: nothing lower covers the app. export const instant = false; // A function rather than an exported `metadata` object because `siteUrl` is read from // the environment at request time (`lib/runtime-config.ts`), and a module-level constant // would be evaluated while the image is being built - freezing whatever the build // machine had into every instance that ever runs it. // // `metadataBase` is what relative URLs in metadata resolve against - `/join`'s canonical // `/` among them - and a relative URL field without it fails the build. A self-hosted // instance sets `SITE_URL` to its own origin; the localhost fallback is right for // development and harmless anywhere else, since the only pages worth unfurling are // public ones a private deployment doesn't expose. export function generateMetadata(): Metadata { const { siteUrl } = runtimeConfig(); return { metadataBase: new URL(siteUrl), // Every page names itself through `metadata`, and this wraps the name; `default` // is only for a route that gives none. So Firefox on iOS, which names a // home-screen shortcut after `document.title` and never reads the manifest, // offers the page's own title there - "Dives – OpenDiving" - for the diver to // edit; a distinct name per tab is the owner's call over a bare one there. title: { default: "OpenDiving", template: PAGE_TITLE_TEMPLATE, }, description: SITE_DESCRIPTION, applicationName: "OpenDiving", // The site's own fields only, so every page previews as itself: Next fills an // `openGraph` without a title or description from the page's own, and the Twitter // card from that. No `url` either - an unfurler names the address it fetched. openGraph: { type: "website", siteName: "OpenDiving" }, // `summary`, since there is no image to make a large card of. twitter: { card: "summary" }, }; } // The browser chrome of the installed app. The app's theme follows the system by // default, so the bar follows it too: white in light, the dark theme's `--background` // (`240 4% 9%`) in dark. A diver who pins the app light under a dark OS gets a dark // bar, which the media query cannot know about. export const viewport: Viewport = { themeColor: [ { media: "(prefers-color-scheme: light)", color: "#ffffff" }, { media: "(prefers-color-scheme: dark)", color: "#161618" }, ], }; export default async function RootLayout({ children, }: { children: React.ReactNode; }) { // Set by src/proxy.ts alongside the per-request CSP nonce; forwarded to // next-themes so it can tag its no-flash-of-wrong-theme bootstrap script // with it - the CSP's script-src only allows same-nonce scripts to run. const nonce = (await headers()).get("x-nonce") ?? undefined; return ( {/* Rendered rather than imported, and rendered here rather than anywhere narrower: evaluating that module is what arms the device-memory switch's write suppression, and a side-effect-only import from this file - an async Server Component - would reach the browser bundle only conditionally. See the component. */} {/* Everything below is a Client Component, and this is the only place the environment is legible - so the instance's configuration is read here and carried down rather than looked up where it is used. */} {children} ); }