From 68c0b1306dc9b56ee43b25ecf4fa56a6069db95c Mon Sep 17 00:00:00 2001 From: Leon Serfaty <80597822+silkoserfo@users.noreply.github.com> Date: Mon, 7 Sep 2026 12:16:09 -0400 Subject: [PATCH] fix(csp): blank page on statically prerendered routes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The nonce-based script-src is incompatible with Next's static prerendering. The nonce is minted per request in middleware, but statically generated HTML was produced at build time with no nonce on its inline scripts, so on "/" and the marketing routes every inline hydration script was blocked: the page painted from SSR HTML, React failed to hydrate, and the tree unmounted to a blank screen a moment later. Dynamic routes (/sign-in, /dashboard) render per request, receive the nonce, and worked — which is why this was missed. Browsers that honour a nonce ignore 'unsafe-inline', so the two cannot be combined. Drop the nonce and allow inline scripts; the policy still restricts which origins may serve scripts, and frame-src/connect-src stay strict. Restoring nonces would require every route to render dynamically. Co-Authored-By: Claude Opus 5 (1M context) --- middleware.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/middleware.ts b/middleware.ts index 41a7811..9c26cec 100644 --- a/middleware.ts +++ b/middleware.ts @@ -67,7 +67,16 @@ export function middleware(req: NextRequest) { // proxied analytics tracker at /_a), and the Turnstile host-source actually // takes effect instead of being silently ignored. The nonce is kept, so // dynamic routes and any inline script still get the stronger guarantee. - `script-src 'self' 'nonce-${nonce}' ${TURNSTILE_ORIGIN}${devScriptSrc}`, + // NOTE: nonce-based script-src is incompatible with Next's statically + // prerendered pages. The nonce is minted per request, but static HTML was + // baked at build time WITHOUT one, so every inline hydration script on those + // pages ("/", the marketing routes) got blocked: the page painted, React + // failed to hydrate, and the tree unmounted to a blank screen. Dynamic routes + // (/sign-in, /dashboard) were fine, which is what made this easy to miss. + // A browser that honours a nonce ignores 'unsafe-inline', so the two cannot + // be combined — this policy still restricts which ORIGINS may serve scripts. + // To go back to nonces, every page must render dynamically. + `script-src 'self' 'unsafe-inline' ${TURNSTILE_ORIGIN}${devScriptSrc}`, "style-src 'self' 'unsafe-inline'", "img-src 'self' data: https://oaidalleapiprodscus.blob.core.windows.net https://images.unsplash.com", "media-src 'self'",