import type { Metadata } from 'next'; import { connection } from 'next/server'; import localFont from 'next/font/local'; import { Analytics } from '@/components/analytics/Analytics'; import { CookieNotice } from '@/components/legal/CookieNotice'; import { SentryInit } from '@/components/observability/SentryInit'; import { Toaster } from '@/components/ui/Toaster'; import { umamiWebsiteId } from '@/lib/analytics/config'; import { sentryClientConfig } from '@/lib/observability/config'; import { GITHUB_URL } from '@/lib/constants'; import { OG_LOCALE, SITE_DEFAULT_TITLE, SITE_DESCRIPTION, SITE_KEYWORDS, SITE_NAME, SITE_TAGLINE, TITLE_TEMPLATE, } from '@/lib/seo/config'; import { siteUrl } from '@/lib/seo/site-url'; import './globals.css'; // Self-hosted so the build never depends on Google Fonts being reachable. The // full variable woff2 files (wght axis, latin + latin-ext) live in ./fonts. const jetbrainsMono = localFont({ src: './fonts/JetBrainsMono-Variable.woff2', weight: '100 800', display: 'swap', variable: '--font-mono', fallback: ['ui-monospace', 'SFMono-Regular', 'Menlo', 'monospace'], }); const spaceGrotesk = localFont({ src: './fonts/SpaceGrotesk-Variable.woff2', weight: '300 700', display: 'swap', variable: '--font-sans', fallback: ['ui-sans-serif', 'system-ui', 'sans-serif'], }); /** * `metadataBase` makes every page's relative canonical and OG url absolute, so * it is the one place the public origin enters page metadata. * * `connection()` is what makes that origin correct. Without it this function * runs during `next build` and freezes whatever `SITE_URL` was set then — which * for a domain-agnostic image is nothing. It pushes rendering to request time, * so the value is read from the container's environment instead. The cost is * that no page under this layout prerenders statically; the marketing pages are * light server-rendered copy, and a canonical URL that is right everywhere is * worth more than their prerender. * * Neither `icons` nor `manifest` belongs here: `app/icon.svg` and * `app/manifest.ts` already emit their own `` tags, and a value set here * would override them. */ export async function generateMetadata(): Promise { await connection(); const base = siteUrl(); return { metadataBase: new URL(base), title: { default: SITE_DEFAULT_TITLE, template: TITLE_TEMPLATE }, description: SITE_DESCRIPTION, applicationName: SITE_NAME, authors: [{ name: SITE_NAME, url: GITHUB_URL }], keywords: [...SITE_KEYWORDS], robots: { index: true, follow: true }, alternates: { canonical: '/' }, openGraph: { type: 'website', siteName: SITE_NAME, locale: OG_LOCALE, title: SITE_DEFAULT_TITLE, description: SITE_TAGLINE, url: '/', }, twitter: { card: 'summary_large_image', title: SITE_DEFAULT_TITLE, description: SITE_TAGLINE, }, }; } /** * Structured data for the site as a whole. * * Plans and prices are deliberately absent: the backend is their source of * truth, and an `offers` block here would drift the moment one changes. */ function buildJsonLd(base: string) { const organizationId = new URL('/#organization', base).toString(); return { '@context': 'https://schema.org', '@graph': [ { '@type': 'Organization', '@id': organizationId, name: SITE_NAME, url: base, sameAs: [GITHUB_URL], }, { '@type': 'SoftwareApplication', name: SITE_NAME, description: SITE_DESCRIPTION, applicationCategory: 'DeveloperApplication', operatingSystem: 'Web', url: base, publisher: { '@id': organizationId }, }, ], }; } export default async function RootLayout({ children, }: { children: React.ReactNode; }) { await connection(); // Escaped so a future value containing `` cannot close the tag. const jsonLd = JSON.stringify(buildJsonLd(siteUrl())).replace(/ {/* The route groups share no layout, so the notice mounts once, here. */} {children} {websiteId !== undefined && } {sentry !== undefined && ( )}