Skip to main content

Vue

Quickstart

Before you start

This guide is for a Vue 3 single-page app built with Vite. The build bundles your project's public policy, and the browser resolves each visitor's policy from it after the page loads, so it works on any static host. For a Vue app that renders on the server, use the c15t Nuxt module. 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.

Bundling the policy at build time is the recommended production setup. Rebuild after changing policies, translations or vendors.

Install c15t

npm install c15t@alpha @c15t/integrations@alpha

Add the Vite plugin

Add consentManifest() from c15t/vue/vite to vite.config.ts, next to @vitejs/plugin-vue:

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()] });

Set VITE_C15T_BACKEND_URL to your project's backend URL, including any path prefix, in .env or wherever you build:

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

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

consentManifest() downloads your project's public policy from /manifest during vite build, or in vite dev when the app first loads it, and hands it and the backend URL to the Vue plugin. A build fetches only when the app uses manifest(). No file is written into your project. It also keeps the c15t components, which ship as .vue files, out of Vite's dependency pre-bundling.

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.

Install the Vue plugin

Install the plugin from c15t/vue/vue-plugin in src/main.ts:

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');

c15t/vue is the Nuxt module. Do not pass it to app.use().

The plugin creates the consent runtime for this app. When the app mounts, it reads the visitor's stored choice, resolves their policy, and starts the script loader for the scripts you pass. mode: manifest() resolves the policy in the browser from the snapshot consentManifest() downloaded, with no /manifest or /init request, and sends consent choices to the same backend URL. Vite reads VITE_C15T_BACKEND_URL at build time, so set it before you build, not when you serve the files.

The browser does not know the visitor's location, so without inputs or geoURL on manifest(), or a country and region passed to ConsentRoot, every visitor gets the policy your project assigns when the location is unknown. Configure that fallback rule in your project, or see rendering. The bundle carries English copy; other languages load on demand.

To apply policy edits without rebuilding, use manifest({ source: 'runtime' }) or hosted(). Rendering compares the modes.

Remove any existing PostHog snippet so the script loads once, and add measurement to your Inth policy. The scripts option in src/main.ts loads PostHog once the visitor allows measurement.

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

src/App.vue
<script setup lang="ts">
import { ConsentDialogLink, ConsentRoot } from 'c15t/vue/vue-plugin';
</script>

<template>
	<ConsentRoot />
	<main>
		<h1>c15t with Vue</h1>
		<p>PostHog loads after you allow measurement.</p>
	</main>
	<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. Without slot content, its label is the dialog's title in the visitor's language: Privacy Settings in English. It renders nothing until the policy offers preferences. Replace the <main> element with your own content or router view. Keep ConsentRoot in the root component so it stays mounted. The components bring their own styles; there is no stylesheet to import.

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.

src/VideoEmbed.vue
<script setup lang="ts">
import { ConsentDialogLink, ConsentGate } from 'c15t/vue/vue-plugin';
</script>

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

Render <VideoEmbed /> in a page where the video belongs.

Verify the setup

Build and preview the production app with vite build and vite preview. Open it in a private window with your browser's DevTools on the Network tab, under a policy that asks for consent.

  1. Load the page. The bundled manifest resolves the policy and the banner appears, with no /manifest or /init request. Test the unknown-location rule, or pass geoURL to manifest() if you need regional policies. No requests to posthog.com or youtube-nocookie.com appear.
  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