Skip to main content

Astro Verify and troubleshoot

Troubleshooting

The build says manifest() needs a server adapter

Check. The error reads manifest() resolves each visitor on the server and injects an on-demand route at /api/c15t/[...path]. manifest() is the default mode, and astro build stops when no adapter can serve its route. Look for an adapter and server output in astro.config.mjs.

Fix. Add your host's adapter and use server output. For a static site, set c15t({ mode: hosted() }) or c15t({ mode: manifest({ resolve: 'browser' }) }). See Rendering and deployment.

The config fails with "is a function"

Check. A serialized c15t() option holds a function. The error names it, such as c15t().scripts[0].onLoad. A vendor helper from @c15t/integrations, such as posthog(), returns a script with callbacks.

Fix. Move the option to the default export of src/c15t.client.ts. See clientEntrypoint.

The build says ui needs an Astro integration

Check. The preference dialog is an island in the framework that the ui option names. Look in astro.config.mjs for that framework's Astro integration, listed before c15t().

Fix. Install the integration and list it before c15t(). For ui: 'svelte', install @astrojs/svelte and svelte. For 'react', install @astrojs/react, react and react-dom. For 'vue', install @astrojs/vue and vue. A site that only shows a notice and never opens the dialog can set requireUIIntegration: false instead.

Check. The site registers more than one of @astrojs/svelte, @astrojs/react and @astrojs/vue, and the dialog renders in Svelte. With several registered, ui falls back to 'svelte'.

Fix. Set ui: 'react' or ui: 'vue' to the framework your pages already load, so visitors do not download Svelte as well.

Check. Look at PUBLIC_C15T_BACKEND_URL and PUBLIC_INTH_PROJECT_URL in .env, .env.local and the build environment, and at any backendURL in astro.config.mjs, top-level or in hosted({ backendURL }). One is still the placeholder https://your-project.inth.app, the demo project, or a URL from another project.

Fix. Replace it with your Inth project's backend URL, including any path prefix. Astro reads the config when astro dev or astro build starts, so restart the dev server after you change it. A built site keeps the URL from build time, even when the config reads it from an environment variable, so rebuild it.

astro build or astro dev can't fetch the manifest

Check. In manifest() mode the integration downloads ${backendURL}/manifest, or your manifestURL, when astro build or astro dev starts, and waits at most 10 seconds. When the download fails, astro build stops with an error that starts with @c15t/astro: could not fetch the consent manifest from <url> during the build. astro dev logs the same message as a warning, and the server fetches the policy at runtime. The part in parentheses names the cause:

  • fetch failed with a network error, or no response within 10 seconds: the build machine can't reach the backend.
  • /manifest responded 404, or another status: the URL is not your project's backend. The placeholder https://your-project.inth.app returns 404.
  • /manifest returned an invalid consent manifest: the URL answers, but not with a c15t manifest.

A relative or empty URL, such as /api/c15t, skips the fetch. With onBuildError: 'fail', it stops the build with build-time manifests require an absolute upstream URL.

A missing backend URL counts as a failed download: astro build stops with no backend URL is set, and astro dev warns.

Fix. Set PUBLIC_C15T_BACKEND_URL (or PUBLIC_INTH_PROJECT_URL), or backendURL on c15t(), to your Inth project's absolute backend URL, and make sure the machine that builds can reach it. To deploy while the backend is down, build with C15T_ON_BUILD_ERROR=runtime, or set onBuildError: 'runtime'. To never fetch at build time, set mode: manifest({ source: 'runtime' }). Either way, the server fetches the policy at runtime.

A component says Astro.locals.c15t is missing

Check. ConsentBanner and IABConsentBanner read the consent context that the integration's middleware sets. Confirm that:

  • c15t() is in the integrations list of astro.config.mjs.
  • The integration does not set middleware: false.
  • The route is not listed in middleware.skip.

Fix. Add c15t() to integrations, remove middleware: false, or take the route out of middleware.skip.

Why is there no banner?

Check. In DevTools Network, find the browser's /init request. Its response, or Astro.locals.c15t.snapshot.policyRule, shows which policy matched.

Fix. Work through these causes in order:

  1. The visitor already chose. A saved choice hides the banner. Open a private window.
  2. The policy asks for no banner. A rule whose prompt is none, such as one you set up for visitors in the United States, shows no banner. Policies explains each case.
  3. The policy did not resolve. If the browser's /init request failed or was blocked, no banner or ConsentDialogLink appears and optional categories stay denied.
  4. The page shares its HTML. On a static or prerendered page, the banner appears after the /init request returns, not in the first HTML.

On server output, the location comes from your host's headers, such as cf-ipcountry on Cloudflare or x-vercel-ip-country on Vercel. To test a region locally, send x-c15t-country with the request. Only your own edge may set x-c15t-* headers in production, because they override the host's.

The browser's /init request fails with a CORS error

Check. With hosted(), the browser calls your backend directly, so the backend must trust the site's origin. A preview deployment has its own origin.

Fix. Add the exact origin of the site, including the scheme and port, to your Inth project's trusted origins. Add each preview deployment's origin too.

The banner is missing from server HTML now and then

Check. The middleware waits up to 500 ms for the policy. When the backend is slower, for example on the first request after a deploy, the page renders without the banner and the browser shows it after its own request. This happens when the server fetches the policy at runtime, with hosted(), with manifest({ source: 'runtime' }), or after a build with onBuildError: 'runtime' could not fetch the manifest.

Fix. Use manifest() with an absolute backend URL and let the build reach it, so the policy ships with the build. Or raise the budget with middleware: { timeoutMs }. See Rendering and deployment.

The config fails with "manifest() needs a backend URL"

Check. The server reads the policy from ${backendURL}/manifest, and the browser saves consent there, so astro.config refuses to load without one. hosted() fails the same way with hosted() needs a backend URL.

Fix. Set PUBLIC_C15T_BACKEND_URL (or PUBLIC_INTH_PROJECT_URL) in .env, or pass c15t({ backendURL: 'https://your-project.inth.app' }) with your project's URL in place of the placeholder. A manifest({ snapshot }) still needs one: the snapshot replaces the manifest download, but the consent route answers GET only, so saves go to the backend.

Options from an earlier alpha have no effect

Check. astro.config.mjs uses hosted({ url }), manifest({ backendURL, manifest }), endpoints or buildManifest. The integration no longer reads them, and TypeScript reports them as unknown. A layout that imports consent-dialog-trigger.astro fails to resolve it.

Fix. Use the current names:

EarlierNow
hosted({ url })hosted() with the top-level backendURL, or hosted({ backendURL })
manifest({ backendURL })manifest() with the top-level backendURL
manifest({ manifest })manifest({ snapshot })
endpoints: { initPath, manifestPath }routePrefix, one catch-all for both
endpoints: falseroutePrefix: false
buildManifest: falsemanifest({ source: 'runtime' })
buildManifest: trueonBuildError: 'fail'
ConsentDialogTrigger from consent-dialog-trigger.astroConsentDialogLink from c15t/astro/components
hosted({ domain })hosted({ backendURL }), or the top-level backendURL
manifest({ reportSessions })The top-level reportSessions
The c15t/astro/api/init and c15t/astro/api/manifest entries, and route handlers from c15t/astro/apiroutePrefix, and createConsentRouteHandlers and the manifest cache helpers from c15t/astro/server
resolveTransportFactory, custom, and the C15tModeDescriptor, C15tHostedDescriptor, C15tManifestDescriptor, C15tOfflineDescriptor and C15tEndpointOptions typesConsentMode from c15t/astro
/// <reference types="c15t/astro/middleware" /> in src/env.d.tsNothing. The integration adds the Astro.locals.c15t type.

A vendor loads before the visitor chooses

Check. Search your layout and any tag manager for the vendor's own snippet.

Fix. Remove it, so only c15t loads the vendor.

Check. Look at the vendor's inline script. It needs type="text/plain" and is:inline.

Fix. Add the missing attribute. Without is:inline, Astro bundles the script and runs it straight away.

Check. Read effectivePermissions in the snapshot. The policy may allow the category before a choice: under an opt-out policy, measurement can be allowed from the start.

Fix. None. The vendor loads because the policy allows it. Scripts and Embeds cover the gating options.

A gated inline script never runs

Check. Open the console. c15t logs a warning for a data-c15t-category value that is not a category name, and leaves that script inert.

Fix. Set the attribute to one category, such as measurement.

Check. Under a nonce-based Content Security Policy, c15t also skips a gated tag that lacks the page's nonce. The console shows a warning about a script without the page's CSP nonce, and the tag has data-c15t-activated="untrusted" in the Elements panel.

Fix. Add nonce={Astro.locals.c15t?.nonce} to the tag and reload. c15t never retries a tag it marked untrusted. See put the nonce on your gated scripts.

The preference dialog opens without styles

Check. Look for styles: false in your c15t setup. With it, c15t inlines no rules and links no stylesheet when the dialog opens. Without it, check the Network panel for a failed request to the dialog's stylesheet, and the console for a Content Security Policy error.

Fix. Import c15t/astro/styles.css, and c15t/astro/primitives.css for the Svelte dialog, from your global stylesheet. See load the stylesheet yourself.

The banner loses its padding with Tailwind CSS

Check. Tailwind's preflight in the base layer is overriding c15t's components layer. This happens when your own stylesheet names its layers before c15t's rules do.

Fix. Put components after base in your first @layer statement, or set styles: false and import c15t/astro/styles.css after your layer order statement.

The dark mode is wrong or flashes

Check. Find where ConsentScript renders. It belongs in <head>, so the color scheme is set before first paint.

Fix. Keep ConsentScript in <head>. If your site has its own theme switch, set colorScheme: 'none' and toggle c15t-dark on <html> yourself, including after ClientRouter navigation. See set light or dark mode.

getConsentClient() returns null

Check. The runtime starts from a module script. Your script ran before it, or it ran on the server.

Fix. Call getConsentClient() in the browser, and try again on DOMContentLoaded. See Client API.

Check. ConsentBanner and ConsentDialog show only the legal links their legalLinks prop lists, even when the integration defines them. Look for the prop on each component.

Fix. Pass legalLinks={['privacyPolicy', 'cookiePolicy']} to each, and define those keys in the integration's legalLinks option. See ConsentBanner legal links.

An iframe loads before the visitor chooses

Check. Look for an iframe with src in the HTML. It loads before any script runs.

Fix. Put the URL in data-src with a data-category, or render the iframe from a component that adds it only while the category is allowed. See Embeds.

My own island is out of step with the banner, or turns dark mode off

Check. Your island created its own consent runtime, or its adapter is managing the c15t-dark class. Look at the options the island passes to its adapter.

Fix. Pass getConsentClient()?.runtime to the adapter as runtime, set colorScheme: null in its options, and render the island with client:only. See use consent in your own islands.

The console reports a Content Security Policy violation

Check. c15t renders inline scripts and a theme <style>. Find out whether your site uses a nonce policy or Astro's security.csp.

Fix. Under a nonce policy, set Astro.locals.c15t.nonce from your own middleware so they carry the nonce. Under Astro's security.csp, the integration adds their hashes for you. A ConsentBannerDeferred island stays blocked under a nonce policy, because Astro's island loader carries no nonce. See Content Security Policy.

More help

Troubleshoot consent covers problems shared by every framework, such as a missing banner, analytics that load before a choice, imports that fail because npm installed c15t v2, choices that disappear on reload, server HTML that differs from the browser, static builds that fail and content blockers that hide the consent UI.