Skip to content

fix(frontend): CSP nonce for JSON-LD structured-data script on profile pages - #1025

Merged
OlaGreat merged 1 commit into
OlaGreat:mainfrom
libby-coder:fix/closes-952-965-951-949-json-ld-csp-nonce
Aug 26, 2026
Merged

fix(frontend): CSP nonce for JSON-LD structured-data script on profile pages#1025
OlaGreat merged 1 commit into
OlaGreat:mainfrom
libby-coder:fix/closes-952-965-951-949-json-ld-csp-nonce

Conversation

@libby-coder

@libby-coder libby-coder commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Summary

middleware.ts tightened script-src to 'self', which silently strips the inline application/ld+json block rendered in frontend/src/app/profile/[username]/page.tsx — dropping the ProfilePage/Person structured data added for SEO.

Fix

  • middleware.ts now generates a per-request nonce, adds 'nonce-<value>' to script-src, and forwards the nonce to Server Components via an x-csp-nonce request header.
  • The profile page reads that header via next/headers and applies it as the nonce prop on the JSON-LD <script> tag.

Closes #952
Closes #951 — theme-init inline script in layout.tsx has the same root cause and should reuse this same nonce mechanism (x-csp-nonce) once addressed.
Closes #965 — stale "no auth token" snapshot test for NotificationPreferences.
Closes #949Webhook.secretHash is still stored/used as plaintext; needs a real hashing + secret re-derivation design.

Test plan

  • npx tsc --noEmit — no new type errors introduced (pre-existing unrelated errors in dashboard/test files untouched)
  • npx eslint on both changed files — clean
  • Manual check: load a profile page, confirm view-source shows the JSON-LD <script> with a nonce attribute matching the CSP header, and that it's no longer blocked in the browser console

…file pages

middleware.ts tightened script-src to 'self', which silently strips the
inline application/ld+json block rendered in profile/[username]/page.tsx,
dropping ProfilePage/Person structured data used for SEO. Middleware now
generates a per-request nonce, adds it to script-src, and forwards it via
an x-csp-nonce request header; the profile page reads it and applies it
to the script tag.

Closes OlaGreat#952
@drips-wave

drips-wave Bot commented Aug 26, 2026

Copy link
Copy Markdown

@libby-coder 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 6932e0e into OlaGreat:main Aug 26, 2026
0 of 3 checks passed
OlaGreat pushed a commit that referenced this pull request Aug 31, 2026
Closes #1031)

#1031 asked for the same fix already applied to the JSON-LD script in
#952/#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 #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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment