Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 | 1x 1x 21x 21x 8x 7x 1x 21x 11x 11x 11x 10x 10x 10x 3x 2x 2x 2x 3x 7x 7x 11x 5x 11x 11x 5x 11x 1x 1x 1x | // Cookie banner + Google Consent Mode v2 (#1086).
//
// GoogleTagManager.astro sets the consent default (all four types denied)
// inline, before GTM, and re-applies a stored "granted" on every page load.
// This module owns the banner: CookieBanner.astro renders it `hidden` (the
// edge cache keys on URL only, so the server cannot know the visitor's
// answer), `syncBanner` shows it until they answer, and a click on Accept or
// Reject pushes the consent update and stores the answer for a year.
//
// No side effects on import: GoogleTagManager.astro imports CONSENT_COOKIE
// during SSR, where `document` does not exist. CookieBanner.astro's bundled
// script calls `initCookieConsent()`.
//
// ponytail: withdrawing consent stops new analytics/ads cookies but does not
// delete `_ga` / `_fbp` already set. Delete them here if the owner or a
// regulator asks.
export const CONSENT_COOKIE = "ir_consent";
export type ConsentChoice = "granted" | "denied";
declare global {
interface Window {
gtag?: (...args: unknown[]) => void;
}
}
const CHOICE_RE = new RegExp(
`(?:^|;\\s*)${CONSENT_COOKIE}=(granted|denied)(?:;|$)`,
);
/** The stored answer in a `document.cookie` string, or null if none. */
export function readChoice(cookie: string): ConsentChoice | null {
const match = CHOICE_RE.exec(cookie);
return match ? (match[1] as ConsentChoice) : null;
}
/** The four Consent Mode v2 types, all set to one choice. */
export function consentState(choice: ConsentChoice): Record<string, string> {
return {
ad_storage: choice,
analytics_storage: choice,
ad_user_data: choice,
ad_personalization: choice,
};
}
/** The `document.cookie` assignment that stores a choice for 12 months. */
export function cookieFor(choice: ConsentChoice, secure: boolean): string {
return `${CONSENT_COOKIE}=${choice}; Max-Age=31536000; Path=/; SameSite=Lax${secure ? "; Secure" : ""}`;
}
// The "Cookie settings" control that reopened the banner. Focus goes back to
// it after a choice; on a first visit there is none and focus stays put.
let opener: HTMLElement | null = null;
function banner(): HTMLElement | null {
return document.querySelector<HTMLElement>("[data-cookie-banner]");
}
/** Show the banner until the visitor has answered; keep it hidden after. */
export function syncBanner(): void {
const el = banner();
if (el) el.hidden = readChoice(document.cookie) !== null;
}
export function handleConsentClick(e: MouseEvent): void {
if (!(e.target instanceof Element)) return;
const el = banner();
// "Cookie settings" (footer, privacy page) reopens the banner to change the answer.
const settings = e.target.closest<HTMLElement>("[data-cookie-settings]");
if (settings) {
if (!el) return;
opener = settings;
el.hidden = false;
el.querySelector<HTMLButtonElement>("button")?.focus();
return;
}
const button = e.target.closest<HTMLElement>("[data-consent-choice]");
const choice = button?.dataset.consentChoice;
if (choice !== "granted" && choice !== "denied") return;
window.gtag?.("consent", "update", consentState(choice));
// biome-ignore lint/suspicious/noDocumentCookie: the async Cookie Store API is missing in older Safari and Firefox
document.cookie = cookieFor(choice, location.protocol === "https:");
if (el) el.hidden = true;
opener?.focus();
opener = null;
}
export function initCookieConsent(): void {
syncBanner();
// One delegated listener survives ClientRouter navigations; after-swap
// (not page-load) re-syncs the freshly swapped-in banner before paint.
document.addEventListener("click", handleConsentClick);
document.addEventListener("astro:after-swap", syncBanner);
}
|