---
title: Scripts
description: Load vendor scripts by consent category in a Nuxt app with the c15t
  Nuxt module, what happens when a visitor withdraws consent, and how to move
  off @nuxt/scripts and nuxt-gtag.
group: frameworks
lastModified: "2026-10-10T16:01:45+01:00"
---
## 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`](#migrate-from-nuxtscripts) or
[migrate from `nuxt-gtag`](#migrate-from-nuxt-gtag).

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

```ts title="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](/docs/integrations/overview) gives the
helper and options for that vendor.

## Embeds and other requests

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

* [Embeds](/docs/frameworks/nuxt/embeds) gates iframes with `ConsentGate` or
  the iframe blocker.
* [Network blocker](/docs/frameworks/nuxt/network-blocker) holds `fetch` and
  XHR calls that match a rule until their category is allowed.

## When a visitor withdraws consent

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](/docs/frameworks/nuxt/callbacks#before-a-revocation-reload)
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](/docs/frameworks/nuxt/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](/docs/frameworks/nuxt/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](/docs/frameworks/nuxt/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](#keep-nuxtscripts-for-necessary-scripts).

### Map registry scripts to helpers

|`@nuxt/scripts`|c15t helper|
|--|--|
|`useScriptGoogleAnalytics`|[`gtag`](/docs/integrations/google-tag)|
|`useScriptGoogleTagManager`|[`googleTagManager`](/docs/integrations/google-tag-manager)|
|`useScriptMetaPixel`|[`metaPixel`](/docs/integrations/meta-pixel)|
|`useScriptTikTokPixel`|[`tiktokPixel`](/docs/integrations/tiktok-pixel)|
|`useScriptRedditPixel`|[`redditPixel`](/docs/integrations/reddit-pixel)|
|`useScriptSnapchatPixel`|[`snapchatPixel`](/docs/integrations/snapchat-pixel)|
|`useScriptXPixel`|[`xPixel`](/docs/integrations/x-pixel)|
|`useScriptLinkedInInsight`|[`linkedinInsights`](/docs/integrations/linkedin-insights)|
|`useScriptBingUet`|[`microsoftUet`](/docs/integrations/microsoft-uet)|
|`useScriptClarity`|[`clarity`](/docs/integrations/microsoft-clarity)|
|`useScriptHotjar`|[`hotjar`](/docs/integrations/hotjar)|
|`useScriptPostHog`|[`posthog`](/docs/integrations/posthog)|
|`useScriptSegment`|[`segment`](/docs/integrations/segment)|
|`useScriptMixpanelAnalytics`|[`mixpanelAnalytics`](/docs/integrations/mixpanel-analytics)|
|`useScriptPlausibleAnalytics`|[`plausibleAnalytics`](/docs/integrations/plausible-analytics)|
|`useScriptFathomAnalytics`|[`fathomAnalytics`](/docs/integrations/fathom-analytics)|
|`useScriptMatomoAnalytics`|[`matomoAnalytics`](/docs/integrations/matomo-analytics)|
|`useScriptUmamiAnalytics`|[`umamiAnalytics`](/docs/integrations/umami-analytics)|
|`useScriptRybbitAnalytics`|[`rybbitAnalytics`](/docs/integrations/rybbit-analytics)|
|`useScriptDatabuddyAnalytics`|[`databuddy`](/docs/integrations/databuddy)|
|`useScriptVercelAnalytics`|[`vercelAnalytics`](/docs/integrations/vercel-analytics)|
|`useScriptCloudflareWebAnalytics`|[`cloudflareWebAnalytics`](/docs/integrations/cloudflare-web-analytics)|
|`useScriptAhrefsAnalytics`|[`ahrefsAnalytics`](/docs/integrations/ahrefs-analytics)|
|`useScriptCrisp`, `ScriptCrisp`|[`crisp`](/docs/integrations/crisp)|
|`useScriptIntercom`, `ScriptIntercom`|[`intercom`](/docs/integrations/intercom)|
|`ScriptYouTubePlayer`|`ConsentGate` around an iframe. See [embeds](#move-youtube-and-google-maps-embeds).|
|`ScriptGoogleMaps`|`ConsentGate` around an iframe. See [embeds](#move-youtube-and-google-maps-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](/docs/integrations/building-integrations).

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

```ts title="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](#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](/docs/integrations/building-integrations) 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](/docs/frameworks/nuxt/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:

|`loadMode`|Until the category is allowed|Use 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](/docs/integrations/google-tag-manager#configure-consent-inside-the-container).

Set it on each Google helper in your `scripts` array:

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

The [Google Tag](/docs/integrations/google-tag) and
[Google Tag Manager](/docs/integrations/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:

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

```vue title="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](/docs/integrations/overview).

### 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](/docs/frameworks/nuxt/embeds) and the
[YouTube](/docs/integrations/youtube) and
[Google Maps](/docs/integrations/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-gtag`|c15t|
|--|--|
|`gtag.id` in `nuxt.config.ts`|`gtag({ id, category: 'measurement' })` in `app/consent-scripts.ts`|
|`gtag.config`|The `config` option on `gtag`|
|`gtag.tags` with several IDs|One `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](#tags-with-their-own-settings).|
|`gtag.initCommands` with `consent` `default`|Delete. 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](/docs/integrations/building-integrations).|
|`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](#stop-all-requests-until-consent).|
|`enabled: false` for an environment|Leave the helper out of `scripts` in that environment.|
|`url`, `loadingStrategy`|No 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`.

```ts title="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](#register-vendor-scripts), and choose a
[load mode](#choose-when-google-loads) 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.

### Stop all requests until consent

`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](/docs/frameworks/nuxt/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](/docs/integrations/building-integrations). 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](/docs/integrations/building-integrations#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](/docs/guides/verify-consent) has the full checklist.
