Skip to content

Commit f5d47c5

Browse files
pubkeyclaude
andauthored
docs: replace Cookiebot with a client-side consent banner (EU-only) (#8783)
* docs: replace Cookiebot with client-side consent (EU-only banner) Replaces the hosted Cookiebot CMP with the fully client-side npm package vanilla-cookieconsent and gates the trackers behind consent. - Show the consent banner only to likely EU/EEA (and UK) visitors, detected from the browser timezone (request-free, no PII sent). Non-EU visitors keep the previous behavior where trackers start immediately. - Add Google Consent Mode v2 defaults (denied, region-scoped to EU/EEA/UK) as the first head tag so GA4/GTM are gated accurately by Google's server-side region detection until the visitor consents. - Gate the non-Consent-Mode trackers (Reddit pixel, Pipedrive) and all triggerTrackingEvent calls behind the consent decision. - Remove Cookiebot script/preconnect/CSS remnants and add banner theming. - Note the consent banner and a "reopen settings" link on the privacy page. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEnuj2ikuRMC56mwKmYqwY * docs: add required attributes field to Consent Mode head tag Docusaurus headTags entries require an "attributes" object even for inline script tags, otherwise config validation fails with "headTags[0].attributes is required". Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEnuj2ikuRMC56mwKmYqwY * docs: match cookie consent banner to the dark site theme The vanilla-cookieconsent default is a light banner, which looked too bright against the dark RxDB docs theme. Fully theme the banner with the site dark palette and brand color, mapped through the existing CSS variables. Uses the "html #cc-main" selector so the overrides win the cascade over the library stylesheet, which is injected at runtime after custom.css. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEnuj2ikuRMC56mwKmYqwY * docs: shorten cookie consent banner description Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEnuj2ikuRMC56mwKmYqwY * docs: use requested cookie consent banner wording Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEnuj2ikuRMC56mwKmYqwY --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent ab9dcfb commit f5d47c5

8 files changed

Lines changed: 490 additions & 134 deletions

File tree

docs-src/docusaurus.config.ts

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type { Config } from '@docusaurus/types';
88
import rehypePrettyCode from 'rehype-pretty-code';
99
import type { Options as RehypePrettyCodeOptions, Theme } from 'rehype-pretty-code';
1010
import { createCssVariablesTheme, ThemeRegistrationAny } from 'shiki';
11+
import { EU_EEA_REGION_CODES } from './src/theme/eu-consent';
1112

1213
const rehypePrettyCodeOptions: RehypePrettyCodeOptions = {
1314
theme: createCssVariablesTheme({
@@ -26,10 +27,35 @@ const config: Config = {
2627
tagline: 'Realtime JavaScript Database',
2728
favicon: '/files/logo/logo.svg',
2829
headTags: [
30+
/**
31+
* Google Consent Mode v2 default. For EU/EEA (and UK) regions the
32+
* analytics and ad storage is denied until the visitor accepts in the
33+
* client-side consent banner (see src/theme/consent-manager.ts).
34+
* Google detects the region server-side from the IP, so this stays
35+
* accurate independent of the timezone heuristic used to decide
36+
* whether the banner is shown. The commands are queued on dataLayer
37+
* before the async gtag/GTM libraries boot, so the default applies to
38+
* the very first hit.
39+
*/
40+
{
41+
tagName: 'script',
42+
attributes: { type: 'text/javascript' },
43+
innerHTML: `
44+
window.dataLayer = window.dataLayer || [];
45+
function gtag(){dataLayer.push(arguments);}
46+
gtag('consent', 'default', {
47+
ad_storage: 'denied',
48+
ad_user_data: 'denied',
49+
ad_personalization: 'denied',
50+
analytics_storage: 'denied',
51+
wait_for_update: 500,
52+
region: ${JSON.stringify(EU_EEA_REGION_CODES)}
53+
});
54+
gtag('set', 'ads_data_redaction', true);
55+
`.replace(/\s+/g, ' ').trim(),
56+
},
2957
{ tagName: 'meta', attributes: { name: 'theme-color', content: '#ed168f' } },
3058
{ tagName: 'link', attributes: { rel: 'apple-touch-icon', href: '/img/apple-touch-icon.png', sizes: '180x180' } },
31-
{ tagName: 'link', attributes: { rel: 'preconnect', href: 'https://consentcdn.cookiebot.com/' } },
32-
{ tagName: 'link', attributes: { rel: 'preconnect', href: 'https://consent.cookiebot.com/' } },
3359
{ tagName: 'link', attributes: { rel: 'preconnect', href: 'https://region1.analytics.google.com/' } },
3460
{ tagName: 'link', attributes: { rel: 'preconnect', href: 'https://www.redditstatic.com/' } },
3561
{ tagName: 'link', attributes: { rel: 'preconnect', href: 'https://pixel-config.reddit.com/' } },
@@ -304,21 +330,6 @@ Topic-specific documentation files:
304330
},
305331
],
306332
scripts: [
307-
// {
308-
// id: 'CookieDeclaration',
309-
// src: 'https://consent.cookiebot.com/c429ebbd-6e92-4150-b700-ca186e06bc7c/cd.js',
310-
// type: 'text/javascript'
311-
// }
312-
313-
// already included via google tag manager
314-
// {
315-
// id: 'Cookiebot',
316-
// src: 'https://consent.cookiebot.com/uc.js?cbid=c429ebbd-6e92-4150-b700-ca186e06bc7c',
317-
// 'data-cbid': 'c429ebbd-6e92-4150-b700-ca186e06bc7c',
318-
// 'data-blockingmode': 'auto',
319-
// type: 'text/javascript',
320-
// async: true
321-
// },
322333
/*
323334
* Pipedrive embedded chat.
324335
* Disabled because people should fill out the premium form

docs-src/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@
3131
"shiki": "3.19.0",
3232
"to-vfile": "6.1.0",
3333
"unified": "9.0.0",
34-
"unist-util-is": "4.0.2"
34+
"unist-util-is": "4.0.2",
35+
"vanilla-cookieconsent": "3.1.0"
3536
},
3637
"devDependencies": {
3738
"@docusaurus/module-type-aliases": "3.9.2",

docs-src/src/components/trigger-event.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,24 @@
11
import { useEffect } from 'react';
22
import { getTestGroupEventPrefix } from './a-b-tests';
33
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
4+
import { isLikelyEuUser } from '../theme/eu-consent';
5+
6+
/**
7+
* Gates all tracking. EU/EEA visitors start blocked and are only unlocked
8+
* by setTrackingConsent(true) once they accept via the consent banner.
9+
* Everyone else is allowed from the first event, matching the previous
10+
* behavior. During SSR we stay blocked (no events fire there anyway).
11+
*/
12+
let trackingConsentGranted = ExecutionEnvironment.canUseDOM
13+
? !isLikelyEuUser()
14+
: false;
15+
16+
/**
17+
* Called by the consent manager once the visitor made a choice.
18+
*/
19+
export function setTrackingConsent(granted: boolean): void {
20+
trackingConsentGranted = granted;
21+
}
422

523

624
export type RedditEventType =
@@ -124,6 +142,13 @@ export function triggerTrackingEvent(
124142
if (!ExecutionEnvironment.canUseDOM) {
125143
return;
126144
}
145+
/**
146+
* Do not send anything to analytics, ad or conversion services until the
147+
* visitor allowed it. For non-EU visitors this is granted by default.
148+
*/
149+
if (!trackingConsentGranted) {
150+
return;
151+
}
127152
const prefix = 'event_count_';
128153
const stored = localStorage.getItem(prefix + type);
129154
const triggeredBefore = stored ? parseInt(stored, 10) : 0;

docs-src/src/css/custom.css

Lines changed: 66 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -2782,62 +2782,73 @@ footer a:visited {
27822782
}
27832783

27842784
/**
2785-
* Change stuff in cookie banner
2785+
* Theming for the client-side cookie consent banner
2786+
* (vanilla-cookieconsent, see src/theme/consent-manager.ts).
2787+
*
2788+
* The website is a dark theme, but the library ships a light default. Its
2789+
* stylesheet is injected at runtime (after custom.css), so we use the higher
2790+
* specificity selector "html #cc-main" to make sure these values win the
2791+
* cascade regardless of stylesheet order, and map everything to the RxDB
2792+
* dark palette / brand color.
27862793
*/
2787-
#CybotCookiebotDialogHeader {
2788-
display: none !important;
2789-
}
2790-
2791-
#CybotCookiebotDialog {
2792-
padding: 12px !important;
2793-
font-family: var(--ifm-font-family-monospace) !important;
2794-
}
2795-
2796-
.CybotCookiebotDialogBodyBottomWrapper {
2797-
margin-top: 0.5em !important;
2798-
}
2799-
2800-
#CybotCookiebotDialogBodyLevelButtonLevelOptinAllowAll {
2801-
background-color: var(--color-top) !important;
2802-
}
2803-
2804-
#CybotCookiebotDialogBodyLevelButtonCustomize {
2805-
border-color: var(--color-top) !important;
2806-
}
2807-
2808-
#CybotCookiebotDialogPoweredByText {
2809-
display: none !important;
2810-
}
2811-
2812-
#__docusaurus {
2813-
position: relative;
2814-
}
2815-
2816-
/**
2817-
* Change stuff in cookie banner
2818-
*/
2819-
#CybotCookiebotDialogHeader {
2820-
display: none !important;
2821-
}
2822-
2823-
#CybotCookiebotDialog {
2824-
padding: 12px !important;
2825-
}
2826-
2827-
.CybotCookiebotDialogBodyBottomWrapper {
2828-
margin-top: 0.5em !important;
2829-
}
2830-
2831-
#CybotCookiebotDialogBodyLevelButtonLevelOptinAllowAll {
2832-
background-color: var(--color-top) !important;
2833-
}
2834-
2835-
#CybotCookiebotDialogBodyLevelButtonCustomize {
2836-
border-color: var(--color-top) !important;
2837-
}
2838-
2839-
#CybotCookiebotDialogPoweredByText {
2840-
display: none !important;
2794+
html #cc-main {
2795+
--cc-font-family: var(--ifm-font-family-base);
2796+
--cc-modal-border-radius: 12px;
2797+
--cc-btn-border-radius: 8px;
2798+
2799+
/* surfaces + text */
2800+
--cc-bg: var(--bg-color, #20293c);
2801+
--cc-primary-color: #ffffff;
2802+
--cc-secondary-color: #c7ccd6;
2803+
--cc-separator-border-color: rgba(255, 255, 255, 0.1);
2804+
2805+
/* primary button = brand color */
2806+
--cc-btn-primary-bg: var(--color-top, #ed168f);
2807+
--cc-btn-primary-color: #ffffff;
2808+
--cc-btn-primary-border-color: var(--color-top, #ed168f);
2809+
--cc-btn-primary-hover-bg: var(--color-middle, #b2218b);
2810+
--cc-btn-primary-hover-color: #ffffff;
2811+
--cc-btn-primary-hover-border-color: var(--color-middle, #b2218b);
2812+
2813+
/* secondary button = recessed dark surface */
2814+
--cc-btn-secondary-bg: var(--bg-color-dark, #0d0f18);
2815+
--cc-btn-secondary-color: #ffffff;
2816+
--cc-btn-secondary-border-color: rgba(255, 255, 255, 0.16);
2817+
--cc-btn-secondary-hover-bg: var(--bg-color-code, #282330);
2818+
--cc-btn-secondary-hover-color: #ffffff;
2819+
--cc-btn-secondary-hover-border-color: rgba(255, 255, 255, 0.28);
2820+
2821+
/* toggles */
2822+
--cc-toggle-on-bg: var(--color-top, #ed168f);
2823+
--cc-toggle-off-bg: #565f78;
2824+
--cc-toggle-on-knob-bg: #ffffff;
2825+
--cc-toggle-off-knob-bg: #ffffff;
2826+
--cc-toggle-enabled-block-bg: var(--color-top, #ed168f);
2827+
--cc-toggle-readonly-bg: #39415a;
2828+
--cc-toggle-readonly-knob-bg: #c7ccd6;
2829+
--cc-toggle-readonly-knob-icon-color: var(--bg-color, #20293c);
2830+
2831+
/* preferences category blocks */
2832+
--cc-cookie-category-block-bg: var(--bg-color-dark, #0d0f18);
2833+
--cc-cookie-category-block-border: rgba(255, 255, 255, 0.08);
2834+
--cc-cookie-category-block-hover-bg: var(--bg-color-code, #282330);
2835+
--cc-cookie-category-block-hover-border: rgba(255, 255, 255, 0.16);
2836+
--cc-cookie-category-expanded-block-bg: var(--bg-color-code, #282330);
2837+
--cc-cookie-category-expanded-block-hover-bg: #333044;
2838+
2839+
/* misc */
2840+
--cc-overlay-bg: rgba(0, 0, 0, 0.65);
2841+
--cc-webkit-scrollbar-bg: #39415a;
2842+
--cc-webkit-scrollbar-bg-hover: #565f78;
2843+
--cc-footer-bg: var(--bg-color-dark, #0d0f18);
2844+
--cc-footer-color: #c7ccd6;
2845+
--cc-footer-border-color: rgba(255, 255, 255, 0.1);
2846+
}
2847+
2848+
/* Links inside the banner follow the brand color. */
2849+
html #cc-main a,
2850+
html #cc-main .cc__link {
2851+
color: var(--color-top, #ed168f);
28412852
}
28422853

28432854

docs-src/src/pages/privacy.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,20 @@ export default function Privacy() {
8787
Analyse- und Marketing-Dienste Cookies setzen. Cookies und Local-Storage-Einträge
8888
können Sie jederzeit über die Einstellungen Ihres Browsers löschen.
8989
</p>
90+
<p>
91+
Besuchern aus der EU/dem EWR wird beim ersten Aufruf ein Einwilligungsbanner
92+
angezeigt. Analyse- und Marketing-Dienste werden erst nach Ihrer Einwilligung
93+
geladen. Ihre Auswahl können Sie jederzeit ändern oder widerrufen:{' '}
94+
<a
95+
href="#"
96+
onClick={(e) => {
97+
e.preventDefault();
98+
(window as any).rxdbShowConsentPreferences?.();
99+
}}
100+
>
101+
Cookie-Einstellungen &ouml;ffnen
102+
</a>.
103+
</p>
90104

91105
<h3>4. Analyse und Reichweitenmessung</h3>
92106
<p>
@@ -270,6 +284,20 @@ export default function Privacy() {
270284
services listed below may set cookies. You can delete cookies and local storage
271285
entries at any time via your browser settings.
272286
</p>
287+
<p>
288+
Visitors from the EU/EEA are shown a consent banner on their first visit.
289+
Analytics and marketing services are only loaded after you consent. You can
290+
change or withdraw your choice at any time:{' '}
291+
<a
292+
href="#"
293+
onClick={(e) => {
294+
e.preventDefault();
295+
(window as any).rxdbShowConsentPreferences?.();
296+
}}
297+
>
298+
Open cookie settings
299+
</a>.
300+
</p>
273301

274302
<h3>4. Analytics and reach measurement</h3>
275303
<p>

0 commit comments

Comments
 (0)