---
title: Troubleshooting
description: Fix a SvelteKit build that cannot fetch the c15t manifest, a
  missing bundled policy, a banner missing from server HTML, failed saves
  through the consent route, a wrong backend URL and ignored theme colors.
group: frameworks
lastModified: "2026-10-10T16:01:45+01:00"
---
## The build, dev server or type check fails fetching the manifest

**Check.** `vite build`, `vite dev` and, on SvelteKit 3, `svelte-kit sync`
download the policy through the `consentManifest` plugin in `vite.config.ts`.
`vite build` stops with an error that starts with `@c15t/svelte/vite: could not fetch the consent manifest`, and `vite dev` logs it as a warning. The part
in parentheses names the cause:

|Error|Cause|
|--|--|
|`/manifest responded 404`|The backend URL is wrong, or still the `https://your-project.inth.app` placeholder.|
|`no backend URL is set`|Neither the `backendURL` option nor `PUBLIC_C15T_BACKEND_URL` (or `PUBLIC_INTH_PROJECT_URL`) is set, or it is empty. `vite build` stops; `vite dev` warns.|
|`skipped the consent manifest fetch` (a notice)|The backend URL is relative, such as `/api/c15t`. With `onBuildError: 'fail'`, this is the error `build-time manifests require an absolute upstream URL`.|
|`/manifest returned an invalid consent manifest.`|The URL answers, but not with a c15t v3 manifest.|
|`fetch failed` or `no response within 10 seconds`|The machine running the command cannot reach the backend.|

**Fix.** Set `PUBLIC_C15T_BACKEND_URL` (or `PUBLIC_INTH_PROJECT_URL`) to your
project's absolute backend URL where the command runs, in `.env` or the CI environment. The plugin reads it
when `vite.config.ts` passes no `backendURL`. Make sure the build machine can
reach the backend. To deploy while it is down, build with
`C15T_ON_BUILD_ERROR=runtime`.
A `check-types` script that starts with `svelte-kit sync` needs the backend
too.

## The server fetches the manifest at runtime

**Check.** With the default `manifest()` mode, the server log shows a
manifest request to the backend, or a page waits on it after a cold start.
The build had no policy to bundle: `consentManifest` is missing from
`plugins` in `vite.config.ts`, the fetch failed in `vite dev` or in a build
with `C15T_ON_BUILD_ERROR=runtime`, or the backend URL is relative.

**Fix.** Add the plugin or fix the cause from the
[fetch errors](#the-build-dev-server-or-type-check-fails-fetching-the-manifest),
then restart `vite dev` or rebuild. A build fetches once, and `vite dev`
fetches the first time the server loads the policy.
Until then, the server fetches and caches the policy at runtime.

## Why is the banner missing from the server HTML?

**Check.** View the page source and search for `consent-banner-root`.

**Fix.** If the banner only appears after hydration, work through these causes
in order:

1. **`ConsentRoot` has no server state.** Check that the root
   `+layout.server.ts` exports `loadConsent` as `load` and that
   `+layout.svelte` passes `state={data.consent}`. With
   `manifest({ resolve: 'browser' })` or `offline()`, the browser resolves
   the policy by design.
2. **`loadConsent` fell back.** It returns the stored choice without a policy
   when resolution fails or takes longer than `timeoutMs`, 500 ms by default.
   Check the server logs. With runtime fetching or `hosted()`, check the
   backend's response time, and raise `timeoutMs` if it is slow.
3. **The page is prerendered.** Prerendered pages never carry a visitor's
   consent. See
   [prerender some pages](/docs/frameworks/sveltekit/rendering#prerender-some-pages).
4. **No banner is due.** The policy for the visitor's location may ask for no
   prompt, or the visitor already chose. Clear site data and check the policy.

## The banner flashes, or the stored choice is ignored on the server

**Check.** The server reads the choice from the `c15t` cookie. Look for a
changed `storageConfig.storageKey` on the provider. Without a matching cookie
name, the server reads nothing and renders the banner for every visitor.

**Fix.** Pass the same name as `cookieName` to `loadConsent` or `c15tHandle`.

**Check.** Look at what the layout passes to `ConsentRoot`. Rebuilding
`data.consent` from booleans loses the policy and the stored record.

**Fix.** Pass `data.consent` to `ConsentRoot` unchanged.

## Saving fails with 405 from /api/c15t

**Check.** `c15tHandle({ backendURL: '/api/c15t' })` sends saves to the
consent route, but the route does not forward writes. A route file that
exports only `GET` still answers a save with `405`.

**Fix.** Pass `proxy: true` to `createConsentRoute` and export
`POST`, `PATCH`, `PUT`, `DELETE` and `OPTIONS` next to `GET` from the
catch-all route. See
[save consent through your own origin](/docs/frameworks/sveltekit/rendering#save-consent-through-your-own-origin).

## The consent route times out or calls itself

**Check.** `createConsentRoute` needs the backend's URL. It reads it from
`c15tHandle()` and then `consentManifest` unless you pass `backendURL`, and
skips a handle `backendURL` that points at the route itself. A relative
`backendURL` passed to the route names a route in your own app, which the
handlers call in-process; if it points at `/api/c15t`, the route requests
itself.

**Fix.** Pass the Inth URL as `createConsentRoute({ backendURL })`, or leave
it unset so the route uses `PUBLIC_C15T_BACKEND_URL` or
`PUBLIC_INTH_PROJECT_URL`.

## 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 `vite.config.ts`,
`src/hooks.server.ts` and a `hosted({ backendURL })` mode. One still has the
demo project, the placeholder `https://your-project.inth.app`, or a URL from
another project. The build reads the variable, so the URL for the policy and
for saves is fixed when you build, and setting it when the built app starts
changes nothing.

**Fix.** Set `PUBLIC_C15T_BACKEND_URL` (or `PUBLIC_INTH_PROJECT_URL`) to your
Inth project's backend URL, including any path prefix, for the build and for the running app, then
rebuild. The policy and saves need the same backend.

## Your own `:root` token rules are ignored

**Check.** A theme from `generateThemeCSS()` uses `:root:root`, which outranks a
plain `:root` rule wherever each one sits in the page. If you also set
`--c15t-*` variables on `:root` in a stylesheet, the generated theme wins for
every token both set.

**Fix.** Move those values into the theme, or write the rule on `:root:root`.
See [customize](/docs/frameworks/sveltekit/customize).

## Server code ends up in the browser bundle

**Check.** Look for imports of `@c15t/svelte/kit` or `@c15t/svelte/server` in
components. They are for server files: `hooks.server.ts`,
`+layout.server.ts`, `+page.server.ts` and `+server.ts`. Imported from a
component, they pull the manifest resolver and every bundled translation into
the browser bundle.

**Fix.** Import the components from `@c15t/svelte` in components, and set
the mode on `c15tHandle()` in `hooks.server.ts`.

## c15tHandle() throws on `custom()`

**Check.** `c15tHandle({ mode })` received `custom()`. The error reads
"c15tHandle() takes manifest(), hosted() or offline() from @c15t/svelte/kit".
The handle's mode must be data that can reach the browser, and a custom
transport is code.

**Fix.** Pass the `custom()` transport to `ConsentRoot`'s `mode` prop, and
keep the handle's `mode` for one of the data modes from `@c15t/svelte/kit`.

## Names from an earlier alpha are missing

**Check.** An import or option from an earlier v3 alpha fails, such as
`createSvelteKitConsentRouteHandlers`, `c15tPreload` or
`loadConsent(event, { backendURL })`. `@c15t/svelte` was not public in v2, so
the alphas renamed these with no alias.

**Fix.** Use the current names:

|Removed|Use instead|
|--|--|
|`ConsentManagerProvider` with `prefetch={...}`|`<ConsentRoot state={data.consent}>`|
|`Frame`|`ConsentGate`|
|`createSvelteKitConsentRouteHandlers`|`createConsentRoute`|
|`c15tPreload`|Part of `consentManifest()` from `@c15t/svelte/vite`|
|`ConsentManifestOptions`, `consentManifest({ outputFile })`|Removed|
|`loadConsent(event, { backendURL, manifest, initRoute, shared })` returning `prefetch`|`c15tHandle({ mode, routePrefix, snapshot, backendURL })` in `src/hooks.server.ts`, and `export { loadConsent as load }`, which returns `{ consent }`|
|`resolveConsent({ manifest })` from `@c15t/svelte/server`|`resolveConsent({ snapshot })`|

## Why is there no banner?

**Check.** Read `getConsentManager().policyRule.prompt` and `.location`, or open
DevTools. In the Network panel, find the `/init` request.

**Fix.** Work through these causes in order:

1. **The policy asks for no prompt.** Visitors in regions without a consent
   law, and opt-out policies with no prompt, get no banner. See
   [why the banner may be absent](/docs/concepts/policies#why-the-banner-may-be-absent).
2. **A choice is already stored.** Clear site data for your origin, or use the
   DevTools panel, and reload.
3. **The backend request failed.** A CORS error means your site's origin is
   not in the Inth project's trusted origins. A 404 means the backend URL is
   wrong. While the request fails, optional categories stay denied and no
   banner shows.
4. **The component is outside the provider.** `ConsentBanner` must render
   inside `ConsentProvider`; outside it, Svelte throws
   `c15t: no v3 consent context`.

## Svelte throws "no v3 consent context"

**Check.** A getter or component ran without a `ConsentProvider` above
it.

**Fix.** Move it inside the provider.

**Check.** Look for a getter called in an event handler or effect. Getters use
Svelte's `getContext`.

**Fix.** Call getters at the top level of a component's `<script>`.

**Check.** If the provider is there, check that the app resolves one copy of
`@c15t/svelte`. Two copies create two separate contexts.

**Fix.** Make the app resolve a single copy of `@c15t/svelte`.

## Theme colors are ignored

**Check.** The provider's `theme` prop applies slot styles and `consentActions`
only. Colors, radius, typography and other tokens in `theme` are not turned
into CSS in the browser, and in development the provider logs a warning about
it.

**Fix.** Set the `--c15t-*` variables on `:root` in a stylesheet, or render
`generateThemeCSS()` output as `<style id="c15t-theme">`. The customize page
shows both.

**Check.** If your values still lose, inspect the element. A generated theme
uses `:root:root`, so it beats your own `:root` rule.

**Fix.** Write your rule on `:root:root` too, or move the value into the theme.

## The dialog or banner has no styles

**Check.** Look in `<head>` for `<style data-c15t-styles>` elements, and in
the console for a Content Security Policy error. On a server-rendered
SvelteKit page, check that `src/hooks.server.ts` exports `c15tHandle`.

**Fix.**

* No c15t `<style>` element: the provider sets `styles={false}` or
  `noStyle`. With `styles={false}`, import `@c15t/svelte/styles.css` once,
  in the component that renders the provider or in your global stylesheet.
* The SvelteKit banner is unstyled until hydration: add `c15tHandle` from
  `@c15t/svelte/kit` to `src/hooks.server.ts`, or set `styles={false}` and
  import `@c15t/svelte/styles.css`.
* The console reports a blocked inline style: pass the provider a `nonce`, or
  set `styles={false}` and import the stylesheet. The Content Security Policy
  page covers both.
* IAB TCF surfaces are unstyled with automatic styles disabled: import
  `@c15t/svelte/styles.css` and `@c15t/svelte/iab/styles.css` together. With
  automatic styles enabled, check the policy and IAB configuration, the
  provider's `noStyle` option, and CSP errors.

## A vendor loads before consent, or twice

**Check.** The vendor is still loaded some other way: a `<script>` tag in
`index.html` or `app.html`, a tag manager, or an SDK import in your code. Check
the Network panel with site data cleared; nothing from the vendor should load
before you allow its category.

**Fix.** Remove the other loader and keep only the `scripts` registration.

## The page reloads after saving

**Check.** When a save withdraws a category that was granted, the provider
reloads the page so code that already ran is gone. This is expected.

**Fix.** Set `reloadOnConsentRevoked: false` on the provider only if every
registered vendor stops itself through its consent API.

## 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. [Verify
consent](/docs/guides/verify-consent) lists the checks to run before release.
