Compliant cookies in Astro with Consent Mode v2
Loading Google Analytics in the <head> just like that breaks the GDPR: non-essential cookies need prior consent. But blocking GA until the user clicks “Accept” leaves you with no data at all from anyone who doesn’t accept. Google’s official solution solves both: Consent Mode v2. One important nuance first: this is not about hiding the banner —the banner always shows on load—, it’s about the tracking state.
GA starts denied
Before loading gtag.js, I tell Google the default consent is denied. GA loads, but without writing cookies: it only sends anonymous pings, which are legal without consent.
<script>
window.dataLayer = window.dataLayer || [];
function gtag() { dataLayer.push(arguments); }
gtag('consent', 'default', {
ad_storage: 'denied',
ad_user_data: 'denied',
ad_personalization: 'denied',
analytics_storage: 'denied',
wait_for_update: 500,
});
</script>
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXX"></script>
<script>gtag('js', new Date()); gtag('config', 'G-XXXX');</script>
From the banner to Google: consent update
The banner is drawn by vanilla-cookieconsent (v3, free, no external service). When the user decides, I reflect their choice into gtag: if they accept the analytics category, I flip analytics_storage to granted.
import * as CookieConsent from 'vanilla-cookieconsent';
function updateConsent() {
const analytics = CookieConsent.acceptedCategory('analytics');
gtag('consent', 'update', {
analytics_storage: analytics ? 'granted' : 'denied',
});
}
CookieConsent.run({
categories: { necessary: { readOnly: true }, analytics: {} },
onFirstConsent: updateConsent,
onConsent: updateConsent,
onChange: updateConsent,
// ...translations
});
Conclusion
With Consent Mode v2, GA loads in denied (no cookies) and only flips to granted when the user accepts. You comply with the GDPR, you don’t lose the anonymous aggregate data, and you use the pattern Google has required in the EU since 2024. In Astro it’s two pieces: the default before gtag.js and the update from the banner.