Skip to main content

SvelteKit Scripts and embeds

Scripts

Register scripts in the root layout

Pass the array from src/lib/scripts.ts to ConsentRoot as the scripts prop in src/routes/+layout.svelte. The quickstart sets up both files:

src/lib/scripts.ts
import { posthog } from '@c15t/integrations/posthog';

export const scripts = [
	// Loads PostHog only after the visitor allows measurement.
	posthog({
		id: 'phc_your_project_key',
		initOptions: { cookieless_mode: 'never' },
		loadMode: 'after-consent',
	}),
];

Create the scripts in the layout component, not in +layout.server.ts. Script configurations hold functions, which a server load cannot send to the browser. Scripts load only in the browser, after hydration, so they never appear in server HTML.

Preload the script loader

The provider loads the script loader only on pages whose scripts array is not empty or that have network blocker rules. The loader and the blocker share one separate chunk. Pages with neither never download it. On a page with either, the browser would otherwise request the chunk after the app's JavaScript has run, and a returning visitor's consented scripts and held requests would wait for that extra request.

consentManifest() from @c15t/svelte/vite, already in vite.config.ts from the quickstart, lets c15tHandle link the chunk from the page's <head> with <link rel="modulepreload">. It does this whenever the sveltekit() plugin is present; there is nothing else to add.

SvelteKit builds the server before the client, so the server cannot know the chunk's file name. After the client build, consentManifest() writes the chunk URL into the server output, before SvelteKit prerenders pages and before the adapter copies the build. Then c15tHandle adds one link to every page whose provider has scripts or blocker rules, prerendered pages included. The link carries the provider's nonce, or else the nonce SvelteKit put on its own scripts. It asks for low priority (fetchpriority="low"): the runtime needs the chunk only after hydration, so the browser fetches your app's own chunks first. It does nothing in vite dev.

Without the plugin, or without c15tHandle in hooks.server.ts, scripts still load, one request later.

How registered scripts load

The provider's scripts prop takes an array of script configurations. Each has a category. The loader adds a script to the page when its category becomes allowed and removes it when the category is withdrawn. Nothing optional loads while the policy is still resolving, or when it fails.

Helpers in @c15t/integrations, such as posthog() from @c15t/integrations/posthog, return a configuration with the right category and the vendor's own consent calls. Integrations lists every helper. For an SDK without a helper, write a configuration with an id, category and src as shown in building integrations.

Remove the vendor's original <script> tag, app.html snippet or SDK import before you register it. A banner does not block code that loads some other way, and a vendor loaded twice sends events twice.

The scripts array is read when the provider is created. Build it once, at the top level of the component, not inside an effect.

Script options

Helpers set these for you. For a script without a helper, write the object yourself:

OptionDefaultBehavior
idrequiredA unique name. The loader uses it to add and remove the script once.
categoryrequiredThe category or condition, such as 'measurement' or { and: ['measurement', 'marketing'] }, that must be allowed.
src or textContentnoneThe script's URL, or inline code.
callbackOnlyfalseAdds no <script> element and only runs the callbacks. Use it to switch an SDK you load yourself on and off.
alwaysLoadfalseLoads at once, whatever the consent state. Only for scripts that apply consent themselves, such as a tag manager with Consent Mode.
persistAfterConsentRevokedfalseKeeps the element after withdrawal instead of removing it.
target'head'Where the element goes: 'head' or 'body'.
async, defer, fetchPriority, attributes, noncenoneSet on the <script> element.
anonymizeIdtrueGives the element a random id, so ad blockers do not match it by name.
vendornoneAlso waits for this vendor to be allowed, for vendor-level consent outside IAB.
onBeforeLoad, onLoad, onError, onConsentChange, onDisposenoneLifecycle callbacks. See callbacks.

Gate embeds

Iframes are not scripts. Wrap them in ConsentGate, which keeps the iframe out of the DOM until its category is allowed:

src/YouTubeEmbed.svelte
<script lang="ts">
	import { ConsentDialogLink, ConsentGate } from '@c15t/svelte';
</script>

<!-- The iframe mounts only while measurement is allowed. -->
<ConsentGate category="measurement">
	{#snippet placeholder()}<div class="placeholder">
			<p>Allow measurement to load this YouTube video.</p>
			<ConsentDialogLink>Choose video permissions</ConsentDialogLink>
		</div>{/snippet}
	<iframe
		title="YouTube video"
		src="https://www.youtube-nocookie.com/embed/czTksCF6X8Y?playsinline=1"
		allow="encrypted-media; picture-in-picture"
		allowfullscreen
	></iframe>
</ConsentGate>

For iframes from a CMS or Markdown, which you cannot wrap, use the iframe blocker's data-category and data-src attributes. Embeds covers both.

Block requests from code already on the page

The provider's networkBlocker option holds fetch and XMLHttpRequest calls to the domains you list until their category is allowed. It is a backstop for code you cannot move into scripts. Network blocker covers the rules and what it cannot stop.

Removing a script tag cannot stop code that already ran. So when a save withdraws a category that was granted, the provider reloads the page after the save request, and the new page starts with only the permitted code. Set reloadOnConsentRevoked: false on the provider if you handle withdrawal yourself, for example through a vendor's own opt-out call in onConsentChange.

ConsentGate content unmounts without a reload. To delete first-party cookies a vendor set, configure clearOnRevocation; see clearing data on revocation.

Let visitors turn off one vendor

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

Verify vendor loading

Open DevTools, clear site data for your origin and reload:

  1. With the banner showing, the Network panel has no requests to your vendors, and gated iframes are absent from the Elements panel.
  2. Allow one category in preferences. Only that category's vendors load, and its iframes appear.
  3. Reject, reload, and confirm the vendor requests stay absent.
  4. Withdraw a category you allowed. The page reloads and its vendors no longer load.

Verify consent covers automated checks.