Skip to main content

Nuxt Scripts and embeds

Scripts

Register vendor scripts

A banner does not stop a script you load with a <script> tag, useHead or a vendor's Nuxt module. Remove those loaders and register the vendor with c15t instead, so c15t loads it only while its category is allowed. For @nuxt/scripts and nuxt-gtag, follow migrate from @nuxt/scripts or migrate from nuxt-gtag.

Register the helpers from @c15t/integrations under the c15t key in app/app.config.ts:

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

The module starts one script loader in the browser after hydration, once it has applied the visitor's stored choice and privacy signals. Do not also call createScriptLoader yourself, or each script loads twice.

scripts must go in app.config.ts. Module options in nuxt.config.ts reach the browser as JSON, which drops the functions inside each script.

Every vendor guide under integrations gives the helper and options for that vendor.

Embeds and other requests

Scripts cover vendor code c15t loads for you. For the rest:

  • Embeds gates iframes with ConsentGate or the iframe blocker.
  • Network blocker holds fetch and XHR calls that match a rule until their category is allowed.

Removing a script tag cannot stop code that already ran. When a save turns off a category or vendor that was allowed, c15t reloads the page so the new document starts with only permitted code. Set reloadOnConsentRevoked: false to handle revocation yourself, or use the onBeforeConsentRevocationReload callback to run code before the reload.

clearOnRevocation deletes first-party cookies and storage keys that belong to a category when it is withdrawn. It is plain data, so it can go in nuxt.config.ts. See clear on revocation.

Let visitors turn off one vendor

A visitor can allow marketing and still switch off one vendor in it. Declare the vendors in the vendors option; helpers from @c15t/integrations already carry their vendor slug. See vendor consent.

Content Security Policy

Allow each vendor's script host in your script-src directive. The module's nonce option is fixed when the app builds, so prefer the host allowlist. See Content Security Policy.

Migrate from @nuxt/scripts

@nuxt/scripts loads a registry script when its own trigger fires, whatever the visitor chose in c15t. That covers a useScriptGoogleAnalytics() call in a component, a scripts.registry entry with a trigger in nuxt.config.ts, and components such as ScriptYouTubePlayer. Move every tracking vendor to c15t's scripts. You can keep @nuxt/scripts for necessary scripts.

Map registry scripts to helpers

@nuxt/scriptsc15t helper
useScriptGoogleAnalyticsgtag
useScriptGoogleTagManagergoogleTagManager
useScriptMetaPixelmetaPixel
useScriptTikTokPixeltiktokPixel
useScriptRedditPixelredditPixel
useScriptSnapchatPixelsnapchatPixel
useScriptXPixelxPixel
useScriptLinkedInInsightlinkedinInsights
useScriptBingUetmicrosoftUet
useScriptClarityclarity
useScriptHotjarhotjar
useScriptPostHogposthog
useScriptSegmentsegment
useScriptMixpanelAnalyticsmixpanelAnalytics
useScriptPlausibleAnalyticsplausibleAnalytics
useScriptFathomAnalyticsfathomAnalytics
useScriptMatomoAnalyticsmatomoAnalytics
useScriptUmamiAnalyticsumamiAnalytics
useScriptRybbitAnalyticsrybbitAnalytics
useScriptDatabuddyAnalyticsdatabuddy
useScriptVercelAnalyticsvercelAnalytics
useScriptCloudflareWebAnalyticscloudflareWebAnalytics
useScriptAhrefsAnalyticsahrefsAnalytics
useScriptCrisp, ScriptCrispcrisp
useScriptIntercom, ScriptIntercomintercom
ScriptYouTubePlayerConsentGate around an iframe. See embeds.
ScriptGoogleMapsConsentGate around an iframe. See embeds.

Registry options do not carry over one to one. Copy each vendor's ID into the helper, then check the vendor guide for the rest. Meta's id becomes pixelId, and Tag Manager's l or dataLayer becomes dataLayer. The gtag helper always uses window.dataLayer, so Google Analytics' l has no equivalent. The googleTagManager helper has no options for Tag Manager's auth, preview, envName, cookiesWin or onBeforeGtmStart. A container environment that needs them, or a vendor without a helper such as AdSense, needs a custom integration.

Put the helpers in app/consent-scripts.ts, keeping the same IDs:

app/consent-scripts.ts
import { gtag } from '@c15t/integrations/google-tag';
import { googleTagManager } from '@c15t/integrations/google-tag-manager';
import { metaPixel } from '@c15t/integrations/meta-pixel';

export const scripts = [
	// Was useScriptGoogleAnalytics({ id: 'G-XXXXXXXXXX' })
	gtag({ category: 'measurement', id: 'G-XXXXXXXXXX' }),
	// Was useScriptGoogleTagManager({ id: 'GTM-XXXXXXX' })
	googleTagManager({ id: 'GTM-XXXXXXX' }),
	// Was useScriptMetaPixel({ id: '123456789012345' })
	metaPixel({ pixelId: '123456789012345' }),
];

Register scripts in app/app.config.ts as in register vendor scripts. Then delete the useScript* calls for those vendors, their scripts.registry entries in nuxt.config.ts, and any scripts.globals entry that loads a tracker. A leftover composable call loads a second copy of the vendor outside c15t.

The helpers load each vendor from the host in its guide, usually the vendor's own domain, and the vendor's requests go straight to it. @nuxt/scripts bundles some registry scripts and proxies many by default, including Google Analytics, when the app has a server runtime. Before a request reaches the vendor, the proxy truncates the visitor's IP address, strips cookie and authorization headers, and, depending on the script, generalizes fingerprinting fields such as language and hardware. After the move, the vendor receives these unchanged.

If your privacy notice or legal review relies on that proxy, keep the vendor's requests on your own domain: write a custom integration that loads the script and points the vendor's collection endpoint at a reverse proxy you run. Otherwise, update the notice. In both cases, allow the hosts the browser now contacts in your Content Security Policy.

Choose when Google loads

The gtag and googleTagManager helpers take a loadMode option. It decides whether the page contacts Google before the helper's category is allowed:

loadModeUntil the category is allowedUse it when
'always' (default)c15t loads Google's script, sends Consent Mode default with the current permissions, then sends update as they change. Google receives requests with the optional consent types denied.You want Consent Mode signals from visitors who have not allowed the category.
'after-consent'c15t sends no request to Google.Your site must make no request to Google before opt-in. You give up Consent Mode's cookieless pings and conversion modeling for visitors who haven't allowed the category.

With 'after-consent', gtag waits for its category, and googleTagManager waits for measurement or marketing because a container usually holds both kinds of tag. Pass category to googleTagManager to change that, for example category: 'measurement' for an analytics-only container. Until the helper loads, window.gtag doesn't exist, and neither does window.dataLayer unless you seeded it, so event calls written as window.gtag?.(...) do nothing.

'after-consent' waits for the category to be allowed, not for a recorded choice. Under an opt-in policy, that happens when the visitor allows it. Under an opt-out or none policy, optional categories are allowed before a choice, so the helper loads on the first page.

In either mode, the container runs every tag in it once it starts. Google tags inside follow Consent Mode, but Custom HTML tags and third-party pixels fire on their own triggers. With the default category, a visitor who allowed only measurement starts the container, and a marketing pixel in it loads too. With 'always', both load before any choice. Add a consent check to each of those tags in GTM, or move the vendor out of the container to its own c15t helper. See configure consent inside the container.

Set it on each Google helper in your scripts array:

gtag({
	id: 'G-XXXXXXXXXX',
	category: 'measurement',
	loadMode: 'after-consent',
}),
googleTagManager({ id: 'GTM-XXXXXXX', loadMode: 'after-consent' }),

The Google Tag and Google Tag Manager guides list what each mode sends and how to check it in DevTools.

Remove useScriptTriggerConsent

useScriptTriggerConsent loads a script once its own consented ref turns true, through accept() or the consent option. c15t records consent separately, so you end up with two answers that can disagree. If any code calls accept(), the script loads even after the visitor rejects in the c15t banner. Global Privacy Control, vendor switches and policy changes in c15t never reach the trigger. Delete every useScriptTriggerConsent call and the banner code that called accept() or revoke(). c15t decides when each registered script loads.

Remove the Consent Mode calls too: the defaultConsent option and consent.update() on the Google registry scripts. The gtag and googleTagManager helpers send default and update from the visitor's c15t permissions, and a second source sends conflicting commands.

Keep your event calls

A registry composable returns a proxy that queues calls until its script loads. Calling the composable to get that proxy also loads the script through @nuxt/scripts, so event code has to stop calling it. Call the global the vendor defines once c15t loads it:

BeforeAfter
proxy.gtag(...) from useScriptGoogleAnalytics()window.gtag?.(...)
proxy.dataLayer.push(...) from useScriptGoogleTagManager()window.dataLayer?.push(...), or window[name] with a custom dataLayer name
proxy.fbq('track', ...) from useScriptMetaPixel()metaPixelEvent(...) from @c15t/integrations/meta-pixel, after checking marketing permission

The proxy did nothing during server rendering. window does not exist there, so call these from an event handler or onMounted:

app/components/PurchaseButton.vue
<script setup lang="ts">
import { metaPixelEvent } from '@c15t/integrations/meta-pixel';

const props = defineProps<{ value: number }>();
const consent = useConsent();

const onPurchase = () => {
	// Was proxy.gtag('event', 'purchase', ...) from useScriptGoogleAnalytics()
	window.gtag?.('event', 'purchase', { currency: 'USD', value: props.value });

	// Was proxy.fbq('track', 'Purchase', ...) from useScriptMetaPixel().
	// fbq stays defined after a revocation until the page reloads, so check
	// the permission, not the global.
	if (consent.value.marketing) {
		metaPixelEvent('Purchase', { currency: 'USD', value: props.value });
	}
};
</script>

<template>
	<button type="button" @click="onPurchase">Buy now</button>
</template>

Until c15t has set up a vendor, its global is undefined and the optional call drops the event. Other vendors' event APIs are in their integration guides.

Move YouTube and Google Maps embeds

ScriptYouTubePlayer puts a dns-prefetch or preconnect link for i.ytimg.com in the server HTML and loads the video thumbnail from YouTube before the visitor clicks. ScriptGoogleMaps puts a dns-prefetch link for maps.googleapis.com in the server HTML. For a video or a map that only needs to display, render the iframe inside ConsentGate, as in embeds and the YouTube and Google Maps guides.

If you need the YouTube IFrame Player API or the Maps JavaScript API, such as player events or markers, keep the @nuxt/scripts component and put it inside ConsentGate. The component then renders only while the category is allowed, and its links and requests wait with it.

Keep @nuxt/scripts for necessary scripts

c15t does not need to own every script. A first-party script or an SDK your checkout needs, such as Stripe, can stay on @nuxt/scripts with its usual trigger, provided your policy does not ask consent for it. Do not load a tracker there.

Migrate from nuxt-gtag

The nuxt-gtag client plugin adds gtag.js to every page as soon as the app loads, unless you set initMode: 'manual'. c15t cannot gate it. Replace the module's configuration and composables:

nuxt-gtagc15t
gtag.id in nuxt.config.tsgtag({ id, category: 'measurement' }) in app/consent-scripts.ts
gtag.configThe config option on gtag
gtag.tags with several IDsOne gtag helper, when the extra IDs need no config of their own. Add them as destinations of that Google tag in Google's tag settings. Otherwise see tags with their own settings.
gtag.initCommands with consent defaultDelete. The helper sends default from the visitor's permissions.
initMode: 'manual' and useGtag().initialize()Delete. c15t loads the tag. The helper needs its id when you register scripts, so an ID passed to initialize() at runtime, as in nuxt-gtag's multi-tenancy setup, needs a custom integration.
useGtag().gtag(...)window.gtag?.(...)
useTrackEvent(name, params)window.gtag?.('event', name, params)
useGtag().disableAnalytics() and enableAnalytics()Delete, and set loadMode: 'after-consent' if you used them to stop all Analytics requests. See stop all requests until consent.
enabled: false for an environmentLeave the helper out of scripts in that environment.
url, loadingStrategyNo equivalent. The helper loads gtag.js from googletagmanager.com with async.

Register one gtag helper only. c15t tracks scripts by ID, and every gtag helper uses the script ID gtag.

app/consent-scripts.ts
import { gtag } from '@c15t/integrations/google-tag';

export const scripts = [
	// Was gtag: { id: 'G-XXXXXXXXXX' } in nuxt.config.ts. Move a `gtag.config`
	// object to the `config` option unchanged.
	gtag({ category: 'measurement', id: 'G-XXXXXXXXXX' }),
];

Register scripts in app/app.config.ts as in register vendor scripts, and choose a load mode for the tag. Then remove 'nuxt-gtag' from modules and the gtag key from nuxt.config.ts, and uninstall the package, for example with npm uninstall nuxt-gtag. useGtag and useTrackEvent were auto-imports, so nuxt typecheck reports any call you missed.

Call window.gtag from browser code. useGtag() returned a function that did nothing on the server, and window does not exist there.

disableAnalytics() sets Google's ga-disable-<ID> flag, so gtag.js sends nothing to Analytics for that ID. The gtag helper's default loadMode: 'always' works differently: it loads gtag.js before consent and sends Consent Mode default with the optional types denied. Google then receives cookieless pings. Turning Google off with vendor consent sends the same denied signals.

If you called disableAnalytics() so that a visitor who has not opted in sends nothing, set loadMode: 'after-consent' on the helper. gtag.js then waits for the helper's category.

Tags with their own settings

The gtag helper sends one config command, for its id. A gtag.tags entry with its own config, or a mix of Analytics IDs with Google Ads or Floodlight IDs, has no direct mapping:

  • Destinations added in Google's tag settings share the helper's config.
  • With loadMode: 'after-consent', every destination waits for the helper's one category. An Ads ID under measurement stays off for a visitor who allowed only marketing.

For these setups, replace the gtag helper with a custom integration. It loads gtag.js once, sends Consent Mode default and update from the visitor's permissions, as in load before consent and signal the vendor, and sends a config command for each tag with that tag's settings. To send nothing before opt-in, leave out alwaysLoad and set category: { or: ['measurement', 'marketing'] }. That condition only decides when gtag.js loads. Set each Google consent type from its own category, so Consent Mode keeps the other purpose's destinations restricted: measurement sets analytics_storage, and marketing sets ad_storage, ad_user_data and ad_personalization.

Verify gating

In a private window, open the Network tab and load a page under a policy that asks for consent. Requests to your vendors are absent. Allow one category and save, and only that category's vendors load. Withdraw it, and the page reloads without loading the vendor again. Verify consent has the full checklist.