Skip to main content

Nuxt

Rendering and deployment

Pick the setup for your deployment

Two module options in nuxt.config.ts decide where each visitor's policy comes from. mode is manifest(), hosted() or offline(), imported from c15t/vue. routePrefix is where the module adds its consent route.

Your deploymentc15t optionsBanner in the first HTML
A Nuxt server renders each request (nuxt build)None: mode defaults to manifest() and routePrefix to '/api/c15t'. See the quickstart.Yes
A Nuxt server with some prerendered or cached routes (routeRules with prerender, swr, isr or cache)The same. See prerendered and cached routes.No on those routes, yes elsewhere
Policy edits must apply without a rebuildmode: manifest({ source: 'runtime' }). See fetch the manifest at runtime.Yes
Static hosting with nuxt generatemode: manifest({ resolve: 'browser' }) and routePrefix: false, or mode: hosted(). See deploy to static hosting.No
A single-page app with ssr: falseThe same as static hosting. See build a single-page app.No

mode and routePrefix are read from nuxt.config.ts only. They decide what the build bundles and which routes the server has, so app.config.ts cannot change them. Import the modes from c15t/vue, the module entry: they return plain data that reaches the browser through runtime config. Passing a transport, such as manifest from c15t/vue/vue-plugin, throws during setup.

The module has one configuration for the whole app. It decides per route whether the HTML belongs to one visitor or to everyone.

Render each request on the server

By default, every server render reads the visitor's consent cookie and location headers, resolves their policy from the bundled snapshot, and sends the result with the page. The browser reuses it during hydration, so consent-gated scripts can start right after hydration and the banner needs no extra request. The browser bundle holds no resolver, snapshot or policy rules. The quickstart sets this up, and examples/nuxt in the c15t repository runs it.

The module adds one catch-all route at ${routePrefix}/**, /api/c15t/** by default. It answers GET /api/c15t/init, the visitor's resolved policy, and GET /api/c15t/manifest, the deployment's manifest. The server render resolves through the init route in-process, through Nitro's local fetch, so it never leaves the server. The browser uses the init route when it resolves the policy itself, for example on a prerendered route or after a server render that timed out. Change the path with routePrefix: '/consent'.

Server rendering waits at most timeoutMs, 500 milliseconds by default, for the policy. With a bundled snapshot that is local work. When the server fetches the manifest at runtime and the backend is slow or down, the page renders without consent UI in the HTML, optional categories stay denied, gated scripts and iframes stay blocked, and the browser resolves the policy after hydration.

A banner in the server HTML shows before the app hydrates, which on a slow phone can take seconds. The stock banner puts a small inline script in front of its buttons that holds an Accept all, Reject all, notice dismiss or Customize tap made in that gap. Accept, reject and dismiss hide the banner at once. When the banner hydrates and the runtime has started, c15t records the choice with the time of the tap, saves it and loads the scripts it allows; Customize opens the dialog. A tap on a banner for one model or prompt is dropped when the browser resolves another, and the banner shows again.

routePrefix: false adds no route. The server render still resolves each visitor from the bundled snapshot, and the browser asks the backend's /init whenever it resolves the policy itself, such as on a prerendered page. Keep the route unless something else owns /api/c15t.

Ask the backend on every render

mode: hosted() sends every server render to the backend's /init, which resolves the visitor's location itself. The module adds no route and bundles no snapshot:

nuxt.config.ts
import { hosted } from 'c15t/vue';

export default defineNuxtConfig({
	c15t: { mode: hosted() },
	modules: ['c15t/vue'],
});

That request carries the resolved location, language and GPC and the user-agent, and, over https or to a loopback host, the consent cookie alone, never the rest of the cookie jar. Every render waits on the backend, up to timeoutMs. Prefer the default manifest(), or manifest({ source: 'runtime' }) when you need policy updates without a rebuild.

Bundle the manifest during builds

The build fetches your public policy once and bundles it, so the server never fetches it at runtime.

The default mode, manifest(), bundles the manifest. The module reads NUXT_PUBLIC_C15T_BACKEND_URL, then NUXT_PUBLIC_INTH_PROJECT_URL, when the c15t key sets no backendURL, so nuxt.config.ts needs only the module:

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

The module fetches the snapshot during nuxt build and when nuxt dev starts, and embeds it in the server bundle. nuxt prepare does not fetch, so dependency installation and type generation do not need the backend. The consent route and the server render read the snapshot; it stays out of runtime config and out of the browser bundle. Only manifest({ resolve: 'browser' }) adds it to the browser bundle, because the browser then resolves the policy itself. Production server startup does not fetch another manifest.

The module skips the fetch for manifest({ source: 'runtime' }), a manifest({ snapshot }) you supply, and the hosted() and offline() modes. A relative backend URL is also skipped, unless onBuildError is 'fail'.

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.

Set onBuildError under the c15t key in nuxt.config.ts. In dev, and in a build with onBuildError: 'runtime', a failed fetch leaves the server without a snapshot, and the server fetches and caches the policy at runtime.

For policy updates without a rebuild, set mode: manifest({ source: 'runtime' }). The server then fetches and caches the policy at runtime, and refreshes it in the background.

Fetch the manifest at runtime

manifest({ source: 'runtime' }) skips the build-time download. The server fetches the public manifest the first time it needs it, caches it, and refreshes it in the background, so policy edits apply without a rebuild:

nuxt.config.ts
import { manifest } from 'c15t/vue';

export default defineNuxtConfig({
	c15t: { mode: manifest({ source: 'runtime' }) },
	modules: ['c15t/vue'],
});

By default the server fetches ${backendURL}/manifest. Set manifest({ manifestURL }) to fetch from another URL. The consent route and the server render share the cache. The first render after a cold start waits for the fetch, up to timeoutMs.

Prerendered and cached routes

A prerendered route, or one with a prerender, swr, isr or cache route rule, sends the same HTML to every visitor. The module renders that HTML without any visitor's consent record, location or request headers, so it has no banner. After hydration the browser requests the visitor's policy from /api/c15t/init and reads their stored choice, then shows the banner if the policy asks for one. With hosted(), or routePrefix: false, the browser asks the backend's /init instead.

nuxt.config.ts
export default defineNuxtConfig({
	routeRules: { '/blog/**': { swr: 3600 }, '/about': { prerender: true } },
});

Keep the quickstart setup. Routes without these rules still render per request with the banner in the HTML. Until the browser has the policy, optional categories stay denied and gated scripts and iframes stay blocked.

The browser also keeps the visitor's choice in localStorage. When localStorage holds a newer denial than the consent cookie the server read, the browser applies the denial, so an older cookie cannot undo a rejection.

Deploy to static hosting

nuxt generate prerenders every page with server rendering on, so crawlers get your content in the HTML. A static host has no Nuxt server, so no consent route exists and no server resolves visitors. With the default manifest(), nuxt generate logs a warning that it cannot resolve the policy there. Pick a mode that lets the browser resolve it:

  • manifest({ resolve: 'browser' }) with routePrefix: false. The build bundles the manifest into the browser bundle, and the browser resolves the policy. A policy that depends on location still asks the backend, as described below.
  • hosted(). The browser calls your backend's /init directly, and the backend resolves the visitor's location.
nuxt.config.ts
import { manifest } from 'c15t/vue';

export default defineNuxtConfig({
	c15t: {
		// A static host has no server: the browser resolves the policy the
		// build bundled, and the module adds no consent route.
		mode: manifest({ resolve: 'browser' }),
		routePrefix: false,
	},
	compatibilityDate: '2026-07-04',
	modules: ['c15t/vue'],
	ssr: false,
});

examples/nuxt-static uses ssr: false as well; nuxt generate works the same without it. Run nuxt generate and deploy .output/public. Every page renders without a banner in its HTML, and the browser resolves each visitor after hydration, as on a prerendered route.

With browser resolution, only the manifest and English copy are in the bundle. The resolver loads as its own chunk when the app starts, and a visitor who resolves to another language loads that language's text the first time it is needed.

The browser has no request location. When your policy depends on the visitor's country or region and neither inputs nor geoURL supplies one, the browser asks the backend's /init on the first visit, as every single-page app does. It doesn't apply the rule for an unknown location, which could be another region's. To resolve by location in the browser, pass manifest({ resolve: 'browser', geoURL: '/geo' }) with a route on your own origin, such as an edge function your host runs, that returns the visitor's country and region as JSON, for example { "country": "DE", "region": "BE" }. c15t resolves the policy again when the response arrives. Or use hosted().

Consent saves go to the backend from the browser, so the backend must allow your site's origin. With Inth, add it to the project's trusted origins.

Build a single-page app with ssr: false

ssr: false turns the site into a single-page app. Nuxt sends an empty shell and renders every page in the browser, so crawlers that do not run JavaScript see no content. That is the tradeoff. Prerendering works without it, so use ssr: false only when you want a single-page app for other reasons.

On a static host, use the static hosting config above. The module reads the visitor's stored choice from their own cookie, and the browser resolves the policy from the bundled manifest. The banner appears once the resolver chunk loads. examples/nuxt-static in the c15t repository builds this setup. With a Nuxt server, the default manifest() also works: the browser asks the server's /api/c15t/init.

With hosted(), the module adds a small inline script to the head of every ssr: false page, prerendered ones included, that starts the backend's /init request while the browser parses the HTML. The app uses that response once its JavaScript loads, so the policy request no longer waits for the app's bundle. The script contains your backend URL and nothing about the visitor. Turn it off with the module option initPrefetch: false, or for one route with routeRules: { '/path': { c15t: { initPrefetch: false } } }. Use the route rule when a client plugin changes c15t's configuration for that route, because the server cannot see that change and the early request would go unused.

Module options

The Nuxt module page lists mode, routePrefix, onBuildError, timeoutMs and reportSessions with their defaults.

The server reads the visitor's location from request headers. See geography headers for the list and for which ones to trust.

Verify your deployment

For request-time rendering, view the page source under a policy that asks for consent and find data-testid="consent-banner-root". Reject, reload, and confirm the banner stays closed.

For a prerendered or cached route, view the page source and confirm it has no consent-banner-root. Open the Network tab and reload. The browser requests /api/c15t/init after the page loads, and the banner appears. Reject, reload, and confirm the banner stays closed.

For static output with browser resolution, serve .output/public and open the Network tab. The browser makes no request to the backend's /manifest or /init, and none to /api/c15t. With hosted(), it requests the backend's /init once per page load. Reject, reload, and confirm the banner stays closed. Then follow verify consent for the vendor checks.