Geo-targeted consent: one banner for GDPR, another for the rest of the world
Not every visitor needs the same consent experience. A user in Berlin needs an explicit opt-in. A user in Texas needs a “Do Not Sell” link. A user in Tokyo needs nothing at all.
Showing the wrong experience to the wrong jurisdiction is either over-compliance (annoying users unnecessarily) or under-compliance (breaking the law). Neither is good.
The three jurisdiction buckets
Most consent requirements fall into three categories:
| Jurisdiction | Requirement | What to show |
|---|---|---|
| GDPR (EEA, UK) | Explicit opt-in before tracking | Full consent banner with granular categories |
| US state privacy (CA, CO, CT, VA, etc.) | Opt-out of sale/sharing | ”Do Not Sell or Share My Personal Information” link |
| Rest of world | Nothing (yet) | Nothing |
How geo-targeting works
Geo-targeting resolves the visitor’s jurisdiction from their IP address. This happens client-side through a lightweight API call. The result is cached so it only runs once per session.
Based on the result:
- GDPR region → show the full consent banner, default to “denied”
- US privacy-law state → show a “Do Not Sell” link, don’t block cookies by default
- Neither → show nothing, assume implied consent where lawful
The gotchas
VPNs and proxies can throw things off. A user in Germany using a US VPN gets the US experience. It is rare enough to be an acceptable edge case, but worth knowing about.
Travelers are trickier. A California resident visiting France should get the GDPR experience. Geo-targeting by current location, not residency, is the standard approach.
Without JavaScript, there is no geo-resolution at all. Zest falls back to showing the full banner when JS is disabled. It is better to over-comply than under-comply.
How Zest handles it
Zest’s geo module is opt-in. Set geo: true in your config, and we handle the rest. The hosted gateway resolves jurisdictions for free. No API key, no third-party account. The resolution data never leaves the browser.
If you want to use your own resolver, plug it in. The geo.resolve callback accepts any async function that returns a jurisdiction code.
—
May the source be with you.
Alex @ FreshJuice