Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions website/docusaurus.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -326,15 +326,16 @@ const config = {
},
},
],
process.env.POSTHOG_API_KEY && [
[
"posthog-docusaurus",
{
apiKey: process.env.POSTHOG_API_KEY,
appUrl: 'https://compose.diamonds/54Q17895d65',
uiHost: 'https://us.posthog.com',
enableInDevelopment: false,
capturePageLeave: true,
cookieless_mode: 'always',
defaults: '2026-01-30',
cookieless_mode: 'on_reject',
},
],
].filter(Boolean),
Expand Down
120 changes: 120 additions & 0 deletions website/src/components/consent/CookieConsentBanner.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/**
* Consent gate for PostHog when using cookieless_mode: "on_reject".
* Accept enables cookies, session replay, and full SDK features; decline keeps cookieless counting.
*
* @see https://posthog.com/docs/tutorials/cookieless-tracking
* @see https://posthog.com/tutorials/react-cookie-banner
*
* If consent stays pending (user ignores the banner), PostHog captures nothing until
* they choose. After AUTO_DECLINE_MS we call opt_out_capturing() so the banner hides
* and cookieless visit counting can run—same as clicking Decline.
*/
import React, {useEffect, useState} from 'react';
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
import styles from './styles.module.css';

const POLL_MS = 50;
const POLL_MAX = 200;

/** Silence after this long is treated as decline (cookieless only). */
const AUTO_DECLINE_MS = 30_000;

export default function CookieConsentBanner() {
const [consent, setConsent] = useState(
/** @type {'unknown' | 'pending' | 'granted' | 'denied' | 'skip'} */ ('unknown'),
);

useEffect(() => {
if (!ExecutionEnvironment.canUseDOM) return undefined;

let cancelled = false;
let tries = 0;
const id = window.setInterval(() => {
tries += 1;
const ph = window.posthog;

if (cancelled) {
window.clearInterval(id);
return;
}

if (!ph) {
if (tries >= POLL_MAX) {
window.clearInterval(id);
setConsent('skip');
}
return;
}

if (typeof ph.get_explicit_consent_status !== 'function') {
if (tries >= POLL_MAX) {
window.clearInterval(id);
setConsent('skip');
}
return;
}

window.clearInterval(id);
setConsent(ph.get_explicit_consent_status());
}, POLL_MS);

return () => {
cancelled = true;
window.clearInterval(id);
};
}, []);

useEffect(() => {
if (!ExecutionEnvironment.canUseDOM) return undefined;
if (consent !== 'pending') return undefined;

const id = window.setTimeout(() => {
window.posthog?.opt_out_capturing?.();
setConsent('denied');
}, AUTO_DECLINE_MS);

return () => window.clearTimeout(id);
}, [consent]);

if (consent !== 'pending') {
return null;
}

const handleAccept = () => {
window.posthog?.opt_in_capturing?.();
setConsent('granted');
};

const handleDecline = () => {
window.posthog?.opt_out_capturing?.();
setConsent('denied');
};

return (
<div
className={styles.banner}
role="region"
aria-label="Analytics and cookie consent"
aria-describedby="cookie-consent-desc">
<div className={styles.inner}>
<p className={styles.text} id="cookie-consent-desc">
We <b>only</b> use cookies to <b>measure & improve</b> our documentation experience.
</p>
<div className={styles.actions}>
<button
type="button"
className={styles.btnSecondary}
onClick={handleDecline}>
Decline
</button>
<button
type="button"
className={styles.btnPrimary}
onClick={handleAccept}>
Accept
</button>
</div>
</div>
</div>
);
}
104 changes: 104 additions & 0 deletions website/src/components/consent/styles.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
.banner {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 400;
padding: 0.75rem 1rem calc(0.75rem + env(safe-area-inset-bottom, 0));
display: flex;
justify-content: center;
pointer-events: none;
animation: cookieBannerIn 0.45s cubic-bezier(0.22, 1, 0.36, 1) both;
}

@keyframes cookieBannerIn {
from {
opacity: 0;
transform: translateY(100%);
}
to {
opacity: 1;
transform: translateY(0);
}
}

.inner {
pointer-events: auto;
max-width: 42rem;
width: 100%;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 0.75rem 1rem;
padding: 0.85rem 1.1rem;
border-radius: 12px;
background: var(--ifm-background-surface-color);
color: var(--ifm-font-color-base);
border: 1px solid var(--ifm-color-emphasis-200);
box-shadow:
0 -4px 24px rgba(0, 0, 0, 0.08),
0 -1px 0 rgba(0, 0, 0, 0.04);
}

html[data-theme='dark'] .inner {
box-shadow:
0 -4px 28px rgba(0, 0, 0, 0.35),
0 -1px 0 rgba(255, 255, 255, 0.06);
}

.text {
margin: 0;
flex: 1 1 12rem;
font-size: 0.875rem;
line-height: 1.45;
color: var(--ifm-color-emphasis-800);
}

html[data-theme='dark'] .text {
color: var(--ifm-color-emphasis-700);
}

.actions {
display: flex;
flex-shrink: 0;
gap: 0.5rem;
}

.btnPrimary,
.btnSecondary {
font-family: var(--ifm-font-family-base);
font-size: 0.8125rem;
font-weight: 600;
padding: 0.45rem 0.9rem;
border-radius: 8px;
cursor: pointer;
border: 1px solid transparent;
transition:
background 0.15s ease,
border-color 0.15s ease,
color 0.15s ease;
}

.btnPrimary {
background: var(--ifm-color-primary);
color: var(--ifm-font-color-base-inverse);
}

.btnPrimary:hover {
filter: brightness(1.05);
}

.btnSecondary {
background: transparent;
color: var(--ifm-font-color-base);
border: none;
}

.btnSecondary:hover {
background: var(--ifm-color-emphasis-100);
}

html[data-theme='dark'] .btnSecondary:hover {
background: var(--ifm-color-emphasis-200);
}
3 changes: 3 additions & 0 deletions website/src/theme/Root.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import React from 'react';
import { Toaster } from 'react-hot-toast';
import NavbarEnhancements from '@site/src/components/navigation/NavbarEnhancements';
import CookieConsentBanner from '@site/src/components/consent/CookieConsentBanner';

export default function Root({children}) {
return (
<>
Expand Down Expand Up @@ -77,6 +79,7 @@ export default function Root({children}) {
},
}}
/>
<CookieConsentBanner />
{children}
</>
);
Expand Down
Loading