DIL.DEV
LOADING0%
DIL.DEV
← ALL NOTES

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.

analyticsconsentGA4privacy

A minimal preloader screen showing a skull mark with a single consent prompt beneath it.

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, 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.

Get the next note in your inbox.

One email per note. No spam. Reply “stop” any time and you are off the list.