---
title: Quickstart
description: Add c15t to a server-rendered Nuxt app with the Nuxt module, Inth,
  consent-gated scripts and a preferences link.
group: frameworks
lastModified: "2026-10-10T16:01:45+01:00"
---
## 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](/docs/frameworks/nuxt/rendering#deploy-to-static-hosting)
instead.
[Choose your setup](/docs/concepts/choose-your-setup) compares every option.

The guide uses [Inth](https://inth.com) 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](/docs/self-host/overview) 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

|Package manager|Command|
|:--|:--|
|npm|`npm install c15t@alpha @c15t/integrations@alpha`|
|pnpm|`pnpm add c15t@alpha @c15t/integrations@alpha`|
|yarn|`yarn add c15t@alpha @c15t/integrations@alpha`|
|bun|`bun add 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`:

```ts title="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.

```sh title=".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](/docs/concepts/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](/docs/concepts/modes#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:

|Command|Default when the fetch fails|
|--|--|
|Production build: `next build`, `vite build`, `nuxt build`, `astro build`|The build stops with an error.|
|Dev: `next dev`, `vite dev`, `nuxt dev`, `astro dev`|A 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:

```sh
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](/docs/concepts/modes#what-happens-when-the-download-fails)
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](/docs/frameworks/nuxt/rendering) covers the other
modes.

## Mount the consent UI

Add `ConsentRoot` and a preferences link to `app.vue`:

```vue title="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.

## Register consent-gated scripts

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.

```ts title="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.

```vue title="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](/docs/guides/verify-consent) has the full release checklist.

## Next steps

* [Rendering and deployment](/docs/frameworks/nuxt/rendering) covers
  prerendered and cached routes, `nuxt generate` and `ssr: false`.
* [Nuxt module](/docs/frameworks/nuxt/module) lists every option and where it
  goes.
* [Scripts](/docs/frameworks/nuxt/scripts), [embeds](/docs/frameworks/nuxt/embeds)
  and the [network blocker](/docs/frameworks/nuxt/network-blocker) cover
  gating and revocation.
* [Geography headers](/docs/frameworks/nuxt/geography-headers) explains how
  the server finds the visitor's location.
* [Customize](/docs/frameworks/nuxt/customize) changes colors, layout and copy.
* [Components](/docs/frameworks/nuxt/components) and
  [composables](/docs/frameworks/nuxt/composables) list the full API.
* [Troubleshooting](/docs/frameworks/nuxt/troubleshooting) covers common
  failures.
