Skip to main content

Astro Scripts and embeds

Scripts

Register vendor scripts

The ConsentBanner component does not stop a <script> tag you already have. Give c15t each vendor script to load instead, and remove the vendor's own snippet so it loads once.

Vendor helpers from @c15t/integrations contain callbacks, and the integration options in astro.config.mjs must survive JSON serialization. So register helpers in src/c15t.client.ts, which the integration finds on its own, or the module its clientEntrypoint option names. This list loads PostHog on measurement:

src/c15t.client.ts
import { posthog } from '@c15t/integrations/posthog';
import type { C15tClientOptionsExtension } from 'c15t/astro';

export default {
	scripts: [
		posthog({
			id: 'phc_your_project_key',
			initOptions: { cookieless_mode: 'never' },
			loadMode: 'after-consent',
		}),
	],
} satisfies C15tClientOptionsExtension;

The integration imports that module into the page's boot script, so every page shares one script loader. Each helper's guide under integrations lists its options and the requests to expect.

Add a script without a helper

A script with no callbacks can go straight into the integration options. Give it an id, a category, and either a src or inline textContent:

astro.config.mjs (partial)
c15t({
	scripts: [
		{
			id: 'example-analytics',
			category: 'measurement',
			src: 'https://analytics.example.com/script.js',
		},
	],
});

Scripts from astro.config.mjs and from the client entrypoint both load.

What a site without scripts skips

The script loader is part of the page's boot script only when the site configures scripts in astro.config.mjs or sets a clientEntrypoint, which may add some. A site with neither never downloads it, about 4 KB gzip less on every page. The network blocker works the same way: it ships in the boot script when the integration options have networkBlocker rules, and loads as its own chunk when only the client entrypoint sets them.

Gate an inline script

For a script that has to stay in the page's markup, make it inert and label it with a category. c15t runs it once the category is allowed:

src/pages/index.astro (partial)
<script data-c15t-category="measurement" is:inline type="text/plain">
	document.getElementById('inline-script-status').textContent =
		'Measurement script ran';
</script>

Three attributes matter:

  • type="text/plain" stops the browser from running the script.
  • data-c15t-category names one category. An unknown name logs a warning and the script never runs.
  • is:inline makes Astro ship the tag as written. Without it, Astro bundles the script and runs it regardless of consent.

Add data-c15t-vendor with a vendor slug to also hold the script while the visitor has switched that vendor off. See vendor consent.

Under a nonce-based Content Security Policy, where your middleware sets Astro.locals.c15t.nonce, also add nonce={Astro.locals.c15t?.nonce} to the tag. c15t then activates only gated tags that carry the page's nonce, and skips the rest with a console warning. See put the nonce on your gated scripts.

c15t checks gated scripts when the page loads, after each consent change and after each ClientRouter navigation, so a script on a page you navigate to runs as soon as it is allowed. A script with src works the same way. For markup you insert later from your own code, call activateGatedScripts(getConsent(), container) from c15t/astro/client. Under a nonce policy, give the inserted tags the nonce and pass it as a third argument.

A script that has run cannot be undone. When the visitor withdraws consent, c15t reloads the page, and the reloaded page leaves the script inert.

Gate embeds and requests

An iframe loads as soon as it is in the page, so gate embeds separately. See Embeds for a component that renders an iframe only while its category is allowed, and for the iframe blocker.

To stop fetch and XMLHttpRequest calls to tracking hosts until consent, add rules to networkBlocker. See Network blocker.

Set clearOnRevocation in the integration options to remove a vendor's cookies and storage keys when its category is withdrawn. See clear on revocation for the shape.

By default c15t reloads the page after a save turns off a category that was allowed, because it cannot stop code that has already run. Set reloadOnConsentRevoked: false in the integration options only if every script on the page stops itself when its category is withdrawn. Stop your own code from the onPermissionsChanged callback. See Callbacks.

Check script loading

Build the site and open it in a private window with DevTools Network open:

  1. Before you choose, filter for each vendor's domain. There are no requests, and a gated inline script has not run.
  2. Allow one category from Privacy settings. Only that category's vendors load, and inline scripts gated on it run.
  3. Reload. The allowed vendors load again, and the others stay absent.
  4. Withdraw the category and save. The page reloads and the vendor does not load.

See Verify consent for the full checklist.