Cookie consent
A consent banner is only the interface: what matters is which code runs before, after and instead of a choice, and whether that holds on a reload, in another tab, after a withdrawal and when the consent backend is down. Source: c15t's documentation at https://c15t.com (Apache-2.0, https://c15t.com/docs/oss/license.md; copied with the tag [c15t], read 2026-10-18 against c15t 3.1.0, the latest npm dist-tag), its hosted setup skill https://c15t.com/SKILL.md, and the official pages each file cites (Google Consent Mode, the California Attorney General's CCPA FAQ, GDPR). The landing page's own banner is the owner's design in landing-page-ui SECTIONS.md; this skill states c15t's rules and lists where that design differs, never as fixes. Google Analytics itself is google-analytics SKILL.md and the Google side of Consent Mode is its google-analytics CONSENT-MODE.md; PostHog's own consent options are posthog PROXY-AND-PRIVACY.md; a consent journey as an end-to-end test is browser-ui-qa CONSENT-JOURNEY.md.
Apply only the routes and checks for the selected job. Read a linked skill's named file from its installed folder when available, or from the linked source. Report an unavailable required reference instead of inventing it. [skill-authoring, DISTRIBUTION.md#composing-skills]
| The job | Read |
|---|---|
The five categories by purpose, policy models (opt-in, opt-out, iab, none) and prompts (choice, notice, none), regional presets and their limits, Global Privacy Control, withdrawal as easy as consent, permission vs recorded choice, why a banner is missing |
CATEGORIES-AND-POLICIES.md |
Inventory before install, one loader per vendor, framework vendor packages to replace, loading behaviour per helper, keeping events without fake queues, withdrawal and reload, the network blocker and what it cannot stop, data-src iframes, plain HTML gating, clearing cookies on revocation, Google Consent Mode loadMode, tag managers, PostHog, naming |
SCRIPT-GATING.md |
| What happens on save, the retry queue, cookie plus localStorage with the cookie authoritative, per-category merge across tabs, the clear epoch, server rendering and caching, the build-time manifest, backend modes, ad blockers | CONSENT-STATE.md |
| Banner variants and positions, blocking vs non-blocking, accept and reject at equal prominence, the notice prompt, closing without saving, motion and reduced motion, keyboard and narrow screens, Content Security Policy, banner experiments | BANNER-UI.md |
| The release check by network requests: production build, the visitor flow, locations, GPC, a failed backend, the interface, per-vendor checks, what to record | VERIFY-CONSENT.md |
c15t setup: choosing the setup per framework and rendering, exact-version install, one @c15t/core, Next.js App Router and React quickstarts, the upgrade and replace paths, the three troubleshooting checks |
C15T.md |
The rules
- The banner does not gate anything. "A banner is only the interface. It does not stop a script you load with a plain
<script>tag or a vendor SDK you initialize yourself." Register every optional tool with the consent manager and remove its other loaders [c15t]. - Assign a category by what the code does. "Calling analytics
necessarydoes not make it necessary." [c15t] (CATEGORIES-AND-POLICIES.md) - A permission is not a recorded choice. Gate code on the permission ("may this run now?"); report and display the recorded choice ("what did the visitor decide?"). "Never turn a permission into a saved choice. Hydrating server state, reading a cookie or rendering a component does not count as a visitor action." [c15t]
- Nothing optional runs until the policy has resolved: "while it loads, and if it fails, every optional category is denied." "A missing banner must never mean 'allowed'." Do not "fix" a missing banner by granting every category or turning consent off [c15t].
- Never save a choice on page load "to make persistence 'stick'. That records consent the visitor did not give." [c15t]
- Keep a way back. "Keep a persistent preferences entry point wherever the policy provides that right. A visitor who dismissed a notice or rejected tracking still needs a way to revisit their choice." [c15t] GDPR Art 7(3): "It shall be as easy to withdraw as to give consent." [https://eur-lex.europa.eu/eli/reg/2016/679/oj/eng]
- Withdrawal must stop the tool. "A one-time gate at startup is not enough." Code that already ran cannot be unloaded, so a revocation reloads the page; gating also leaves cookies behind, so list what each category owns for cleanup [c15t] (SCRIPT-GATING.md).
- Reject and accept at equal prominence on a choice prompt; an arm that makes accept primary while reject stays neutral is flagged
equivalent-prominence-overridden[c15t] (BANNER-UI.md). - Do not claim compliance. "Denied consent does not always mean zero network traffic, and choosing c15t does not make the site legally compliant. State the behavior you configured; do not claim compliance." Presets "do not decide which laws apply to your business" [c15t].
- Verify by network requests and storage, in a production build. "'The banner appears' is not a test result." [c15t] (VERIFY-CONSENT.md)
Prove it
In a production build and a fresh private window, under a policy that asks for a choice: no optional vendor domain appears in the Network panel before a choice (or, for an always-loading Google helper, its consent default denies every optional type); Reject survives a reload and a new tab; the footer control reopens preferences with the rejected categories off; allowing one category loads only its vendors, once; turning it off reloads the page, the vendor stays absent and its listed cookies are gone; blocking the init request shows no banner and no vendor request; GPC denies the categories the policy restricts and the restriction ends when GPC is off; the save reaches the backend; and every file's own Prove it checks pass for the parts you touched.