SvelteKit Advanced
Geography headers
Why location matters
Your consent policy picks a rule by the visitor's country and region: opt-in in Europe, opt-out in US states with privacy laws, no banner where no law applies. On SvelteKit, the server helpers read that location from request headers your host or CDN adds. If no header arrives, the policy resolves with its rule for an unknown location.
Which headers are read
loadConsent, c15tHandle, the manifest route and resolveConsent read the
same headers, in this order. The first one present wins.
| Input | Headers, highest priority first |
|---|---|
| Country | x-c15t-country, cf-ipcountry, x-vercel-ip-country, x-amz-cf-ipcountry, x-country-code, x-country |
| Region | x-c15t-region, cf-region-code, x-vercel-ip-country-region, x-region-code |
| Language | accept-language, negotiated by quality value to a primary tag such as de |
| Global Privacy Control | x-c15t-gpc, sec-gpc; only 1 and 0 count |
Vercel adds its country and region headers on every request. Cloudflare adds
cf-ipcountry; for cf-region-code, turn on Cloudflare's visitor location
headers. On another host, or a Node server without a CDN, set x-c15t-country
and x-c15t-region at your proxy from whatever location data it has.
With a build-time snapshot, as in the quickstart, loadConsent resolves the
policy in the server process from these inputs and makes no policy request.
In hosted() mode, it sends the resolved inputs to the backend's /init as
x-c15t-country, x-c15t-region, accept-language and sec-gpc, along
with user-agent. It adds the visitor IP as x-forwarded-for only with
trustForwardedHeaders: true. It never forwards forwarded,
x-forwarded-host or x-forwarded-proto; with that option it reads them to
resolve a relative backendURL. When the backend is a route in your own
app, it passes the same headers through event.fetch, which forwards only
cookies and authorization by itself.
Resolve the inputs once per request
c15tHandle in src/hooks.server.ts reads the headers once, stores the
inputs on event.locals.c15t.inputs, and rewrites the request's headers to
the normalized x-c15t-country, x-c15t-region, accept-language and
sec-gpc. Later loads, endpoints and proxied backend calls then see one
form, whichever CDN header carried the value:
Server API lists its options.
Trust the headers
The x-c15t-* headers always win, and a browser can send them. A visitor who
sets x-c15t-country: US gets the policy for the United States. That changes
only their own banner, not anyone else's, but if you need the policy to
follow your CDN's location, remove incoming x-c15t-* headers at your edge
before the request reaches SvelteKit.
cf-ipcountry and the Vercel headers are set by the platform and cannot be
forged through it. Behind your own proxy, make sure it overwrites the country
headers rather than passing a client's value through.
The consent route fetches a relative backendURL through event.fetch and
never resolves it against the request, so a forged Host or
x-forwarded-host header cannot send its requests elsewhere. loadConsent
resolves a relative backendURL against event.url. With its default
event.fetch, the call stays in-process. With your own fetch, it goes to the
origin of event.url. adapter-node builds that origin from paths.origin
in SvelteKit 3 (the ORIGIN variable in SvelteKit 2), or from its
PROTOCOL_HEADER and HOST_HEADER settings; with none of them, it takes the
host from the Host header. SvelteKit 3's adapter-node ignores ORIGIN
without a warning, so move it to paths.origin when you upgrade. A forged x-forwarded-host changes
neither unless you set trustForwardedHeaders. See
Server API.
Force a location
Pass country, region or language to force an input:
- On
c15tHandle({ country: 'DE' }), for every request, such as a site that serves one country. - On one
loadConsent(event, { country: 'DE' })call, from a layout's ownload, such as a/de/section.
A per-call value wins over the handle's. The browser's provider takes
overrides={{ country, region, language, gpc }} for the same purpose after
hydration.
Test another location
Send the override header with a request and look for the banner in the HTML:
The first request prints 1 under a policy that asks Europe for opt-in. What
the others print depends on your policy's rules for California. In the
browser, the dev tools panel's location tab changes the location for the
current page without a request header.
Verify
- On your deployed site, open DevTools and check the document request's headers for your CDN's country header.
- View the page source. The banner is in the HTML for a location that needs one, and absent for one that does not.
- With the dev tools panel, confirm the resolved country and policy match where you are.