Discuss your task

Search

Start typing to search articles, cases, and services.

navigate Esc close

How to Set Up GDPR Cookie Consent on a Headless Site

GDPR requires explicit user consent before loading any non-essential cookies and trackers - a “we use cookies” banner with a single “Accept” button doesn’t meet that bar. On a headless site, the task gets harder: there’s no server to store consent state during static generation, and analytics scripts are often wired directly into the build pipeline. Below is a working architecture that closes both the legal risk and the conversion drop.

According to the IAPP-EY 2024 survey, GDPR fines across the EU have exceeded 5.88 billion euros since 2018, and a notable share of enforcement actions trace back specifically to cookie practices - missing granular consent, or trackers that fire before the user clicks anything. In Exceltic.dev projects, we regularly see the same mistake among companies expanding into international markets: the team bolts on a ready-made cookie banner as a decorative element, while Google Tag Manager keeps firing pixels in the background regardless of the user’s choice. This article covers how to get consent technically right on a statically generated site, without losing conversions to UX friction.

Headless architecture adds its own wrinkle: with static generation (Astro, Next.js SSG), there’s no session and no user on the server at build time - all consent state has to live and be processed on the client, in sync with the first render.

Companies entering the EU market, or the US market with European users, regularly err in one of two directions: either they skip the banner entirely and take on legal risk, or they deploy a heavy banner-wall that blocks content and cuts entry conversion by 15-30%. Both extremes stem from treating consent management as a purely design problem instead of an architectural one.

CMP (Consent Management Platform): a service or script that displays the consent banner, stores the user’s choice by cookie category, and passes that choice to analytics and advertising scripts.

GDPR (Articles 6 and 7, read together with the ePrivacy Directive) requires consent to be prior - obtained before loading any cookies beyond the strictly necessary ones (session, security, load balancing). Analytics, advertising, and most heatmap tools don’t qualify as strictly necessary.

The second requirement is granular opt-in: the user selects categories separately (analytics, marketing, personalization) rather than a single “Accept all” button. Wording like “by continuing to use this site, you agree” doesn’t legally count as consent, either under GDPR or under how most EU regulators (CNIL, ICO, Datatilsynet) interpret it.

Third is equal-prominence rejection. The “Reject all” button must be as visible on the banner as “Accept all”, with no extra clicks through a settings menu required. The German regulator LfD Niedersachsen and France’s CNIL have already fined sites specifically for asymmetric banner design - a large green consent button against a small gray rejection link.

Reputational risk runs alongside the legal one: EU B2B buyers and enterprise clients check the cookie policy and the site’s actual behavior during vendor due diligence. A banner that doesn’t match its own policy text is a red flag for the client’s legal department before a salesperson ever gets on a call.

What a naive implementation gets wrong

The most common mistake is a visual banner with no technical blocking. The plugin shows the buttons, but Google Tag Manager, Meta Pixel, and analytics initialize in <head> regardless of the user’s choice. Consent was formally requested, but the trackers already fired their first hit before the click.

The second mistake is a cookie wall with no real choice: the site’s content is hidden behind a full-screen overlay until the user clicks “Accept all”, while the rejection button is either missing or buried in a multi-step settings menu. That’s not consent, it’s coercion - and one of the most common grounds for complaints to EU supervisory authorities.

Third is losing consent state during static generation. On a headless site, the page HTML is built once, ahead of any specific user’s visit. If a developer tries to solve consent server-side (say, through middleware that decides whether to render the GTM tag), they run into the fact that there’s no user yet at the point the static page renders. The solution is inevitably client-side - and it needs to be designed into the architecture from the start, not bolted onto a finished site.

Put together, this produces a site that looks GDPR-compliant but actually isn’t - and carries the reputational cost of a bad banner UX on top.

On a statically generated site, consent state can’t live on the server at build time - there’s nowhere to get it before the user’s visit. A practical scheme:

  1. Storage - the user’s choice is written to a cookie or the browser’s localStorage on first interaction with the banner. A cookie is preferable if you need consistency across subdomains or future server-side reads.
  2. CMP script - a lightweight JS module (Cookiebot, Osano, CookieYes, Klaro, or a self-hosted solution) that renders right after page hydration and blocks other scripts until consent is given.
  3. Google Consent Mode v2 - a Google mechanism that, instead of hard-blocking the script, passes GTM/GA4/Google Ads a consent-state signal by category (ad_storage, analytics_storage, ad_user_data, ad_personalization). The tags are technically connected, but before consent they run in a limited, cookieless mode.
  4. Load order - the CMP script must load first in <head>, ahead of any analytics or advertising tag. On a headless site, that means the tag manager is loaded through the framework’s <Script> component with a beforeInteractive strategy or its equivalent, not just dropped into the template with no control over ordering.

The key architectural decision is that server-side tag blocking is impossible on pure SSG, so all the gating logic moves to the client and needs to be part of the first render, not an afterthought script loaded a few seconds after the page becomes interactive.

Step-by-step implementation

1. Choosing a CMP

For most Astro or Next.js projects, a ready-made CMP with a lightweight client SDK is the right fit - it covers the legal side (up-to-date policy text, regional banner variants via IP geolocation) without building your own consent engine from scratch. A self-hosted open-source option (Klaro, for example) makes sense if the company already has dedicated legal control over the wording and a requirement not to load any third-party script at all.

GTM is set up with an initial default consent state - every category except the strictly necessary ones is set to denied until the user makes an explicit choice. This is configured in GTM itself (the Consent Mode default tag) and duplicated at the site level, so it doesn’t depend on the GTM container’s own load delay.

Once the user makes a choice, the CMP calls update for Consent Mode, and GTM recalculates the behavior of connected tags without reloading the page.

Example: loading GTM with Consent Mode support in Astro
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag() { dataLayer.push(arguments); }
  gtag('consent', 'default', {
    ad_storage: 'denied',
    analytics_storage: 'denied',
    ad_user_data: 'denied',
    ad_personalization: 'denied',
    wait_for_update: 500
  });
</script>
<script async src="https://www.googletagmanager.com/gtm.js?id=GTM-XXXXXXX"></script>

After the user consents, the CMP calls gtag('consent', 'update', {...}) with the new per-category values.

Important: the GTM tag itself technically always loads (that’s part of how Consent Mode v2 works - Google collects anonymized conversion signals through cookieless modeling even without consent). Tags with no native Consent Mode support, though - third-party pixels, heatmap scripts, data-collecting chat widgets - need to be physically blocked by the CMP until explicit consent for their category.

4. Saving the user’s choice

The choice is stored with an expiration period (typically 6-12 months, per most DPA recommendations) and re-asked on the next visit after it expires. Changing the choice must be available at any time - a footer link labeled “Cookie settings” is mandatory, not optional.

How not to lose conversions

A banner styled as a full-screen modal with a dimmed background statistically gets more “Accept all” clicks - but it also drives more people to abandon the site outright. An unobtrusive banner at the bottom of the screen that doesn’t block content usually produces a healthier ratio: the user reads the page and makes the decision without pressure.

What’s technically fine and what isn’t. Fine: visually highlighting the “Accept all” button with the brand’s accent color - that’s a standard UI pattern, not a violation. Not fine: making the reject button smaller or lower-contrast, hiding it behind an extra click, or using guilt-tripping wording (“yes, I want to support this site” against a small gray “no, I don’t want to help”).

With partial consent (necessary cookies only), GA4 analytics through Consent Mode v2 switches to conversion modeling - the platform statistically reconstructs the missing data based on aggregated patterns from users who did consent. Accuracy drops, but you don’t get the total data gap that was typical of the pre-Consent Mode era. A plausible range is 10-25% divergence between reported and modeled conversions depending on the rejection rate - the exact figure depends on traffic volume and market.

Common implementation mistakes

  • The CMP is installed, but the script order in the build doesn’t guarantee it loads before GTM - trackers fire before the banner even appears.
  • The CMP’s cookie categories don’t match the actual tags in GTM - the user disabled analytics, but the tag still fires an event because it isn’t tied to the right consent trigger.
  • The banner is localized to English only, even though the audience is multilingual (for sites with hreflang and multiple language versions, the consent text and regional requirements need to match each site version - more in the article on multilingual sites and hreflang).
  • Consent isn’t re-collected when the cookie policy is updated - legally, a fresh request is required whenever the list of cookies in use changes materially.
  • Testing only happens in the developer’s browser with no ad blocker enabled - in production, a share of users with extensions see the banner differently or don’t see it at all, and conversion measurement doesn’t account for that.

Who this matters most for

This matters most for companies doing website development for international markets to enter the EU and other jurisdictions with strict data regulation (the UK, Switzerland, and to some extent California via the CCPA) - the fines and reputational risk here are real, not hypothetical. It’s just as relevant for companies with an already-live international site where the cookie banner is there for show but technical tracker blocking was never implemented - in that case you need targeted site work, not a full rebuild - just the consent layer and script load order.

Frequently asked questions

If a site uses only strictly necessary cookies (session, CSRF protection, load balancing), a banner isn’t formally required under GDPR - but most sites run at least basic analytics (Google Analytics, Yandex Metrika), which already falls under the consent requirement. It’s worth explicitly checking the list of loaded scripts via DevTools or a network analyzer before deciding to skip the banner.

It’s a mechanism where Google’s tags (GA4, Google Ads, GTM) stay technically connected but send Google’s servers an explicit signal that consent wasn’t given. Instead of fully blocking the script, it switches to a cookieless mode with statistical modeling of the missing data. Since March 2024, Consent Mode v2 has been mandatory for showing personalized ads to EEA audiences through Google’s services.

When the consent rejection rate is high (30% or more), GA4 reporting starts to diverge noticeably from actual traffic - conversions get reconstructed through modeling but don’t reflect each individual user’s actual behavior. For businesses with lower traffic (up to a few thousand visits a month), modeling accuracy drops more sharply than for larger sites with enough data volume to calibrate the model well.

Does each language version of a headless site need its own banner?

The banner text needs to be translated and legally adapted to regional requirements (Germany and France, for instance, have historically interpreted CNIL/LfD guidance more strictly than the average reading of GDPR). Technically it’s the same component, just with localized content wired through the same i18n system as the rest of the site’s content.

Can you use an off-the-shelf CMP plugin on a custom headless stack with no CMS?

Most CMP providers (Cookiebot, Osano, CookieYes) ship as a vanilla JS script with no CMS dependency, so they attach to Astro, Next.js, or any other headless stack without restrictions. The difference is how smoothly the provider integrates with an SSR/SSG framework at the level of script load order; that’s worth verifying on a staging environment before going live.

Bottom line

Cookie consent on a headless site is an architectural problem, not a cosmetic add-on: consent state lives on the client, scripts are gated until an explicit choice is made, and Consent Mode v2 keeps analytics working even with partial rejection. If you’re taking a site into the EU market and you’re not sure the banner technically blocks trackers the way it should, walk the Exceltic.dev team through your current site’s architecture. We’ll check the script load order and scope out the work needed.

More articles

All
Get in touch your way
WhatsApp Telegram

Before you go: a free estimate

Describe your task in a few words and we propose a solution, a stack and a quote with timelines.

Three areas with one team: software, web development and CRM implementation. Over 100 projects, and the project is run directly by an engineer, in Russian and English.