SvelteKit
Quickstart
Before you start
This guide sets up a server-rendered SvelteKit app. The root layout's server
load resolves each visitor's consent before the page renders, so the banner
is part of the first HTML and a returning visitor's choice applies from the
first paint. For prerendered pages, a static site or SPA mode, follow this
guide and then rendering and deployment.
The examples/sveltekit app in the c15t repository is the finished result.
@c15t/svelte supports SvelteKit 2.63 and later, including SvelteKit 3,
with Svelte 5. The examples use SvelteKit 3. On SvelteKit 2, keep the adapter
in svelte.config.js instead of passing it to sveltekit().
The guide uses Inth for policies and consent records. Create an Inth project, then:
- Set its policy rules, including the
measurementcategory for the vendor below. - Add your app's origin, such as
http://localhost:5173, to its trusted origins. - Copy the project's backend URL. The build downloads the policy from it and the browser saves choices to it.
A self-hosted backend works the same way with your own URL; see self-hosting.
The setup bundles your policy into the server at build time, the recommended production setup. Rebuild after changing policies, translations or vendors.
Install
SvelteKit uses @c15t/svelte: components from @c15t/svelte, server
helpers from @c15t/svelte/kit and the Vite plugin from @c15t/svelte/vite.
The c15t package has no Svelte entry point, so you don't install it.
Set the backend URL
Set PUBLIC_C15T_BACKEND_URL to your project's backend URL, including any
path prefix, in .env or your host's build environment:
If your app already sets PUBLIC_INTH_PROJECT_URL for other Inth SDKs, that works
too. When both are set, PUBLIC_C15T_BACKEND_URL wins.
The example app's .env points at https://example-inth.inth.app, a demo
project. The build reads the variable, and the server and the browser use the
value the build read, so rebuild after you change it.
Bundle the policy at build time
Add consentManifest to the plugins in vite.config.ts:
consentManifest downloads your project's public policy from
<backendURL>/manifest during vite build, and in vite dev the first time
the server loads it. The policy stays on the server:
the browser bundle never gets it. No file is written into your project. In a
build, the plugin also lets c15tHandle, added below, preload the script
loader on pages that register scripts.
The snapshot is fixed at build time:
- Rebuild after changing policies, translations or vendors. If your CI caches build output, force a fresh build.
- Consent choices still go to the backend.
The build reads the backend URL from your public backend URL variable when
the config doesn't pass one: NEXT_PUBLIC_C15T_BACKEND_URL in Next.js,
NUXT_PUBLIC_C15T_BACKEND_URL in Nuxt, PUBLIC_C15T_BACKEND_URL in Astro,
Svelte and SvelteKit, and VITE_C15T_BACKEND_URL in TanStack Start and other
Vite apps. Each also reads the matching Inth variable, such as
NEXT_PUBLIC_INTH_PROJECT_URL, when the c15t one is unset. See
set the backend URL.
The fetch waits at most 10 seconds. When it fails, or no backend URL is set, every framework does the same thing:
| Command | Default when the fetch fails |
|---|---|
Production build: next build, vite build, nuxt build, astro build | The build stops with an error. |
Dev: next dev, vite dev, nuxt dev, astro dev | A warning, and the server fetches the policy at runtime. |
Set onBuildError to use one behaviour for both. 'fail' stops dev too.
'runtime' lets a production build finish, and the server fetches the policy
at runtime. The C15T_ON_BUILD_ERROR environment variable overrides the
option, so you can deploy during a backend outage without a code change:
Turborepo's strict environment mode hides undeclared variables from tasks, so
list C15T_ON_BUILD_ERROR in the build task's passThroughEnv there.
The build skips the fetch, without an error, when it can't use a snapshot,
for example when the backend URL is relative. With onBuildError: 'fail', a
relative URL stops the build. Consent modes
lists every case.
To apply policy edits without a rebuild, fetch the policy at runtime instead; see fetch the manifest at runtime.
Add the server hook
c15tHandle holds the consent config and reads the consent cookie, location
headers and Global Privacy Control once per request. It stores them on
event.locals.c15t for loadConsent and any other load or endpoint that
needs them. It also writes a server-rendered banner's rules into the HTML
<head> as <style> elements, so the banner is styled before hydration
without a stylesheet request. Add it to src/hooks.server.ts:
With no options, the mode is manifest(): the server resolves each visitor
from the bundled policy. Rendering and deployment
covers hosted(), offline() and the other options.
Type event.locals.c15t with one line in src/app.d.ts:
Resolve consent in the root layout load
Export loadConsent as the root layout's server load:
loadConsent resolves the visitor's policy from the bundled snapshot with
the inputs c15tHandle read, and returns { consent }, a plain,
serializable object for ConsentRoot. Rendering a page makes no policy
request to the backend. loadConsent never throws. If resolution takes
longer than 500 ms, it returns the stored choice without a policy, and the
browser resolves the policy after hydration.
While SvelteKit prerenders pages at build time, loadConsent returns no
visitor's consent and no policy, because a prerendered page goes to every
visitor. It reads SvelteKit's building flag itself.
Prerender some pages
covers how the browser resolves consent there.
Render ConsentRoot in the root layout
Pass data.consent through unchanged. ConsentRoot starts from it, so the
server and the browser render the same banner and the browser makes no second
request for the policy. A page the server resolved ships no resolver, policy
rules or snapshot to the browser.
PostHog waits for measurement permission. Replace phc_your_project_key
with your PostHog project key. Remove any <script> tags in src/app.html or
SDK imports that already load PostHog, so c15t is the only thing that loads
it. Create scripts in the layout component, not in +layout.server.ts:
they hold functions, which a server load cannot send to the browser.
ConsentBanner shows the banner when the policy asks for one.
ConsentDialog is the preference dialog; it loads in its own chunk after the
page, before anyone opens it. ConsentDialogLink is the persistent way back to
preferences. Put it where visitors look for privacy settings, such as the
footer. There is no stylesheet to import: the components bring their own
rules, and the server hook writes the banner's into the HTML.
Keep ConsentRoot in the root layout so it stays mounted across client-side
navigation.
Verify consent
Build the app with vite build and serve the production build with
vite preview, then open it in a private window with DevTools open:
- View the page source. It contains
data-testid="consent-banner-root", so the banner is in the server HTML, and<head>holds a<style data-c15t-styles>element with its rules. - In the Network panel, no requests go to PostHog.
- Select Reject All and reload. The page source no longer contains the banner, and PostHog requests stay absent.
- Select Privacy settings, turn on Analytics (the
measurementcategory) and save. PostHog loads. - Turn it off and save. The page reloads and PostHog does not load.
- Navigate to another route and back. The choice holds without a new banner.
If the build fails or the banner is missing from the page source, see troubleshooting. Verify consent has the release checklist.
Next steps
- Rendering and deployment: runtime policy fetching, prerendered pages, static sites, SPA mode and edge adapters.
- Customize: brand colors rendered on the server, slots and copy.
- Scripts and embeds: more vendors, gated iframes and network blocking.
- Context getters: read consent in your own components.