Skip to main content

Nuxt Advanced

Geography headers

How Nuxt finds the visitor's location

The Nuxt server reads the visitor's location from request headers that your host or CDN adds. It never looks up an IP address itself. The headers are read in three places:

  • During server rendering, the c15t plugin reads them from the page request.
  • In the default manifest() mode, the /api/c15t/init route reads them from the browser's own request, for example on a prerendered route.
  • In hosted() mode, the plugin forwards them to your backend's /init.

With manifest({ resolve: 'browser' }), no request reaches your server, so the browser has no location. Pass geoURL to manifest() with a route that returns the visitor's country and region, or a policy that depends on location asks the backend's /init on the first visit. See deploy to static hosting.

When no location header is present, the location stays unknown and your policy's rule for unknown locations applies.

A country can also arrive without a region, for example from Cloudflare without its visitor location headers setting. If your policy has rules for regions of that country, such as California, c15t can't tell which one applies. It uses the rule that lists the country in regionFallbacks, and otherwise the rule for unknown locations. c15t's recommended rules send a US visitor without a state to the US opt-out rule this way.

Which headers are read?

Within each row, the first header with a value wins:

InputHeaders, highest precedence firstSource
Countryx-c15t-country, cf-ipcountry, x-vercel-ip-country, x-amz-cf-ipcountry, x-country-code, x-countryc15t override, Cloudflare, Vercel, CloudFront, generic
Regionx-c15t-region, cf-region-code, x-vercel-ip-country-region, x-region-codec15t override, Cloudflare, Vercel, generic
Global Privacy Controlx-c15t-gpc, sec-gpcc15t override, browser signal
Languageaccept-languageBrowser

Vercel sends its headers without setup. Cloudflare sends cf-region-code only with its visitor location headers setting turned on. CloudFront's own CloudFront-Viewer-Country and CloudFront-Viewer-Country-Region headers are not in the list. Map them to x-c15t-country and x-c15t-region at the edge, for example with a CloudFront Function, as in AWS's viewer-request example.

The init route responds with Cache-Control: private, no-store, because its answer belongs to one visitor.

Trust only your own infrastructure

The x-c15t-* headers always win, by design. A visitor who sends x-c15t-country: US picks their own policy unless something removes the header first. The header only changes the policy for the visitor who sent it. For geography you can trust, configure the edge that terminates all your traffic, such as your CDN or load balancer, to delete incoming x-c15t-country, x-c15t-region and x-c15t-gpc headers before it sets any of its own.

Stripping the headers also turns off the country and region props of ConsentRoot when the server resolves the policy. In the browser, those props reach the /api/c15t/init route as x-c15t-country and x-c15t-region headers, so the edge removes them too.

Do not strip them in a Nitro server middleware. During server rendering, the c15t plugin forwards the visitor's location headers to the module's init route inside the same server, and a middleware runs for that internal request too. A middleware that deletes or rewrites location headers there removes the location the plugin has forwarded.

If a client can reach your origin without passing through your CDN, it can send any platform header, such as cf-ipcountry. Block direct origin access with your host's origin protection.

Test another region

Use one of these while you build, never in production:

  • Render <ConsentRoot country="DE" />, or pass region too. c15t resolves the policy again for that location. When the server resolves the policy, this only works while nothing strips the x-c15t-* headers.
  • Send the override header yourself, for example curl -H 'x-c15t-country: DE' http://localhost:3000/api/c15t/init. This only works while nothing strips the header.
  • Use a VPN or your host's geography testing tools against a deployment.

The internals/fixtures/nuxt test app in the c15t repository has a demo middleware that turns ?country=DE into an override header. It lets any visitor pick their policy, so do not copy it into production.

Verify

Deploy and load the site from two locations with different policy rules, for example through a VPN. View the source of each page: an opt-in region has data-testid="consent-banner-root" in the HTML, and a region with no prompt does not. Then request /api/c15t/init with curl -H 'x-c15t-country: US' against production and confirm the resolved location ignores the header.