Skip to content
DocumentationBlog

How to make your vibe-coded site GDPR compliant

The astro-cookie-consent banner overlapping a GDPR reference page, with the Gemini logo

You asked an AI to build you a site, it shipped fast, and it probably added Google Analytics, a Vercel/Plausible snippet, or an embedded widget somewhere along the way — because that’s what sites do. What it almost certainly didn’t add is real consent for any of that. If you have EU (or UK, or a bunch of other) visitors, that’s a compliance gap, not a nitpick.

NOTE! This isn’t legal advice — your privacy policy and what you actually track are still your call. If you want to be fully compliant, refer to GDPR checklist

This is the mechanical part: how to stop non-essential scripts from running before a visitor agrees to them, using astro-cookie-consent.

Why the banner your AI generated probably doesn’t count

Section titled “Why the banner your AI generated probably doesn’t count”

Ask most AI coding assistants for a “cookie consent banner” and you’ll get a <div> with some text and an “OK” button that just hides itself on click. That’s not consent — it’s a notice. The analytics script was already sitting in your <head>, already running, before the visitor read a word of it. Under GDPR/ePrivacy, that’s exactly backwards: non-essential cookies are supposed to load after affirmative consent, not before.

The other common failure mode is a banner that technically gates something, but stacks the deck — a bold “Accept” button next to a greyed-out “Decline” link, or boxes that are pre-ticked to “yes.” Regulators (CNIL, ICO, EDPB) specifically call these out as dark patterns. A banner that nudges isn’t a compliant banner, it’s a compliant-looking banner.

Stripped of legalese, the mechanics regulators check for come down to four things:

  • Nothing non-essential runs before consent. Analytics, ads, embeds — blocked until the visitor says yes.
  • Accept and decline are equally easy. Same visual weight, same number of clicks.
  • Opt-in, not opt-out. Every category defaults to off. A visitor who closes the tab without choosing anything has not consented to anything.
  • You can show your work. If asked, you should be able to point to when and what a given visitor consented to.

That’s the whole list. It’s a mechanism problem, not a writing problem — so let’s fix it with code.

Terminal window
npm install astro-cookie-consent

Drop the banner into your layout, once:

src/layouts/Layout.astro
---
import ConsentBanner from 'astro-cookie-consent/ConsentBanner.astro';
---
<html>
<body>
<slot />
<ConsentBanner privacyHref="/privacy" />
</body>
</html>

That alone gets you equal-weight Accept/Decline buttons, everything off by default, and a banner that reappears after consent expires (365 days by default) — see why those defaults are the way they are. None of it needed a config file or a line of CSS.

The banner is only half of it — the other half is making sure trackers genuinely don’t load until consent is given. That’s what gateScript() is for:

<script>
import { gateScript } from 'astro-cookie-consent';
gateScript({ src: 'https://www.googletagmanager.com/gtag/js?id=YOUR-ID' });
</script>

gateScript() won’t inject that <script src> until the visitor accepts, and it only ever injects it once. If your AI-generated site has a <script src="..."> sitting directly in Layout.astro or a page <head> right now, that’s the line to go find and wrap.

A single accept/decline is enough for a lot of sites, but if you’re running analytics and marketing pixels, GDPR expects visitors to be able to consent to one without the other. Pass categories and check them individually before gating:

<ConsentBanner
categories={[
{ id: 'analytics', label: 'Analytics', description: 'Helps us understand how this site is used.' },
{ id: 'marketing', label: 'Marketing', description: 'Used for ad retargeting.' },
]}
/>
<script>
import { getConsent, gateScript } from 'astro-cookie-consent';
if (getConsent('analytics')) {
gateScript({ src: 'https://www.googletagmanager.com/gtag/js?id=YOUR-ID' });
}
</script>

If you ever need to demonstrate that consent was actually given — Art. 7(1) puts that burden on you, not the visitor — every choice is stored with a timestamp and retrievable with getConsentRecord(). No extra setup, no server, no database.

No config file, no cookie-consent-as-a-service subscription, no CSS to fight. If you vibe-coded the rest of the site, this is the one part worth doing on purpose: