Skip to main content
Back to Guides
Compliance6 min read

Astro Cookie Consent: Islands and Script Blocking

Astro ships zero JavaScript by default, but the moment you drop in a Google Analytics snippet or a Meta Pixel it fires on render, before any consent. Here's how Astro processes scripts, how to block trackers until a visitor opts in, and how to wire up Consent Mode v2.

Astro's whole pitch is that it ships no JavaScript unless you ask for it. That makes for fast marketing sites, and it also makes cookie consent easy to get wrong. The second you paste a Google Analytics snippet into a layout or add a Meta Pixel through an embed, that tag fires as the page renders, before a visitor has agreed to anything. Astro's performance model does nothing to stop it.

Here's the stakes. In September 2025 France's data protection authority fined the retailer SHEIN 150 million euros, in part because trackers fired before any choice was made and kept firing after "Reject all" (CNIL). A banner that looks correct but tracks on load is exactly the failure regulators penalize. If your Astro site serves the EU, the UK, or a US opt-out state, closing that gap is on you.

How Astro treats a script tag

Astro processes <script> tags by default. It bundles them, treats their contents as TypeScript, converts them to type="module", and can inline small ones into the HTML. That's fine for your own interactive code. It's a problem for a third-party analytics snippet, because module scripts are deferred and bundled, which can change when and whether the vendor code runs.

The escape hatch is is:inline. From the Astro docs: "Astro will not process a <script> tag if it has any attribute other than src" (Astro client-side scripts). Adding is:inline renders the tag "exactly as written," with no bundling and no TypeScript transform. Every raw vendor snippet you paste into Astro should carry is:inline so it behaves the way the vendor expects, and so a consent tool can find it and gate it.

Where the consent loader goes

Put the consent loader in your shared layout (commonly src/layouts/Layout.astro), inside <head>, and make it the first script there, ahead of GA4, GTM, or any pixel. Order isn't cosmetic. The consent script has to initialize before the tags it's meant to control, or it has nothing to hold back.

---
// src/layouts/Layout.astro
---
<html lang="en">
  <head>
    <!-- FIRST, before any analytics or marketing tag -->
    <script is:inline async src="https://cdn.cookiebeam.com/banner/YOUR_BANNER_ID/default/loader.js"></script>
  </head>
  <body>
    <slot />
  </body>
</html>

Because this layout wraps every page, the loader runs on each route. Replace YOUR_BANNER_ID with your banner's public ID from the dashboard.

Blocking trackers until consent

The reliable pattern across every platform is to load a tracker in a disabled state and switch it on after consent. CookieBeam's loader activates scripts marked with type="text/plain" and a data-category attribute once the visitor opts into that category. In Astro this works cleanly, because a tag with attributes beyond src is left untouched:

<!-- Blocked until the visitor accepts analytics -->
<script
  type="text/plain"
  data-category="analytics"
  data-cookiebeam-managed="true"
  is:inline
  src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXX">
</script>

The is:inline keeps Astro from rewriting the tag, so the consent runtime sees it exactly as authored and holds it until consent. For general blocking technique across stacks, see how to block scripts before consent.

Consent-gating an interactive island

Astro's islands hydrate framework components on the client with client:* directives: client:load (hydrate immediately), client:idle (on requestIdleCallback), client:visible (when it scrolls into view), and client:only (skip server render, client only). If a React or Svelte island pulls in an analytics SDK, don't hydrate it until consent exists. Render it conditionally based on the consent state the banner writes:

---
// A component island that should only run with consent
import AnalyticsWidget from '../components/AnalyticsWidget.jsx';
---
<div id="analytics-slot">
  <AnalyticsWidget client:only="react" />
</div>
<script is:inline>
  // Hide the slot until analytics consent is granted
  document.addEventListener('cookiebeam:consent', function (e) {
    var slot = document.getElementById('analytics-slot');
    if (slot) slot.hidden = !e.detail.analytics;
  });
</script>

The cookiebeam:consent event fires with the current categories in e.detail, so your island logic reacts to opt-in and opt-out without a page reload. This is the same event-driven approach used in the single-page app consent guide.

Partytown, and why it doesn't replace consent

Some Astro sites use @astrojs/partytown to move heavy third-party scripts off the main thread into a web worker by tagging them type="text/partytown" (Astro Partytown docs). Partytown is a performance tool. It does not ask for or check consent. A script relocated to a worker still sets cookies and still sends data. Treat Partytown and consent as separate jobs: gate the tag on consent first, then let Partytown handle where it runs once it's allowed to run.

Consent Mode v2 with GTM

If your Astro site runs Google Ads or GA4 for EEA or UK traffic, Consent Mode v2 has been required since March 2024. Set the defaults to denied before GTM loads, then let the banner push an update when the visitor chooses. Both scripts need is:inline so Astro renders them verbatim:

<!-- In <head>, after the consent loader -->
<script is:inline>
  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>
<!-- GTM container snippet goes here, also is:inline -->

CookieBeam sets these defaults and fires the update call on acceptance for you. If tags still misbehave, the Consent Mode v2 troubleshooting guide covers the common breakpoints, and the parameters reference lists every signal.

You still need a cookie policy

A banner without a linked policy is half the job. GDPR and ePrivacy expect visitors to see what each cookie does, how long it lasts, and which third parties set it. Publish a /cookie-policy page in Astro and link it from both the banner and the footer. The trap is drift: every new embed, A/B test, or video player can add cookies the hand-written table never mentions. A consent platform that scans your built pages, classifies what it finds, and keeps the policy current beats a manual list that goes stale the day marketing adds a tag. See automated scanning versus manual audits and writing a policy that matches your cookies.

Test before you deploy

  • Open the built site in a fresh private window from an EU IP and check the Application › Cookies panel before clicking. Only strictly necessary cookies should exist.
  • Click "Reject all" and confirm no _ga, _gid, or _fbp appears and no marketing beacons fire in the Network tab.
  • Accept, then confirm the tags load and Consent Mode flips to granted.
  • Move between routes and re-check. Astro does full-page loads by default, but view transitions can persist state, so verify both.

Run astro build and test the built output rather than the dev server, which injects its own scripts. With CookieBeam the loader also blocks unknown scripts by default and keeps a timestamped consent record, so an Astro site passes the same checks a purpose-built platform would. Pair this with the banner performance guide to keep your Core Web Vitals intact, and run a full consent audit after any new integration.

Astro Cookie Consent: Islands & Script Blocking | CookieBeam | CookieBeam