Skip to main content

Vue

Rendering and deployment

Pick the setup for your deployment

The Vue plugin runs in the browser only. The mode option of app.use(c15tVue, { mode }) decides where each visitor's policy comes from. Import the mode from c15t/vue/vue-plugin:

Your appmode
Vue with Vite, served as static filesmanifest(), as in the quickstart. The build bundles the policy and the browser resolves it. Plan location inputs or an unknown-location rule.
Policy edits must apply without a rebuildmanifest({ source: 'runtime' })
Regional policies that need the visitor's location from the backendhosted()
A prototype with no backendoffline(). Not recommended for production environments.
Vue rendered on the server without NuxtAny of the above; the banner mounts after hydration. For the banner in server HTML, use Nuxt.

mode is required. Without it, app.use() throws and asks for one. Each mode is imported statically, so the bundle carries only the modes your app imports.

Bundle the manifest during builds

The quickstart and examples/vue in the c15t repository use this setup.

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

consentManifest() from c15t/vue/vite downloads the policy during vite build, when the app uses manifest(), and in vite dev when the app first loads it:

vite.config.ts
import vue from '@vitejs/plugin-vue';
import { consentManifest } from 'c15t/vue/vite';
import { defineConfig } from 'vite';

export default defineConfig({ plugins: [vue(), consentManifest()] });

Without a backendURL option, the plugin reads VITE_C15T_BACKEND_URL, then VITE_INTH_PROJECT_URL, from the environment or .env. manifest() from c15t/vue/vue-plugin, with no options, reads the downloaded policy and the same backend URL, so src/main.ts needs no import from c15t/generated:

src/main.ts
import { posthog } from '@c15t/integrations/posthog';
import { c15tVue, manifest } from 'c15t/vue/vue-plugin';
import { createApp } from 'vue';

import App from './App.vue';

createApp(App)
	.use(c15tVue, {
		mode: manifest(),
		scripts: [
			posthog({
				id: 'phc_your_project_key',
				initOptions: { cookieless_mode: 'never' },
				loadMode: 'after-consent',
			}),
		],
	})
	.mount('#app');

The browser resolves the visitor's policy from the bundled snapshot, with no /manifest request. When the policy depends on the visitor's country or region and neither inputs nor geoURL supplies one, the browser asks the backend's /init instead. The app bundles the policy and English copy, and loads the resolver as its own chunk when it needs it.

When the build has no snapshot, as in vite dev after a failed download, manifest() fetches ${backendURL}/manifest when the app starts.

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.

vite build and vite dev fetch the manifest when they start. vite preview serves the last build without fetching. The plugin writes no file into your app, so there is nothing to keep out of Git. c15t/generated ships its own types, so tsc, vue-tsc and svelte-check pass on a fresh checkout without a build first.

The plugin can't see the options your app passes to manifest(). When the app passes source: 'runtime' or manifestURL, set consentManifest({ source: 'runtime' }) too. The build then downloads no manifest, so it doesn't fail when the backend's /manifest is down.

For policy edits without a rebuild, use manifest({ source: 'runtime' }), or hosted() when you need the backend's geolocation.

Tell the browser where the visitor is

The browser has no request location. When the policy depends on the visitor's country or region, manifest() asks the backend's /init on the first visit, as React and Svelte do, rather than apply the rule for an unknown location, which could be another region's. vite build warns when it bundles such a policy and suggests hosted(), because the bundled policy then saves no request. To resolve in the browser instead:

  • Pass country and region props to ConsentRoot when the page already knows the location, for example from a value your edge server injected.
  • Pass inputs: { country, region } to manifest() for the same purpose, set once when the plugin installs.
  • Set geoURL to a same-origin route you run that returns the visitor's { country, region } as JSON. manifest() requests it only when the policy depends on a location it does not have.
  • Pass initFallback: false to manifest() to resolve the policy for an unknown location without asking the backend. Only do this when that rule is right for every visitor it reaches.

Load the visitor's language

The bundle includes English copy. When a visitor resolves to another language your project has text for, the browser loads the base text for that language from your site the first time it is needed. A visitor downloads their own language only, never the full set.

Fetch the manifest at runtime

manifest({ source: 'runtime' }) ignores the build's snapshot and fetches ${backendURL}/manifest when the app starts. The browser resolves the policy from it, so policy edits apply without a rebuild:

src/main.ts
import { c15tVue, manifest } from 'c15t/vue/vue-plugin';

app.use(c15tVue, { mode: manifest({ source: 'runtime' }), scripts });

Keep consentManifest() in vite.config.ts: manifest() reads the backend URL from it. The plugin can't see options you pass in app code, so set consentManifest({ source: 'runtime' }) as well. Without it, the build still downloads the manifest, and fails when the backend is down.

To fetch the manifest from somewhere else, such as a CDN in front of your backend, pass manifest({ manifestURL }). The app fetches that URL when it starts and ignores the build's snapshot. As with source: 'runtime', set consentManifest({ source: 'runtime' }) so the build doesn't download the manifest. Consent saves still go to the backend URL from consentManifest().

The manifest is the same for every visitor, so a CDN can cache it. Location, geoURL and language loading work as in the build-time setup. Consent saves still go to the backend.

Ask the backend for each visitor's policy

hosted() sends each visitor's first page load to the backend's /init. The backend reads the visitor's location from the request, resolves the policy and returns the text in the visitor's language:

src/main.ts
import { c15tVue, hosted } from 'c15t/vue/vue-plugin';

app.use(c15tVue, { mode: hosted(), scripts });

hosted() reads the backend URL from consentManifest(), or takes it as hosted({ backendURL: 'https://your-project.inth.app' }). Keep consentManifest() in vite.config.ts either way: it also keeps the c15t components, which are .vue files, out of Vite's dependency pre-bundling. A hosted() build never downloads the manifest, so a backend outage does not stop it.

Until /init answers, every optional category is denied, so gated scripts and iframes wait. The build output is plain static files. Deploy it to any static host. The backend must allow your site's origin. With Inth, add it to the project's trusted origins. Data fetching compares the two paths.

Resolve the policy without a backend

offline() resolves policy rules in the browser and stores choices in the visitor's cookie and localStorage. There is no backend, so no consent records are kept. Not recommended for production environments.

src/main.ts
import { c15tVue, offline } from 'c15t/vue/vue-plugin';

app.use(c15tVue, { mode: offline(), scripts });

Without options, offline() uses c15t's recommended policy rules. Pass offline({ policyRules }) to replace them.

Server rendering without Nuxt

c15t has no server helper for plain Vue. The plugin accepts prefetch and initialRecords, but nothing produces them for a custom Vue server. A server-rendered page renders without the banner, and the banner appears after hydration.

Your theme tokens can still be in the first HTML. generateTokensCSS() from c15t/vue/vue-plugin returns the CSS the plugin applies. Put it in a <style id="c15t-css-vars"> element in your server HTML, and the plugin reuses that element in the browser:

import { generateTokensCSS } from 'c15t/vue/vue-plugin';

const tokensStyle = `<style id="c15t-css-vars">${generateTokensCSS(tokens)}</style>`;

For the banner in server-rendered HTML, use the c15t Nuxt module. Start with the Nuxt quickstart in the framework list. It resolves the policy on the server, renders the banner and tokens in the first HTML, and hydrates without another request.

Verify your deployment

Serve the production build and open the Network tab:

modePolicy requests on the first page load
manifest() with a build snapshotNone to the backend. A language other than English loads its text from your site.
manifest({ source: 'runtime' })One GET to ${backendURL}/manifest
manifest({ manifestURL })One GET to manifestURL
hosted()One GET to ${backendURL}/init
offline()None

Reject, reload, and confirm the banner stays closed. Then follow verify consent for the vendor checks.