Skip to main content

SvelteKit Verify and troubleshoot

Troubleshooting

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:

ErrorCause
/manifest responded 404The backend URL is wrong, or still the https://your-project.inth.app placeholder.
no backend URL is setNeither 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 secondsThe 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, 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.
  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.

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.

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.

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:

RemovedUse instead
ConsentManagerProvider with prefetch={...}<ConsentRoot state={data.consent}>
FrameConsentGate
createSvelteKitConsentRouteHandlerscreateConsentRoute
c15tPreloadPart of consentManifest() from @c15t/svelte/vite
ConsentManifestOptions, consentManifest({ outputFile })Removed
loadConsent(event, { backendURL, manifest, initRoute, shared }) returning prefetchc15tHandle({ mode, routePrefix, snapshot, backendURL }) in src/hooks.server.ts, and export { loadConsent as load }, which returns { consent }
resolveConsent({ manifest }) from @c15t/svelte/serverresolveConsent({ 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.
  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.

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.

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 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 lists the checks to run before release.