Next.js Advanced
Content Security Policy
Pass the request nonce to ConsentRoot
Read the nonce from the x-nonce request header in the root layout and pass it
as options={{ nonce }} on ConsentRoot. This is the standard Next.js
nonce pattern: your proxy.ts (Next.js 16) or middleware.ts (Next.js 15)
generates a nonce per request, sets the Content-Security-Policy response
header with 'nonce-<value>' in script-src and style-src, and forwards the
same value on the request as x-nonce. See the
Next.js CSP guide
for the header-generating proxy.
If you also run c15tProxy, set
both x-nonce and the generated Content-Security-Policy header on
request.headers before calling it, and set the policy on the returned
response as well. Next.js reads the nonce for its own bootstrap and page
scripts from the request-side policy header, so forwarding only x-nonce
leaves the framework scripts without a nonce and the browser blocks hydration.
c15tProxy copies the incoming request headers into the forwarded request, so
both headers travel with the geography headers.
Then read the header in the root layout and pass it to ConsentRoot as
options.nonce. A string can cross from a Server Component to a Client
Component, so no wrapper is needed:
Reading headers() keeps the route dynamic, which a per-request nonce requires
anyway. Do not combine a nonce-based policy with cacheComponents: true
(Partial Prerendering): the static shell is rendered once at build time, so its
scripts cannot carry a per-request nonce and the browser blocks them. Next.js
documents this limitation in its CSP guide. For a prerendered shell use the
hash-based policy that guide describes; a host-only script-src such as
'self' does not authorize Next.js's inline bootstrap scripts and would need
'unsafe-inline', which defeats the policy. Otherwise keep the route fully
dynamic and use the nonce.
options.nonce is read when ConsentRoot mounts. The provider keeps the latest
value in a ref, but the script loader is created once per runtime, so a nonce
that changes during client-side navigation is not applied to scripts already
created. Each full page load gets the fresh nonce from its own request.
What the nonce applies to
c15t stamps a nonce on three kinds of DOM nodes:
- Every
<script>element the script loader creates for ascriptsentry, bothsrcand inlinetextContentscripts, getsoptions.nonce. Anonceset on an individualscriptsentry takes precedence for that element. - The
<style>elements that the banner, dialog and other stock components render with c15t's rules getoptions.nonce. With a nonce set, they render next to each component instead of moving into<head>, because React drops the nonce of a style it moves. - The inline
<script>that a server-renderedConsentBannerputs before its buttons getsoptions.nonce. It holds a tap the visitor makes before the page hydrates. If the policy blocks it, the banner still works after hydration, but a tap before then does nothing. - The
<style id="c15t-theme">element thatConsentThemerenders with the theme's--c15t-*custom properties gets itsnonceprop. Pass it the same request nonce:<ConsentTheme theme={theme} nonce={nonce} />.
It does not apply to anything else:
- Scripts that a vendor script loads itself, such as a tag manager injecting
its tags, do not receive the nonce. Add
'strict-dynamic'toscript-srcso scripts loaded by a nonced script are allowed, or list those vendor hosts explicitly. - The prebuilt stylesheet
c15t/next/styles.css, if you import it withstyles: false, is a regular stylesheet from your own origin. It is covered bystyle-src 'self', not by the nonce. Use that setup when a page cannot carry a per-request nonce. - Inline
styleattributes on rendered components. A nonce cannot authorize style attributes, and browsers ignore'unsafe-inline'in astyle-srclist that also contains a nonce or hash. Allow them with a separatestyle-src-attr 'unsafe-inline'directive, or with'unsafe-hashes'plus the matching'sha256-...'values; hashes only work for static values, not for the animated collapse heights. The IAB TCF dialog, the consent dialog trigger toolbar and the animated collapse used inside the dialogs render inlinestyleattributes. Check the browser console with your policy enforced to see whether your setup triggers astyle-srcviolation. - Iframes, images or requests made by vendor scripts. Allow those hosts in
frame-src,img-srcandconnect-srcaccording to each vendor.
Allow the consent backend in connect-src
The browser sends consent submissions to ${backendURL}/subjects, and, when
the browser initializes or retries after a failed server prefetch, requests to
${backendURL}/init or the manifest URL. Add the backend origin to
connect-src, and the manifest origin too when the mode's manifestURL is an
absolute URL on a different host such as a CDN:
Use the endpoint host supplied by your Inth project or your self-hosted backend.
With proxy: true and routePrefix: '/api/c15t', as
Optimization
shows, the browser sends every consent request to /api/c15t and 'self'
covers them; the Next.js server connects to the backend outside the browser's
CSP.
IAB TCF policies can fetch the Global Vendor List from the URL the policy
provides; allow that host in connect-src if you use the
IAB TCF add-on. Server-side prefetch runs
in Next.js and is not subject to the browser's CSP.
Verify
Load a page with the policy enforced, not in report-only mode, and open the
browser console. The banner should render with its theme colors and no CSP
violation should mention c15t-theme. Grant a category that gates one of your
scripts entries, then check:
- The injected
<script>element carries the request's nonce. Browsers hide the value fromgetAttribute('nonce'); read the element'snonceproperty in the console or inspect it in the Elements panel. - The
<style id="c15t-theme">element carries the same nonce and the--c15t-*variables are applied to the banner. - No
Refused to load the scriptorRefused to apply inline styleerrors mention a c15t element. A refused vendor dependency points to a missing'strict-dynamic'or vendor host. - The consent submission to
/subjectssucceeds. Aconnect-srcviolation here means the backend origin is missing.
Reload with a different nonce and confirm the elements update. A stale nonce after a full page load usually means a cached HTML response; nonce-based pages must not be served from a shared cache.