Geo-targeted Cookie Consent in Astro Using Cloudflare
Join the newsletter
Follow my raw solo dev journey. Monthly direct to your inbox, no spam.
After setting up my site with Astro, I also wanted to set up Google Analytics on it. And if you are integrating with GA4, then you must provide a cookie consent mechanism to adhere to GDPR laws. In the past on WordPress, I simply used some plugins to help with that. On Astro, integration was more hands on - but not that difficult.
1. Integrating with Google Analytics + Cookie Consent
There is already a very well written guide on this topic written by Daniel: The ultimate Astro + Google Analytics guide, so I would rather not repeat it again.
At a high level, you need to do the following:
- Create a Google Analytics account and get your GA4 tracking ID (format:
G-XXXXXXXXXX). - Add the GA4 script to your
Layout.astrocomponent so it loads on every page. - Disable GA4 in development mode so test visits don’t pollute your analytics data.
- For GDPR compliance, you need to handle user consent before collecting any data:
- Initialize the GA4 script with all tracking denied by default - this way no data is collected until the user explicitly agrees.
- Use the
vanilla-cookieconsentlibrary to display a cookie banner to your visitors. - When the user accepts, update the GA4 consent settings to start collecting data. When they reject, GA4 stays in denied mode.
All of these are mentioned on Daniel’s post. Just implement it according to his guidance.
2. Geo-targeting Cookie Consent to GDPR Region Only
The Challenge
Once you have completed step 1 using Daniel’s guide, you will have a working cookie consent integrated with GA4. However, the cookie banner will be shown to everyone (at least that’s what I noticed with my configuration).
Trying to figure out the user location from client side is difficult:
- The browser Geolocation API requires explicit user permission
- Timezone detection is an indirect guess, not a country code. Can be spoofed (but this is not a threat vector).
- Third party IP lookup APIs cost money and add latency
A Failed Solution: Cloudflare Middleware
Since I use Cloudflare Pages to host my Astro site, I decided to use Cloudflare to help me out with user location detection.
My first failed attempt was a middleware that injected user location into the headers. This had two problems:
- CloudFlare caches headers too, so middleware didn’t run most of the time.
- When it ran it used my Worker credits. While I had a lot of free credits per day, it was still consuming a finite resource.
The next solution resolved both of the above issues.
Solution that worked: Probe Asset + Cloudflare Transform Rule
The solution is simple.
- Create a probe asset: Since our pages are getting cached, we need to avoid it. Rather then busting cache for the pages, which will be inefficient, we create a simple 1 px by 1 px asset and bypass cache only for that asset.
- Using Astro, just place it in
public/geo-check/pixel.webp - Since it’s very small, it’s fine to fetch it even without a cache. It won’t add any latency + we will fetch it async.
- Using Astro, just place it in
- Configure Cloudflare
- Add Cache Rule: On cloudflare, add a cache-rule to bypass caching for the exact uri
/geo-check/pixel.webp. - Add Transform Rule: This is the heart of the solution. Cloudflare already has the geo-location info when it receives the REQUEST from user. We just need to forward that to the RESPONSE.
- Create a transform rule named “Add Geo Headers” and run it for path
/geo-check/pixel.webp - Set dynamic header with name
x-countrywith valueip.src.country
- Create a transform rule named “Add Geo Headers” and run it for path
- Add Cache Rule: On cloudflare, add a cache-rule to bypass caching for the exact uri
- Update Your Client Side Code: Now that you have the probe asset ready, simply use that as API endpoint to detect user location for free and determine whether or not you need to show cookie banner.
- Make sure to keep your GA4 scripts in denied mode until you are done fetching the geo-check probe.
Here is what I have:
// /src/utils/consent.ts
const GDPR_COUNTRIES = new Set([
// EU 27
'AT','BE','BG','HR','CY','CZ','DK','EE','FI','FR',
'DE','GR','HU','IE','IT','LV','LT','LU','MT','NL',
'PL','PT','RO','SK','SI','ES','SE',
// Extra GDPR (non-EU but subject to equivalent laws)
'GB','NO','CH','IS','LI'
]);
export async function resolveGDPRStatus(): Promise<boolean> {
// Check cookie first — avoid probe request on repeat visits
const cookie = getCookie('visitor_region');
if (cookie === 'gdpr') return true;
if (cookie === 'none') return false;
// First visit — fetch probe
try {
const res = await fetch('/geo-check/pixel.webp', { cache: 'no-store' });
const country = res.headers.get('x-country') ?? '';
const isGDPR = GDPR_COUNTRIES.has(country);
// Cache result in cookie for 30 days
document.cookie = `visitor_region=${isGDPR ? 'gdpr' : 'none'}; path=/; max-age=2592000; SameSite=Lax`;
return isGDPR;
} catch {
console.error('geo-check probe failed, falling back to timezone detection');
// Fallback to timezone detection
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
return tz.startsWith('Europe/') || tz.startsWith('Atlantic/Reykjavik');
}
}
For a first time visitor, we will not have any cookie stored for them. So we fetch their location by fetching the probe asset, check the response header for location and then check if they are in GDPR zone or not. We then store “gdpr” or “none” as a value to vistor_region as a cookie.
We then use the visitor_region value to determine if cookie consent for tracking needs to be shown or not. Until this value is fetched, keep your GA4 tracking turned off to be legally safe. Once you are done with fetching the probe asset and detecting their region, based on their location, either show the cookie consent or turn back GA4 tracking on immediately.
Testing
How are we going to verify things are working across the world? Here is how I manually tested it:
- Opened my site on chrome browser and cleared the cookies first.
- Opened “Devtools > Network” and filtered for the probe asset name, which in my case was “pixel.webp”.
- Clicked on it and checked that
x-countryfield was populated with right coutry code. - To check from other countries, used ProtonVpn’s free tier to connect from random countries and verified
x-countryvalue was correct.
Conclusion
I don’t know why I always spend so much time configuring the consent management for any project I build. It would have been easier to just show the banner to everyone, or worse, not show it at all.
I have many friends with projects where they don’t bother with GDPR at all stating that they don’t have much users anyways. Same applies for me - I barely have any user to my blog at the moment. Yet, here I am - spending multiple days fiddling with consent management infrastructure.
In my defence, such privacy efforts shouldn’t be this hard in the first place! I was really hoping that there would be some astro package that will just take care of everything for me. Alas, that wasn’t the case. But thankfully, I (together with my trusty AI tools) managed to find a solution (after wasting time with few wrong solutions).
Join the newsletter
Follow my raw solo dev journey. Monthly direct to your inbox, no spam.