---
title: Troubleshooting
description: Fix common c15t problems on Astro sites, from build errors about
  adapters and UI integrations to a missing banner, scripts that run too early,
  unstyled dialogs and saves that fail.
group: frameworks
lastModified: "2026-10-10T16:01:45+01:00"
---
## 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](/docs/frameworks/astro/rendering).

## 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`](/docs/frameworks/astro/integration#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 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](/docs/concepts/policies#why-the-banner-may-be-absent) 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](/docs/frameworks/astro/rendering#set-how-long-a-render-waits-for-the-backend).

## 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](/docs/frameworks/astro/scripts) and
[Embeds](/docs/frameworks/astro/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](/docs/frameworks/astro/content-security-policy#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](/docs/frameworks/astro/customize#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](/docs/frameworks/astro/customize#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](/docs/frameworks/astro/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](/docs/frameworks/astro/components/consent-banner#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](/docs/frameworks/astro/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](/docs/frameworks/astro/islands#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](/docs/frameworks/astro/content-security-policy).

## More help

[Troubleshoot consent](/docs/guides/troubleshooting) 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.
