Skip to main content

Next.js

Client-side initialization

When to initialize in the browser

This setup has the fewest files. The browser calls your Inth backend's /init after the page loads, so the server does no consent work and every page can be static. The HTML is the same for every visitor, and the banner appears after hydration.

Use it when you do not need the banner in the server HTML and do not want a consent route. For a server app, the App Router and Pages Router guides resolve consent on the server and make no /init request per visit. For output: 'export', follow static export. Rendering and deployment compares every path.

Until the policy resolves, optional categories stay denied and the banner stays hidden.

Configure the backend and the mode

Create an Inth project, set its policy rules and add your site's origin to its trusted origins. A self-hosted backend works the same way with its own URL.

npm install c15t@alpha @c15t/integrations@alpha

Set NEXT_PUBLIC_C15T_BACKEND_URL to your project's backend URL, including any path prefix, in .env.local and in your host's build settings:

.env.local
NEXT_PUBLIC_C15T_BACKEND_URL=https://your-project.inth.app

If your app already sets NEXT_PUBLIC_INTH_PROJECT_URL for other Inth SDKs, that works too. When both are set, NEXT_PUBLIC_C15T_BACKEND_URL wins.

Create c15t.config.ts at the project root with mode: hosted():

c15t.config.ts
import { defineConsentConfig, hosted } from 'c15t/next';

// The browser asks the backend's /init. The backend URL comes from
// NEXT_PUBLIC_C15T_BACKEND_URL.
export default defineConsentConfig({ mode: hosted() });

hosted() initializes through ${backendURL}/init and posts choices to ${backendURL}/subjects. Add your scripts to the same config, as the quickstart shows. The file is bundled into the browser, so it must hold no secrets.

Wrap next.config.ts in withConsentManifest, which finds c15t.config.ts:

next.config.ts
import { withConsentManifest } from 'c15t/next/build';
import type { NextConfig } from 'next';

const nextConfig = {} satisfies NextConfig;

// Reads NEXT_PUBLIC_C15T_BACKEND_URL, like defineConsentConfig.
export default withConsentManifest(nextConfig);

With hosted() it downloads no manifest, so next build does not contact the backend.

Mount the root without state

The components render their own styles. With Tailwind CSS 3, load the stylesheet yourself. In the App Router, render ConsentRoot in the root layout without state:

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

import './globals.css';

// No resolveConsent: the browser resolves consent, so pages can be static.
const RootLayout = ({ children }: { children: ReactNode }) => (
	<html lang="en">
		<body>
			<ConsentRoot>
				{children}
				<ConsentBanner />
				<ConsentDialog />
				<footer>
					<ConsentDialogLink>Privacy settings</ConsentDialogLink>
				</footer>
			</ConsentRoot>
		</body>
	</html>
);

export default RootLayout;

In the Pages Router, render it in _app.tsx. Without withConsentProps, pageProps.consent is undefined on every page:

pages/_app.tsx
import {
	ConsentBanner,
	ConsentDialog,
	ConsentDialogLink,
	ConsentRoot,
} from 'c15t/next';
import type { ConsentPageProps } from 'c15t/next/pages';
import type { AppProps } from 'next/app';

import '@/styles/globals.css';

const App = ({ Component, pageProps }: AppProps<ConsentPageProps>) => (
	// Pages without getServerSideProps resolve consent in the browser.
	<ConsentRoot state={pageProps.consent}>
		<Component {...pageProps} />
		<ConsentBanner />
		<ConsentDialog />
		<footer>
			<ConsentDialogLink>Privacy settings</ConsentDialogLink>
		</footer>
	</ConsentRoot>
);

export default App;

Without server state, ConsentRoot initializes in the browser. Keep it mounted across client navigation.

Optional same-origin requests

With routePrefix and proxy: true in c15t.config.ts and a consent route that forwards saves, the browser sends every consent request to your own origin, which saves it a separate connection to the backend's domain. It needs a Next.js server. See optimization.

Verify browser initialization

Open the production build in a fresh browser session with DevTools open.

  1. The browser calls ${backendURL}/init once, then shows the banner. No PostHog request runs.
  2. Click Reject All and reload. The banner stays closed and PostHog does not load.
  3. Open Privacy settings. The rejection is still selected. Turn on Analytics (the measurement category); PostHog loads.
  4. Block the backend's domain in DevTools and reload in a fresh session. No banner appears and no vendor loads. A hidden banner is not permission.