# How to add a cookie consent banner to an Astro site

> In Astro, put the snippet in the head of your shared layout (src/layouts/Layout.astro) as the first script, with is:inline so Astro renders it exactly as written. Mark Google and Meta tags in the same layout with type="text/plain" and data-cq-category, and keep marked tags in the layout rather than in pages when you use the ClientRouter.

Source: https://cookiequiet.com/platforms/astro
Published: 2026-10-20
Publisher: Cookiequiet

## Steps

1. **Open your layout** Open the layout component every page uses, typically src/layouts/Layout.astro, which holds the html, head and body tags around a slot.
2. **Add the snippet as the first script** In the head, before any other script, add the Cookiequiet snippet with the is:inline directive. Astro leaves such a script untouched: no bundling, no type="module".
3. **Mark the tags in the layout** Add the Google tag or Meta Pixel in the same head with is:inline, type="text/plain" and data-cq-category set to analytics or marketing. Or keep the Google tag loading in Consent Mode with Google's default consent block placed before it.
4. **Handle embeds** Write YouTube or Vimeo iframes with data-cq-src instead of src so they load after consent. Facade components that create the player on click are held by the auto-blocker until the category is granted.
5. **Mind the ClientRouter** With view transitions, scripts in the head run once per full load and marked scripts in page bodies are not activated during client-side navigation. Keep marked tags in the layout and read window.Cookiequiet in island scripts.
6. **Add the Cookie preferences link** In the footer component, add a plain anchor with href="#cq-preferences" and the data-cq-show-preferences attribute.
7. **Test with Reject all** Run astro dev or preview the build, open the site in a private window, click Reject all and check that no _ga or _fbp cookie is set. Accept all, reload, and check the consent log.

## Where to paste the snippet in Astro

Astro layouts are components that provide the page shell, with `<html>`, `<head>`, `<body>` and a `<slot />` for the page content. The snippet goes in the head of that layout, before any other script. Astro processes a `<script>` only when its sole attribute is `src`; ours carries data attributes, so Astro leaves it alone, but `is:inline` makes the intent explicit:

```astro
---
// src/layouts/Layout.astro
const { title } = Astro.props;
---
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <script is:inline src="https://cookiequiet.com/cq.js" data-site="YOUR_SITE_KEY" data-consent-mode="1" data-auto-block="1" defer></script>
    <meta name="viewport" content="width=device-width" />
    <title>{title}</title>
  </head>
  <body>
    <slot />
  </body>
</html>
```

Every page that uses the layout, including Markdown pages that name it in their `layout` frontmatter, now loads the banner. If your project has several layouts, put the line in each, or in a small `Head.astro` component they all include.

## How to handle the tags your site loads

**Marked tags in the layout.** Add the tags after the snippet, with `is:inline`, `type="text/plain"` and a category. Cookiequiet runs them when the category is granted, or at once on later visits when a stored choice includes it:

```html
<script is:inline type="text/plain" data-cq-category="analytics" src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXX"></script>
<script is:inline type="text/plain" data-cq-category="analytics">
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());
  gtag('config', 'G-XXXXXXX');
</script>
```

The Meta Pixel base code gets `data-cq-category="marketing"`; its noscript image must be removed, as the [Meta Pixel guide](/platforms/meta-pixel) explains.

**Consent Mode instead of marking.** To keep the Google tag loading with denied defaults and cookieless pings, place Google's default consent block (an inline script) between the snippet and the Google tag, as shown in the [Google Analytics 4 guide](/platforms/google-analytics-4). Cookiequiet sends the update after each choice.

**Partytown.** Keep `cq.js` on the main thread: it needs the document. If you move the Google tag into a Partytown worker, forward `dataLayer.push` so the consent commands reach it.

**Embeds.** Write iframes with `data-cq-src` instead of `src`. Facade components that build the YouTube player on click create the iframe with JavaScript; the auto-blocker holds `youtube.com/embed` under marketing until that category is granted. See [YouTube embeds and consent](/guides/youtube-embeds-and-consent).

## What changes with the ClientRouter

With view transitions, Astro swaps the page content without a full load. The snippet in the head keeps running across navigations, the stored choice stays applied and the banner does not reappear. Two consequences:

- Marked scripts inside a page body loaded by client-side navigation are not activated until the next consent event, because Cookiequiet activates marked tags when a choice is applied on load and after each choice. Keep marked tags in the layout.
- Island scripts that need the choice should read `window.Cookiequiet.get()` (the `c` array lists the granted categories) and subscribe with `window.Cookiequiet.on(callback)`, which returns an unsubscribe function.

## How to add the Cookie preferences link

In `src/components/Footer.astro` or wherever the footer lives:

```html
<a href="#cq-preferences" data-cq-show-preferences>Cookie preferences</a>
```

## How to test

1. Run `astro dev` (or `astro build` then `astro preview`) and open the site in a private window.
2. Click **Reject all**: the cookies are `cq_consent` and nothing from Google or Meta.
3. Clear the cookies, click **Accept all**, reload: the marked scripts run.
4. With the ClientRouter, navigate between pages and check that the choice holds.
5. Check the consent log in the dashboard.

## What is specific to Astro

- **Static output.** The banner is a client script; the built site works on any static host (Netlify, Vercel, Cloudflare Pages, GitHub Pages). The article on [cookie banners for static sites](/blog/cookie-banner-for-a-static-site) covers the rest.
- **Content Security Policy.** Allow `https://cookiequiet.com` in `script-src` and `connect-src`.
- **Multiple languages.** The banner's language follows the visitor's browser, with every string editable per language; it does not read the page's `lang`.
- **Content collections and Markdown.** Pages rendered through the layout need no change; tags pasted inside Markdown content would bypass the layout, so keep tags in the layout.

## Questions

### Why is:inline on the snippet?

Astro bundles and transforms scripts it processes, adds type="module" and can hoist them. is:inline tells Astro to render the tag exactly as written, which keeps the data attributes, the defer attribute and the position in the head. A script with attributes other than src is not processed anyway, but the directive makes that explicit.

### Why not import cq.js as a module?

The banner reads its configuration from the attributes of its own script tag (document.currentScript) and must run before the tags it controls. A bundled module import would lose the attributes and the ordering.

### Does the ClientRouter break the banner?

No. The snippet in the head persists across client-side navigations and the stored choice stays applied. Only marked scripts inside page bodies loaded by navigation are not activated until the next choice, so keep marked tags in the layout and gate islands through window.Cookiequiet.

### Can I add the tag through an Astro integration instead?

An integration that injects a script into the head (injectScript with the head-inline stage) works as long as the result is the same tag in the same position. The layout is simpler to audit.

## Sources

- [Astro Docs: Scripts and event handling](https://docs.astro.build/en/guides/client-side-scripts/)
- [Astro Docs: Layouts](https://docs.astro.build/en/basics/layouts/)
- [Google: Set up consent mode on websites](https://developers.google.com/tag-platform/security/guides/consent)
