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.
Consent requests go to the wrong backend
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 failedwith a network error, orno 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 placeholderhttps://your-project.inth.appreturns 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 theintegrationslist ofastro.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:
- The visitor already chose. A saved choice hides the banner. Open a private window.
- 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. - The policy did not resolve. If the browser's
/initrequest failed or was blocked, no banner orConsentDialogLinkappears and optional categories stay denied. - The page shares its HTML. On a static or prerendered page, the banner
appears after the
/initrequest 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:
| Earlier | Now |
|---|---|
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: false | routePrefix: false |
buildManifest: false | manifest({ source: 'runtime' }) |
buildManifest: true | onBuildError: 'fail' |
ConsentDialogTrigger from consent-dialog-trigger.astro | ConsentDialogLink 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/api | routePrefix, and createConsentRouteHandlers and the manifest cache helpers from c15t/astro/server |
resolveTransportFactory, custom, and the C15tModeDescriptor, C15tHostedDescriptor, C15tManifestDescriptor, C15tOfflineDescriptor and C15tEndpointOptions types | ConsentMode from c15t/astro |
/// <reference types="c15t/astro/middleware" /> in src/env.d.ts | Nothing. 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.
The banner shows no links to my policies
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.