---
title: Rendering and deployment
description: Choose the c15t Nuxt module's mode and routePrefix for server
  rendering, prerendered and cached routes, nuxt generate on static hosting, and
  ssr false.
group: frameworks
lastModified: "2026-10-10T16:01:45+01:00"
---
## 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 deployment|`c15t` options|Banner 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](/docs/frameworks/nuxt/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](#prerendered-and-cached-routes).|No on those routes, yes elsewhere|
|Policy edits must apply without a rebuild|`mode: manifest({ source: 'runtime' })`. See [fetch the manifest at runtime](#fetch-the-manifest-at-runtime).|Yes|
|Static hosting with `nuxt generate`|`mode: manifest({ resolve: 'browser' })` and `routePrefix: false`, or `mode: hosted()`. See [deploy to static hosting](#deploy-to-static-hosting).|No|
|A single-page app with `ssr: false`|The same as static hosting. See [build a single-page app](#build-a-single-page-app-with-ssr-false).|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](/docs/frameworks/nuxt/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.

### Without the consent route

`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:

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

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

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:

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

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

```ts title="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](#prerendered-and-cached-routes).

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](#deploy-to-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](/docs/frameworks/nuxt/module#backend-and-rendering) page
lists `mode`, `routePrefix`, `onBuildError`, `timeoutMs` and
`reportSessions` with their defaults.

The server reads the visitor's location from request headers. See
[geography headers](/docs/frameworks/nuxt/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](/docs/guides/verify-consent) for the
vendor checks.
