fix(frontend): make theme-init script CSP-safe without reviving nonces - #1083
Merged
OlaGreat merged 1 commit intoAug 31, 2026
Merged
Conversation
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.
|
@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! 🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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>inlayout.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 switchedmiddleware.tstoscript-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 inscript-srcmakes 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.tsxis the root layout shared by every route, so callingheaders()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 one669bb39fixed, 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 vianext/script'sbeforeInteractivestrategy:script-src 'self'alone — it needs neither'unsafe-inline'nor a nonce.beforeInteractiveisnext/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.@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-scriptson a raw<script src>attempt, now clean).npm test -- --run— 118/118 tests pass..next/server/app/index.html) — confirms Next emits thebeforeInteractivebootstrap (self.__next_s.push(["/theme-init.js",...])+ preload) that runs before hydration, on a statically prerendered page.