Consent
A consent provider answers one question per request: may the analytics beacons
run? A site installs at most one, and @ouncepage/cookie-consent is the
implementation that ships.
What the engine does
Section titled “What the engine does”Beacons.astro renders the analytics beacons, and it is the only place they
reach a page. When a consent plugin is installed and enabled, that component
asks it before rendering anything.
The gate is server side. While consent is outstanding the tracker’s <script>
is never written into the HTML, so there is no inert tag to reactivate and
nothing in the source for a reader to find. Sites already render <Beacons> in
their layout, so installing a consent plugin gates them with no template change.
interface ConsentDecision { required: boolean; granted: boolean;}
interface ConsentProvider { name: string; label: string; cookie: string; decide(stored: string | null, config: Record<string, unknown>): ConsentDecision;}cookie names the cookie the engine reads. decide receives its value, or
null when it is absent, along with the plugin’s stored configuration. Beacons
render when granted is true or required is false, so a provider that never
blocks anything returns { required: false, granted: true }.
Installing one
Section titled “Installing one”import { cookieConsent } from '@ouncepage/cookie-consent';import { ga4 } from '@ouncepage/ga4-analytics';
export default defineSite({ plugins: [ga4(), cookieConsent()],});Then render the banner once, in the layout that already renders <Beacons>:
<Beacons beacons={beacons} /><Consent />Installing the plugin is what blocks the beacons; rendering the banner is what lets a visitor unblock them. In the default opt-in mode, a site that installs the plugin and forgets the component collects nothing at all.
A third piece is not optional either. Consent that cannot be withdrawn as
easily as it was given is not consent, so the site needs a permanent way back
to the banner. @ouncepage/cookie-consent ships ConsentLink.astro for a
footer, and anything carrying data-consent-open does the same job.
Two providers throw at startup, the same way two assistants or two MCP providers do. Ounce reads one cookie, so there is one answer.
Declaring CSP hosts
Section titled “Declaring CSP hosts”A consent banner needs an inline script, and the public policy is script-src 'self' plus the hosts plugins declare, with no 'unsafe-inline' anywhere.
Any plugin, not only an analytics one, can add to that policy:
export function cookieConsent(): Plugin { return { name: 'cookie-consent', title: 'Cookie consent', hosts: { script: [SCRIPT_HASH] }, };}hosts takes the same six keys as the site’s own csp block: script,
style, img, font, connect and frame. An analytics provider’s
hosts still works and still covers only the three a beacon needs, so a plugin
that wants font or frame uses this instead.
Everything in script is emitted as written, which is what lets a 'sha256-...'
through as well as an origin. The same rule as a beacon’s init applies: one
hash has to cover every site, so hash the bytes you actually emit and keep the
script free of anything that varies.
Writing a provider
Section titled “Writing a provider”export const cookieConsentProvider: ConsentProvider = { name: 'cookie-consent', label: 'Cookie consent', cookie: 'ounce_consent', decide(stored, config) { const { mode } = settings(config); if (mode === 'notice') return { required: false, granted: true }; if (mode === 'opt-out') return { required: true, granted: stored !== 'no' }; return { required: true, granted: stored === 'yes' }; },};decide is called on every public render, so keep it synchronous and cheap. It
reads the cookie’s raw string: treat anything you did not write as no answer
rather than as consent.
Reach the live configuration from your own components with activeConsent,
which returns the plugin, its provider and its stored config, or null when
nothing is installed or the plugin is switched off:
import { activeConsent } from '@ouncepage/core/plugin-store';
const live = await activeConsent(site.ounce);const stored = live ? Astro.cookies.get(live.provider.cookie)?.value ?? null : null;Limits
Section titled “Limits”- It gates analytics beacons and nothing else. A tracker hardcoded into a template is invisible to it.
- The decision is per request, from a cookie. There is no server-side record of who consented and no audit trail.
- Answering reloads the page, so nothing starts tracking mid-visit.