# Consent Under the Mark, Once, Never a Banner

> The preloader asks for analytics consent once, under the mark. No banner, no fixed control. Here is how the pattern works and why it holds.

- Author: Dil Ahdan (https://dil.dev/)
- Published: 2026-09-27
- Updated: 2026-09-27
- Tags: analytics, consent, GA4, privacy
- Canonical: https://dil.dev/notes/consent-under-the-mark-not-a-banner/

## Key takeaways

- Ask for consent once, in the preloader, under the mark, not in a floating banner.
- Gate GA4 and Meta Pixel on that single consent signal, never load vendor base code unconditionally.
- Put change-your-choice controls on the privacy page, linked from the footer, not in a persistent UI element.
- Verify the whole flow with node scripts/verify-consent.mjs from the site/ directory against a preview build.
- Never paste vendor base code directly into pages. Set credentials in the environment instead.

The consent prompt lives in the preloader, under the mark, and it fires exactly once. There is no cookie banner. There is no floating privacy widget. The privacy page, reachable from the footer, is where a user changes their mind later. That is the whole pattern, and it is intentional.

## Why the Preloader Is the Right Moment

The preloader covers the full viewport from the first frame. Nothing else is visible yet. That makes it the only honest moment to ask: the user has not interacted with any content, so the consent is genuinely prior to data collection.

A separate banner would ask the same question a second time, after the page has already partially rendered. That is worse UX and harder to audit. One moment, one prompt, one signal.

The preloader in `site/src/preloader.js` handles two cases:

- **Home page:** gates on the skull mesh being ready before showing the prompt.
- **All other pages:** gates on fonts loading instead.

Either way, the prompt appears before the page becomes interactive. The user sees the mark, reads the ask, makes a choice, and the page continues.

## What Gets Gated

Two vendors are consent-gated: GA4 and Meta Pixel.

- **GA4** identifier is set in the Netlify production environment.
- **Meta Pixel** identifier is also set there.

Neither fires a single request until a positive consent signal comes back from the preloader. If the user declines, both stay silent for the session. If they accept, both initialise from that point forward.

Credentials live in the Netlify environment, not in page source. That is a separate constraint from consent, but it reinforces the same principle: vendor code does not touch the page unconditionally.

## Never Paste Vendor Base Code Into Pages

This one is worth stating plainly because the temptation is real. GA4 and Meta both provide copy-paste snippets they tell you to drop into `<head>`. Do not do it.

Pasting base code into the page means the vendor loads unconditionally, before consent, on every page view. You can try to suppress the actual tracking calls, but the script itself has already executed. That is not consent-gated analytics. That is analytics with a consent-shaped sticker on top.

The correct approach: load vendor scripts programmatically, after the consent signal, using identifiers pulled from the environment. The preloader owns that logic.

## The Footer Link and the Privacy Page

The footer, assembled in `site/lib/chrome.mjs`, includes a link to the privacy page. That link is the only persistent entry point to consent controls.

The privacy page does two things:

- **Explains** what is collected and why.
- **Provides controls** to change the stored consent choice.

That is it. There is no floating button in the corner. There is no persistent banner that reappears after dismissal. AGENTS.md is explicit: never add a fixed privacy control or a banner. The footer link is sufficient.

This keeps the UI clean and keeps the consent logic in one place. If you want to audit what a user can do about their data, you look at one page, not at a widget scattered across every route.

## Verifying the Flow

The verification step is `node scripts/verify-consent.mjs`, run from the `site/` directory against a preview build.

Run it before merging anything that touches:

- The preloader
- The footer or chrome
- Environment variable names for GA4 or Meta Pixel
- The privacy page controls

The script is the canonical check. AGENTS.md lists it as the source of truth. Do not rely on manual browser testing alone, because it is easy to miss the timing edge cases around when scripts initialise relative to the consent signal.

A preview build rather than local dev matters here. Environment variables behave differently in local dev, and you want to verify against the same configuration that production uses.

## What the Pattern Prohibits

To be concrete about what must not be added:

- **No cookie banner.** Not a slide-in, not a modal, not a bottom bar.
- **No fixed privacy control.** Not a floating button, not a persistent icon in a corner.
- **No unconditional vendor script loading.** Not in `<head>`, not in a layout component, not anywhere that runs before the consent signal.
- **No hardcoded vendor identifiers in page source.** They belong in the Netlify environment.

These are not style preferences. They are constraints in AGENTS.md, which is the current production source of truth. If a future contributor adds a banner thinking they are being helpful, the verify script should catch the behavioural change, but the architectural constraint is the more important guard.

## What I'd do differently

The gating on skull mesh versus fonts is a reasonable split, but it means the consent timing is slightly different depending on which page a user lands on first. That is fine in practice, but it adds a branch to reason about.

If I were starting fresh, I would look at whether a single gating condition, probably fonts, could work on every page including home. The skull mesh gate exists because the home page preloader is visually tied to that asset. Decoupling the consent timing from the visual gate would simplify the logic, even if it meant the home preloader had to handle the two concerns separately.

The verify script is good. I would add it to the CI pipeline explicitly rather than leaving it as a manual step, so it runs on every preview deploy automatically rather than depending on a contributor remembering to run it.

## Quick answers

**Why ask for consent in the preloader instead of a cookie banner?**

The preloader already covers the full viewport from the first frame, so it is the natural moment to ask before anything else loads. A separate banner would duplicate that moment and add visual noise. Asking once, in context, is cleaner and easier to reason about.

**Where does a user go to change their analytics choice later?**

The privacy page holds the change-your-choice controls. It is linked from the footer on every page via site/lib/chrome.mjs. There is no persistent floating control anywhere in the UI.

**Which analytics vendors are gated behind consent?**

GA4 and Meta Pixel are both gated. Neither fires until the user has given a positive consent signal in the preloader. Credentials live in the Netlify production environment, not in page source.

**How do I verify the consent flow is working correctly?**

Run node scripts/verify-consent.mjs from the site/ directory against a preview build. That script is the canonical check described in AGENTS.md.

**What happens on pages that are not the home page?**

On the home page the preloader gates on the skull mesh being ready. On other pages it gates on fonts instead. Either way the consent prompt appears before the page becomes interactive.

**Can I add a fixed privacy control to the corner of every page?**

No. AGENTS.md explicitly prohibits a fixed privacy control and a banner. The footer link to the privacy page is the only persistent entry point to the controls.
