Skip to content

fix(frontend): make theme-init script CSP-safe without reviving nonces - #1083

Merged
OlaGreat merged 1 commit into
OlaGreat:mainfrom
precious-akpan:fix/1031-csp-nonce-theme-script
Aug 31, 2026
Merged

fix(frontend): make theme-init script CSP-safe without reviving nonces#1083
OlaGreat merged 1 commit into
OlaGreat:mainfrom
precious-akpan:fix/1031-csp-nonce-theme-script

Conversation

@precious-akpan

Copy link
Copy Markdown
Contributor

Summary

#1031 asks for the same fix already applied to the JSON-LD script (#952/#1025): read the CSP nonce via headers() and pass it to the inline theme-init <script> in layout.tsx. I looked into implementing that literally and found it would reintroduce a regression that was deliberately fixed three days after #1031 was filed:

  • e3a378c (Aug 26) added the nonce mechanism used for the JSON-LD fix.
  • 669bb39 (Aug 29, "repair the red CI pipeline") removed nonces entirely and switched middleware.ts to script-src 'self' 'unsafe-inline', because nonces are incompatible with statically prerendered pages — their HTML has no per-request nonce to carry, and a nonce-source in script-src makes browsers ignore 'unsafe-inline', which blocked all client-side JS on every static route.

At current HEAD, the theme-init script already runs fine — CSP has no nonce at all, just 'unsafe-inline'. So #1031's literal premise (the script is blocked) is no longer true, and adding a nonce back would face the same wall: layout.tsx is the root layout shared by every route, so calling headers() there (a Dynamic API) would force the entire app into dynamic rendering just to mint a per-request nonce — a much bigger regression than the one 669bb39 fixed, and one that would undo #1034's Lighthouse budget work by tanking performance on every currently-static page.

Fix

Move the theme-init script out of line into a static asset (frontend/public/theme-init.js), loaded via next/script's beforeInteractive strategy:

  • A same-origin external script is covered by script-src 'self' alone — it needs neither 'unsafe-inline' nor a nonce.
  • beforeInteractive is next/script's documented mechanism for a script that must run before hydration/paint — the same timing the inline version relied on to avoid the theme flash.
  • This sidesteps @next/next/no-sync-scripts, which a raw <script src> would trip.

Closes #1031

Test plan

  • npm run build — same routes still prerender as static () as before this change; nothing was forced into dynamic rendering.
  • npm run lint — no new warnings/errors (previously: @next/next/no-sync-scripts on a raw <script src> attempt, now clean).
  • npm test -- --run — 118/118 tests pass.
  • Inspected the built static HTML (.next/server/app/index.html) — confirms Next emits the beforeInteractive bootstrap (self.__next_s.push(["/theme-init.js",...]) + preload) that runs before hydration, on a statically prerendered page.

Closes OlaGreat#1031)

OlaGreat#1031 asked for the same fix already applied to the JSON-LD script in
OlaGreat#952/OlaGreat#1025: read the CSP nonce via headers() and pass it to the inline
theme-init <script> in layout.tsx. That approach was deliberately reverted
in 669bb39 ("repair the red CI pipeline") because nonces can't work with
statically prerendered pages — their HTML has no per-request nonce to
carry, and a nonce-source in script-src makes browsers ignore
'unsafe-inline', which blocked all client-side JS on every static route.

Reintroducing that fix here would hit the same wall from the other side:
layout.tsx is the root layout shared by every route, so calling headers()
in it (a Dynamic API) would force the entire app into dynamic rendering to
get a per-request nonce — a much larger regression than the one 669bb39
fixed, and one that would undo issue OlaGreat#1034's Lighthouse budget work by
tanking performance on every previously-static page.

Instead, move the theme-init script out of line into a static asset
(frontend/public/theme-init.js) loaded via next/script's `beforeInteractive`
strategy. A same-origin external script is covered by `script-src 'self'`
alone — it needs neither 'unsafe-inline' nor a nonce — and `beforeInteractive`
is next/script's documented mechanism for a script that must run before
hydration/paint, giving the same anti-flash timing the inline version had
without tripping the `@next/next/no-sync-scripts` lint rule a raw
`<script src>` would.

Verified `npm run build` still prerenders the same routes as static (○)
after this change — no route was forced into dynamic rendering.
@drips-wave

drips-wave Bot commented Aug 30, 2026

Copy link
Copy Markdown

@precious-akpan Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@OlaGreat
OlaGreat merged commit 8d18505 into OlaGreat:main Aug 31, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Frontend] CSP nonce fix never reached the theme-init script — it's still blocked, just like the JSON-LD script was

2 participants