Cookie consent banner
@slashclone/cookie-consent-banner
Add an honest cookie banner to a web app: analytics, ad pixels, and video embeds load only after a yes, and saying no is one click. One registry of every cookie and service drives the banner, the loaders, the CSP, and the privacy page's cookie table. Includes a registry checker and React templates.
- Activity
- New: be the first to try it
- Installs
- 0 installs
- Likes
- 0 likes
- Clones
- 0 clones
- 1/**
- 2 * Cookie consent core: the stored choice and the rules for applying it. Framework free.
- 3 * Pair it with a registry like examples/services.json (ids, categories, what each stores).
- 4 */
- 6export const CONSENT_COOKIE = "consent";
- 7/** About six months, then ask again. */
- 8export const CONSENT_MAX_AGE_SECONDS = 182 * 24 * 60 * 60;
- 10export const CATEGORIES = ["analytics", "advertising", "media"] as const;
- 11export type Category = (typeof CATEGORIES)[number];
- 13export interface ConsentChoice {
- 14 v: 1;
- 15 analytics: boolean;
- 16 advertising: boolean;
- 17 media: boolean;
- 18 /** Service ids the visitor saw when choosing; a new one means ask again. */
- 19 s: string[];
- 20 at: number;
- 21}
- 23export function parseConsent(raw: string | undefined | null): ConsentChoice | null {
- 24 if (!raw) return null;
- 25 try {
- 26 const c = JSON.parse(decodeURIComponent(raw)) as Partial<ConsentChoice>;
- 27 const ok =
- 28 c.v === 1 &&
- 29 typeof c.analytics === "boolean" &&
- 30 typeof c.advertising === "boolean" &&
- 31 typeof c.media === "boolean" &&
- 32 Array.isArray(c.s) &&
- 33 c.s.every((id) => typeof id === "string") &&
- 34 typeof c.at === "number";
- 35 return ok ? (c as ConsentChoice) : null;
- 36 } catch {
- 37 return null;
- 38 }
- 39}
- 41export function serializeConsent(choice: ConsentChoice): string {
- 42 return encodeURIComponent(JSON.stringify(choice));
- 43}
- 45/**
- 46 * The choice to apply, or null to ask. Global Privacy Control keeps advertising off
- 47 * whatever was stored; a service added since the choice asks again.
- 48 */
- 49export function effectiveConsent(
- 50 stored: ConsentChoice | null,
- 51 currentServiceIds: string[],
- 52 gpc: boolean,
- 53): ConsentChoice | null {
- 54 if (!stored) return null;
- 55 if (currentServiceIds.some((id) => !stored.s.includes(id))) return null;
- 56 return gpc ? { ...stored, advertising: false } : stored;
- 57}
- 59/** Page URLs for trackers: origin and path only. Query strings can hold search text or tokens. */
- 60export function trackedUrl(href: string): string {
- 61 try {
- 62 const url = new URL(href);
- 63 return `${url.origin}${url.pathname}`;
- 64 } catch {
- 65 return "";
- 66 }
- 67}
- 69/** Ad pixels read the full URL themselves, so keep them off pages holding what someone typed. */
- 70export function pixelsMaySend(
- 71 href: string,
- 72 typedParams: string[] = ["q", "query", "search"],
- 73): boolean {
- 74 try {
- 75 const params = new URL(href).searchParams;
- 76 return !typedParams.some((p) => params.has(p));
- 77 } catch {
- 78 return false;
- 79 }
- 80}
- 82/** Browser only: deletes cookies and storage keys matching a revoked category's pattern. */
- 83export function clearMatching(pattern: RegExp): void {
- 84 const host = window.location.hostname;
- 85 const domains = ["", host, `.${host.split(".").slice(-2).join(".")}`];
- 86 for (const c of document.cookie.split("; ")) {
- 87 const name = c.split("=")[0] ?? "";
- 88 if (!pattern.test(name)) continue;
- 89 for (const d of domains)
- 90 document.cookie = `${name}=; Path=/; Max-Age=0${d ? `; Domain=${d}` : ""}`;
- 91 }
- 92 try {
- 93 for (const key of Object.keys(localStorage))
- 94 if (pattern.test(key)) localStorage.removeItem(key);
- 95 } catch {
- 96 // Storage can be blocked; then there is nothing to clear.
- 97 }
- 98}
- 100export function gpcOn(): boolean {
- 101 return (
- 102 (navigator as Navigator & { globalPrivacyControl?: boolean }).globalPrivacyControl === true
- 103 );
- 104}
