Skip to main content

Astro Reference

Integration options

What the integration does

Astro islands never share a component tree, so c15t on Astro has no provider component. The c15t() integration in astro.config.mjs takes its place. It adds four things to your site:

  • A middleware that resolves consent for each request into Astro.locals.c15t, which every component reads.
  • A page script that starts one consent runtime per page load. Every script and island on the page shares it.
  • c15t's styles: base and configured IAB banner rules inline in each page, and dialog rules linked before their surface mounts. On a Tailwind CSS 3 site, the full base and configured IAB stylesheets run through your CSS pipeline instead.
  • In manifest() mode, one route at /api/c15t/[...path] that answers /api/c15t/init and /api/c15t/manifest.
astro.config.mjs
import node from '@astrojs/node';
import svelte from '@astrojs/svelte';
import { defineConfig } from 'astro/config';
import c15t from 'c15t/astro';

export default defineConfig({
	adapter: node({ mode: 'standalone' }),
	integrations: [svelte(), c15t()],
	output: 'server',
});

c15t() works with no options: the backend URL comes from PUBLIC_C15T_BACKEND_URL and the mode defaults to manifest(). List the Astro integration of your ui framework before c15t(). Import c15t and the mode helpers from c15t/astro.

Options must survive JSON

The integration serializes its options once at build time and hands the same JSON to the middleware, the components and the browser. Functions, class instances and other live values do not survive that, so c15t() throws when an option holds a function, and names where it is. Put callbacks, vendor helpers from @c15t/integrations and anything else with a function in src/c15t.client.ts, the module clientEntrypoint names.

The options are also written into every page, so keep secrets out of them. The backend URL is public configuration, not a secret.

backendURL

Your Inth or self-hosted backend. The browser saves consent there with POST /subjects, hosted() asks its /init, and manifest() reads ${backendURL}/manifest. It defaults to PUBLIC_C15T_BACKEND_URL, then PUBLIC_INTH_PROJECT_URL, read from the environment or a .env file in the project root when astro.config.mjs loads. A hosted({ backendURL }) of its own wins over both.

On the server, a relative backend URL or manifestURL, such as /api/self-host, resolves against the URL Astro gives the request, Astro.url. c15t does not read x-forwarded-host or other forwarding headers to build it.

mode

Where the visitor's policy comes from. Build it with one of the three helpers from c15t/astro; they return plain data. Without mode, the integration uses manifest().

HelperResolves consentNeeds
manifest()On your server, from your project's public policy, downloaded during the buildA server adapter and a backend URL
manifest({ source: 'runtime' })On your server, from the policy fetched and cached at runtimeA server adapter and a backend URL
manifest({ resolve: 'browser' })In the browser, from the manifestA backend URL. Works on static output: the integration prerenders ${routePrefix}/manifest
hosted()At your backend's /init, from the server on request-rendered pages and from the browser otherwiseA backend URL
offline()In the browser and on your server, with no backendNothing. Not recommended for production environments.

manifest() accepts:

FieldTypeEffect
source'build' | 'runtime''build', the default, uses the snapshot the build downloaded, or fetches at runtime when the build has none. 'runtime' always fetches at runtime.
snapshotConsentManifestA manifest you supply, used instead of downloading one. Not combined with source.
resolve'server' | 'browser'Where the policy resolves. 'server' by default.
manifestURLstringWhere the manifest is fetched. Defaults to ${backendURL}/manifest.
geoURL, inputsstring, { country?, region? }The visitor's location, for resolve: 'browser' only.

hosted() accepts:

FieldTypeEffect
backendURLstringBackend base URL, in place of the top-level backendURL. Absolute, or same-origin such as /api/self-host
headersRecord<string, string>Extra headers sent with /init from the server and the browser. From the server, only geography, language and privacy-signal headers are sent

offline() accepts policyRules, the rules to resolve locally. Without them it resolves the recommended rule pack. Choices persist in the visitor's cookie and localStorage only, and there are no consent records.

manifest() and hosted() stop astro.config from loading without a backend URL, because the server would have no policy to read and the browser nowhere to save consent. A manifest({ snapshot }) needs none for the policy.

Rendering and deployment explains which mode fits which Astro output.

routePrefix

The path of the route the integration injects in manifest() mode: '/api/c15t' by default. One catch-all, ${routePrefix}/[...path], answers ${routePrefix}/init and ${routePrefix}/manifest. A page whose consent changes after it loads, such as after a language change, asks it again.

The route renders on demand, so astro build with manifest() needs a server adapter. manifest({ resolve: 'browser' }) on static output prerenders only ${routePrefix}/manifest. hosted() and offline() inject nothing. Set routePrefix: false to inject nothing in manifest() mode. The browser then asks the backend's /init, not a route of your own. See serve the routes yourself.

OptionTypeDefaultEffect
onBuildError'fail' | 'runtime'UnsetWhat a failed build-time manifest fetch does in manifest() mode. Unset, astro build stops and astro dev logs a warning, and the server fetches the policy at runtime. 'fail' stops both. 'runtime' lets both continue. A missing backend URL counts as a failed fetch. The C15T_ON_BUILD_ERROR environment variable overrides it. See build-time manifests.
reportSessionsbooleantrueIn manifest() mode, reports each visitor the server resolves to the backend, so visitor counts stay complete.
consentCategoriesAllConsentNames[]Inferred from scripts and the policyCategories the banner and dialog offer
scriptsScript[]NoneScripts without callbacks, loaded when their category is allowed. See Scripts
vendorsVendor[]NoneVendors the dialog lists with their own switch inside a category. Merged with vendors from the backend manifest. See Vendor consent
networkBlocker{ rules, enabled?, logBlockedRequests? } | falseOffBlocks matching fetch and XMLHttpRequest calls until consent. See Network blocker
clearOnRevocationClearOnRevocationConfigNoneCookies and storage keys to remove when a category is withdrawn
reloadOnConsentRevokedbooleantrueReloads the page after a save turns off a category that was allowed
storageConfigStorageConfig{ storageKey: 'c15t' }Where the choice is stored

storageConfig takes storageKey, the cookie and localStorage name, crossSubdomain to share the cookie across subdomains, defaultDomain to set the cookie domain yourself, and defaultExpiryDays, which defaults to a year. The server reads the same cookie name, so set it here and not only in the browser.

Appearance and copy

OptionTypeDefaultEffect
themeThemeStock tokensTokens rendered on the server into <style id="c15t-theme">, plus consentActions button styles
colorScheme'system' | 'light' | 'dark' | 'none''system'Who sets the c15t-dark class on <html>
presentation{ prompt?, preferences? }Floating bannerBanner variant, position, action layout and blocking, and the dialog's blocking and default selections
legalLinks{ privacyPolicy?, cookiePolicy?, termsOfService? }NoneEach an { href, label? }. Without label, a link reads as the translated name for its type. ConsentBanner and ConsentDialog show the ones their legalLinks prop lists
i18n{ locale?, messages?, detectLanguage? }Negotiated from Accept-LanguageLanguage and wording. See Translations
stylesbooleantrueInlines base and configured IAB banner rules once per page. Dialog rules load before the surface mounts; IAB panel rules load only for the IAB dialog. Tailwind CSS 3 uses the full external stylesheets. false delivers no styles, so you import the base and optional IAB stylesheets yourself

Customize shows each of these in use.

ui

ui picks the framework that renders the preference dialog and the IAB dialog islands: 'svelte', 'react' or 'vue'. Unset, it is the framework of the one Astro integration among @astrojs/svelte, @astrojs/react and @astrojs/vue your site registers, so the dialog reuses a runtime the site already loads. With none or several of them registered, it is 'svelte'. Install that framework's Astro integration and list it before c15t().

requireUIIntegration, default true, stops the build when the ui framework's Astro integration is missing. Set it to false for a site that never opens a dialog, such as one that only shows a notice.

See Dialog islands and your own islands.

iab

IAB TCF configuration, or false. Takes cmpId, cmpVersion, vendors, publisherCountryCode, publisherRestrictions, gvl, gvlURL and enabled, all plain data. See IAB TCF for each field.

middleware

true (default), false, or an object:

FieldTypeDefaultEffect
enabledbooleantrueRegisters the middleware that sets Astro.locals.c15t
skipstring[][]Path prefixes the middleware leaves alone. '/api' covers /api/health but not /apidocs
timeoutMsnumber | false500Longest a server render waits for the policy. false or Infinity waits however long the backend takes; any other value that is not a finite, non-negative number uses the default

The integration's own route under routePrefix is always skipped. On a skipped route, Astro.locals.c15t is unset and ConsentBanner throws, so skip only routes that render no consent components. See Server API.

clientEntrypoint

A path to a module whose default export adds live values in the browser. A relative path resolves from the project root. Unset, the integration uses src/c15t.client.ts, .js or .mjs when the file exists, so the quickstart sets nothing:

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 default export is a C15tClientOptionsExtension from c15t/astro:

FieldEffect
scriptsScripts added to the integration's scripts. Vendor helpers from @c15t/integrations go here
callbacksonChoiceRecorded, onPermissionsChanged, onError and onBeforeConsentRevocationReload. See Callbacks
networkBlockerReplaces the integration's networkBlocker, so it can include onRequestBlocked
clearOnRevocationReplaces the integration's clearOnRevocation
themeMerged over the integration's theme for button styles. Tokens here have no effect

The browser imports this module into the page script, so every page shares one copy. Keep it small, because it loads on every page.

Errors the integration raises

Message containsCause
mode must be manifest(), hosted() or offline()mode is a transport function, or not built with a helper from c15t/astro
is a functionA serialized option, such as a scripts entry from @c15t/integrations, holds a function. Move it to src/c15t.client.ts
manifest() needs a backend URLNo backendURL, PUBLIC_C15T_BACKEND_URL or PUBLIC_INTH_PROJECT_URL
manifest({ snapshot }) still needs a backend URLA snapshot, but no backendURL, PUBLIC_C15T_BACKEND_URL or PUBLIC_INTH_PROJECT_URL. Saves go to the backend whatever routePrefix is
hosted() needs a backend URLNo backendURL, hosted({ backendURL }), PUBLIC_C15T_BACKEND_URL or PUBLIC_INTH_PROJECT_URL
gives the server no manifest to fetchbackendURL: '' without manifest({ manifestURL }) or manifest({ snapshot })
routePrefix must be a pathroutePrefix is not false and does not start with /
@c15t/astro: routePrefix can't be '/': a consent route at the site root would catch every page. Use a path such as '/api/c15t'.routePrefix is '/'. A catch-all route at the site root would answer every page
clientEntrypoint ... does not existThe clientEntrypoint path does not resolve to a file
unknown uiui is not 'svelte', 'react' or 'vue'
ui: "svelte" needs @astrojs/svelteThe ui framework's Astro integration is missing
injects an on-demand routeastro build runs in manifest() mode with no server adapter
build-time manifests require an absolute upstream URLonBuildError: 'fail' with a relative or empty backend URL or manifestURL
could not fetch the consent manifestastro build could not download a manifest. In astro dev, or with onBuildError: 'runtime', the same message is a warning
no backend URL is setThe build has no backend URL to fetch from. astro build stops; astro dev warns

Troubleshooting has the fix for each.