Skip to main content

Nuxt

Quickstart

Before you start

This guide is for a Nuxt app that runs a Nuxt server, built with nuxt build and deployed to Node or a serverless preset. The server resolves each visitor's policy, so the banner is part of the first HTML. Prerendered routes and routes cached with swr, isr or cache rules work with the same setup; the browser resolves the visitor on those routes after hydration. For nuxt generate on a static host, or ssr: false, follow deploy to static hosting instead. Choose your setup compares every option.

The guide uses Inth for policies and consent records. Create a project, set its policy rules, add your site's origin to its trusted origins, and copy the backend URL. To run the service yourself, use a self-hosted backend URL instead. Offline mode keeps policies in your code and choices in the browser, with no consent records. Not recommended for production environments.

The setup bundles your policy into the server at build time, the recommended production setup. Rebuild after changing policies, translations or vendors.

Install c15t

npm install c15t@alpha @c15t/integrations@alpha

c15t/vue is the Nuxt module. You do not install a separate Nuxt package.

Add the Nuxt module

Register the module in nuxt.config.ts:

nuxt.config.ts
export default defineNuxtConfig({
	compatibilityDate: '2026-07-04',
	modules: ['c15t/vue'],
});

Set NUXT_PUBLIC_C15T_BACKEND_URL to your project's backend URL, including any path prefix, in .env or wherever you build. The module reads it, so the config needs no backendURL. The policy is fixed at build time: setting NUXT_PUBLIC_C15T_BACKEND_URL on a running server moves consent saves, not the policy, so build again to switch projects.

.env
NUXT_PUBLIC_C15T_BACKEND_URL=https://your-project.inth.app

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

The module's default mode, manifest(), fetches your project's public policy once, during nuxt build and when nuxt dev starts, and embeds it in the server bundle. It also adds one consent route to your Nuxt server, /api/c15t/**, which answers /api/c15t/init and /api/c15t/manifest. The server resolves each visitor from the bundled policy and the request's location and language headers, so rendering a page never waits on Inth. The browser bundle carries no policy and no resolver. Consent saves still go to Inth. Data fetching compares this with calling the backend on every request.

The snapshot is fixed at build time:

  • Rebuild after changing policies, translations or vendors. If your CI caches build output, force a fresh build.
  • Consent choices still go to the backend.

The build reads the backend URL from your public backend URL variable when the config doesn't pass one: NEXT_PUBLIC_C15T_BACKEND_URL in Next.js, NUXT_PUBLIC_C15T_BACKEND_URL in Nuxt, PUBLIC_C15T_BACKEND_URL in Astro, Svelte and SvelteKit, and VITE_C15T_BACKEND_URL in TanStack Start and other Vite apps. Each also reads the matching Inth variable, such as NEXT_PUBLIC_INTH_PROJECT_URL, when the c15t one is unset. See set the backend URL.

The fetch waits at most 10 seconds. When it fails, or no backend URL is set, every framework does the same thing:

CommandDefault when the fetch fails
Production build: next build, vite build, nuxt build, astro buildThe build stops with an error.
Dev: next dev, vite dev, nuxt dev, astro devA warning, and the server fetches the policy at runtime.

Set onBuildError to use one behaviour for both. 'fail' stops dev too. 'runtime' lets a production build finish, and the server fetches the policy at runtime. The C15T_ON_BUILD_ERROR environment variable overrides the option, so you can deploy during a backend outage without a code change:

C15T_ON_BUILD_ERROR=runtime npm run build

Turborepo's strict environment mode hides undeclared variables from tasks, so list C15T_ON_BUILD_ERROR in the build task's passThroughEnv there.

The build skips the fetch, without an error, when it can't use a snapshot, for example when the backend URL is relative. With onBuildError: 'fail', a relative URL stops the build. Consent modes lists every case.

To apply policy edits without rebuilding, set c15t: { mode: manifest({ source: 'runtime' }) }, with manifest imported from c15t/vue. The server then fetches and caches the policy at runtime. Rendering and deployment covers the other modes.

Add ConsentRoot and a preferences link to app.vue:

app/app.vue
<template>
	<ConsentRoot />
	<NuxtPage />
	<footer>
		<ConsentDialogLink>Privacy settings</ConsentDialogLink>
	</footer>
</template>

ConsentRoot renders the banner when the visitor's policy asks for one and loads the preference dialog when it opens. ConsentDialogLink renders a button that reopens preferences, labelled with the dialog's title in the visitor's language: Privacy Settings in English. It renders nothing until the policy offers preferences.

The module registers these components and the consent composables, so you write no imports and no app.use(). Keep ConsentRoot in app.vue, outside <NuxtPage />, so it stays mounted across navigation. The components bring their own styles.

Remove any existing PostHog snippet so the script loads once, and add measurement to your Inth policy.

Register PostHog in app/app.config.ts. It waits for measurement permission.

app/app.config.ts
import { posthog } from '@c15t/integrations/posthog';

export default defineAppConfig({
	c15t: {
		scripts: [
			posthog({
				id: 'phc_your_project_key',
				initOptions: { cookieless_mode: 'never' },
				loadMode: 'after-consent',
			}),
		],
	},
});

Replace phc_your_project_key with your PostHog project key. PostHog defaults to its EU region; set region: 'us' for a US project.

The module merges the c15t key of app.config.ts over its options in nuxt.config.ts, then starts one script loader in the browser after hydration. Put options that contain functions, such as scripts and callbacks, in app.config.ts. Module options in nuxt.config.ts reach the browser as JSON, which drops functions.

Gate an embed

Wrap third-party iframes in ConsentGate. The iframe does not exist in the page until its category is allowed, and it is removed when the visitor withdraws permission.

app/components/VideoEmbed.vue
<template>
	<ConsentGate category="measurement">
		<iframe
			src="https://www.youtube-nocookie.com/embed/czTksCF6X8Y"
			title="YouTube video"
			loading="lazy"
			allowfullscreen
		/>
		<template #placeholder>
			<p>Allow measurement to load this YouTube video.</p>
			<ConsentDialogLink>Choose video permissions</ConsentDialogLink>
		</template>
	</ConsentGate>
</template>

Nuxt registers components in app/components for you. Render <VideoEmbed /> in a page where the video belongs.

Verify the setup

Build and start the production server with nuxt build and node .output/server/index.mjs. Open the site in a private window with your browser's DevTools on the Network tab, under a policy that asks for consent.

  1. Load a page and view its source. The banner markup, data-testid="consent-banner-root", is in the HTML. The Network tab shows no requests to posthog.com or youtube-nocookie.com.
  2. Click Reject All, then reload. The banner stays closed and the vendor requests stay absent.
  3. Click Privacy settings, turn on Analytics (the measurement category) and save. PostHog's array.js loads and the video appears.
  4. Turn Analytics off and save. The page reloads, the video is gone and PostHog does not load again.

Verify consent has the full release checklist.

Next steps