Skip to main content

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:

app/layout.tsx
import { ConsentBanner, ConsentDialog, ConsentRoot } from 'c15t/next';
import { resolveConsent } from 'c15t/next/server';
import { headers } from 'next/headers';
import { Suspense } from 'react';
import type { ReactNode } from 'react';

import './globals.css';

async function ResolvedConsent({ children }: { children: ReactNode }) {
	const nonce = (await headers()).get('x-nonce') ?? undefined;
	const state = await resolveConsent();

	return (
		<ConsentRoot state={state} options={{ nonce }}>
			{children}
			<ConsentBanner />
			<ConsentDialog />
		</ConsentRoot>
	);
}

export default function RootLayout({ children }: { children: ReactNode }) {
	return (
		<html lang="en">
			<body>
				<Suspense fallback={null}>
					<ResolvedConsent>{children}</ResolvedConsent>
				</Suspense>
			</body>
		</html>
	);
}

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 a scripts entry, both src and inline textContent scripts, gets options.nonce. A nonce set on an individual scripts entry takes precedence for that element.
  • The <style> elements that the banner, dialog and other stock components render with c15t's rules get options.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-rendered ConsentBanner puts before its buttons gets options.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 that ConsentTheme renders with the theme's --c15t-* custom properties gets its nonce prop. 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' to script-src so 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 with styles: false, is a regular stylesheet from your own origin. It is covered by style-src 'self', not by the nonce. Use that setup when a page cannot carry a per-request nonce.
  • Inline style attributes on rendered components. A nonce cannot authorize style attributes, and browsers ignore 'unsafe-inline' in a style-src list that also contains a nonce or hash. Allow them with a separate style-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 inline style attributes. Check the browser console with your policy enforced to see whether your setup triggers a style-src violation.
  • Iframes, images or requests made by vendor scripts. Allow those hosts in frame-src, img-src and connect-src according to each vendor.

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:

Content-Security-Policy (partial)
connect-src 'self' https://<your-backend-host> https://<your-manifest-host>;

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 from getAttribute('nonce'); read the element's nonce property 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 script or Refused to apply inline style errors mention a c15t element. A refused vendor dependency points to a missing 'strict-dynamic' or vendor host.
  • The consent submission to /subjects succeeds. A connect-src violation 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.