Der Browser leert das Nonce-Attribut, sobald er das Element geparst hat — so schreibt es die CSP-Spezifikation vor, damit kein Skript den Wert auslesen und sich damit selbst nachladen kann. React vergleicht beim Hydrieren trotzdem das Attribut, liest clientseitig den leeren Wert und meldet einen Unterschied, den es nicht gibt. suppressHydrationWarning an diesem einen Element sagt React, dass hier nichts zu vergleichen ist. Das Nonce selbst bleibt: ohne es blockiert die Richtlinie das Skript, und ohne das Skript blitzt beim Laden das falsche Thema auf. Nachgewiesen gegen einen Entwicklungsserver: ohne das Attribut erscheint genau die gemeldete Konsolenmeldung, mit ihm nicht. Ein erster Versuch, das per Konsolenprüfung im e2e abzusichern, war auch ohne den Fix grün — React meldet den Unterschied nur im Entwicklungsmodus, der Testlauf nutzt den Produktionsbau. Die Prüfung ist wieder entfernt; eine, die nicht anschlägt, ist schlimmer als keine. Stattdessen prüft tests/thema-vorab.test.ts die drei Dinge am Skript, die zusammenpassen müssen: Nonce, Hydrierungsausnahme und derselbe Speicherschlüssel wie im Umschalter. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
86 lines
3.0 KiB
TypeScript
86 lines
3.0 KiB
TypeScript
import type { Metadata, Viewport } from 'next'
|
|
import { headers } from 'next/headers'
|
|
import { de } from '@/lib/i18n/de'
|
|
import { THEME_KEY } from '@/components/ThemeToggle'
|
|
import './globals.css'
|
|
|
|
export const metadata: Metadata = {
|
|
title: { default: de.app.name, template: `%s · ${de.app.shortName}` },
|
|
description: de.app.description,
|
|
robots: { index: false, follow: false },
|
|
}
|
|
|
|
export const viewport: Viewport = {
|
|
themeColor: '#15181A',
|
|
width: 'device-width',
|
|
initialScale: 1,
|
|
}
|
|
|
|
/**
|
|
* Setzt das gewählte Thema, **bevor** das erste Mal gemalt wird.
|
|
*
|
|
* Ohne das blitzt beim Laden kurz das falsche Thema auf. Es ist die eine
|
|
* Stelle mit `dangerouslySetInnerHTML`, und der Inhalt ist eine Konstante aus
|
|
* diesem Modul — kein Nutzertext. Das Verbot aus docs/07 gilt Nutzerinhalten.
|
|
*/
|
|
const THEME_SCRIPT =
|
|
`try{var t=localStorage.getItem(${JSON.stringify(THEME_KEY)});` +
|
|
`if(t==='dark'||t==='light')document.documentElement.setAttribute('data-theme',t)}catch(e){}`
|
|
|
|
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
|
// Die strikte Inhaltsrichtlinie erlaubt nur Skripte mit passendem Nonce,
|
|
// siehe middleware.ts.
|
|
const nonce = (await headers()).get('x-nonce') ?? undefined
|
|
|
|
return (
|
|
<html lang="de">
|
|
<head>
|
|
{/*
|
|
`suppressHydrationWarning` wegen des Nonce, nicht wegen des Inhalts.
|
|
Der Browser **leert das Nonce-Attribut**, sobald er das Element
|
|
geparst hat — so schreibt es die CSP-Spezifikation vor, damit kein
|
|
Skript den Wert über `getAttribute` auslesen und sich selbst
|
|
nachladen kann. React vergleicht beim Hydrieren aber genau dieses
|
|
Attribut, liest clientseitig den leeren Wert und meldet einen
|
|
Unterschied, den es nicht gibt: unser Markup ist auf beiden Seiten
|
|
dasselbe.
|
|
|
|
Es bleibt bei einem einzigen Element mit einer Konstante als Inhalt,
|
|
deshalb ist die Unterdrückung hier eng genug. Der Nonce selbst
|
|
entfällt nicht: ohne ihn blockiert die Richtlinie das Skript, und
|
|
ohne das Skript blitzt beim Laden das falsche Thema auf.
|
|
*/}
|
|
<script
|
|
nonce={nonce}
|
|
suppressHydrationWarning
|
|
dangerouslySetInnerHTML={{ __html: THEME_SCRIPT }}
|
|
/>
|
|
{/* Nur die beiden Schnitte vorladen, die auf jeder Seite vorkommen. */}
|
|
<link
|
|
rel="preload"
|
|
href="/fonts/ibm-plex-sans-latin-400-normal.woff2"
|
|
as="font"
|
|
type="font/woff2"
|
|
crossOrigin="anonymous"
|
|
/>
|
|
<link
|
|
rel="preload"
|
|
href="/fonts/ibm-plex-mono-latin-400-normal.woff2"
|
|
as="font"
|
|
type="font/woff2"
|
|
crossOrigin="anonymous"
|
|
/>
|
|
</head>
|
|
<body>
|
|
<a
|
|
href="#inhalt"
|
|
className="sr-only focus:not-sr-only focus:absolute focus:top-2 focus:left-2 focus:z-50 focus:border focus:border-line focus:bg-panel focus:px-3 focus:py-2"
|
|
>
|
|
{de.nav.skipToContent}
|
|
</a>
|
|
{children}
|
|
</body>
|
|
</html>
|
|
)
|
|
}
|