Skip to content

feat: add termly cookie consent banner with cross-subdomain sync#188

Merged
AlexKantor87 merged 3 commits intomainfrom
feat/cookie-consent-banner
Apr 30, 2026
Merged

feat: add termly cookie consent banner with cross-subdomain sync#188
AlexKantor87 merged 3 commits intomainfrom
feat/cookie-consent-banner

Conversation

@AlexKantor87
Copy link
Copy Markdown
Contributor

@AlexKantor87 AlexKantor87 commented Apr 30, 2026

Summary

  • Adds termly.js at the content root. Mintlify auto-includes any .js file there on every page, so this loads the Termly banner across all of docs.kosli.com.
  • Adds an integrations.cookies block in docs.json pointing at the kosli_consent localStorage flag — Mintlify gates its own telemetry on this, so their analytics doesn't fire pre-consent (per https://www.mintlify.com/docs/integrations/privacy/overview#cookie-consent-and-disabling-telemetry).
  • Termly embed uses the same data-website-uuid as www.kosli.com (single Termly property) plus data-master-consents-origin="https://www.kosli.com" to read consent from the parent.
  • Hidden iframe → https://www.kosli.com/consent-sync.html provides Termly's documented cross-subdomain consent sharing, so users who already consented on the marketing site aren't re-prompted on docs.
  • TERMLY_CUSTOM_BLOCKING_MAP includes "kosli.com": "essential" so Termly's auto-blocker doesn't sandbox the consent-sync iframe and break the handshake.
  • SPA-execution guard (window.__kosliTermlyLoaded) prevents duplicate iframes on Mintlify client-side route changes.
  • A termly.consent event listener mirrors Termly's consent state to the kosli_consent localStorage flag that Mintlify reads.

Why

Termly currently runs only on www.kosli.com. Visitors landing directly on docs.kosli.com are never prompted, which is a GDPR/ePrivacy gap. Mintlify doesn't expose <head> injection, so this uses the documented .js-in-content-root path plus the privacy integration.

Dependencies

Land in this order:

  1. https://github.com/kosli-dev/www.kosli.com/pull/1787 — relaxes frame-ancestors / X-Frame-Options on /consent-sync.html so this iframe isn't blocked.
  2. This PR.

Out-of-band setup (issue tracks): add docs.kosli.com to Termly Scan Settings, confirm cookie domain is .kosli.com.

Tracking issue: https://github.com/kosli-dev/server/issues/5551

Known limitation (documented in tracking issue)

Mintlify content-dir JS executes after hydration, so data-auto-block only catches scripts that fire after Termly mounts. Mintlify's own telemetry is now gated by integrations.cookies so that's covered. Anything we embed in MDX that fires earlier (YouTube, Loom, etc.) is the residual risk — worth a network-tab audit before merge.

Test plan

  • Mintlify deploy preview builds without docs.json schema errors.
  • Visit a deploy-preview page: Termly banner appears.
  • Hidden iframe to https://www.kosli.com/consent-sync.html loads without CSP / X-Frame-Options errors in browser console (requires PR 1787 deployed first).
  • Accept cookies on the docs preview → localStorage.kosli_consent === "accepted".
  • Reload docs preview after consenting on www.kosli.com → banner does not re-appear (cross-subdomain sync working).
  • Network tab: confirm Mintlify telemetry endpoints are not called before consent.
  • Navigate between pages: only one Termly script tag and one consent-sync iframe in the DOM (SPA guard working).

🤖 Generated with Claude Code

closes #86

Adds termly.js at the content root (Mintlify auto-includes any .js file
on every page) and an integrations.cookies block in docs.json so
Mintlify's own telemetry is gated on the kosli_consent localStorage flag
that termly.js sets when the user accepts.

The script also embeds a hidden iframe pointing at
https://www.kosli.com/consent-sync.html so a user who already consented
on the marketing site is not re-prompted on docs. Requires the companion
www.kosli.com PR to relax frame-ancestors / X-Frame-Options on that path.

Refs kosli-dev/server#5551

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@AlexKantor87 AlexKantor87 requested a review from a team as a code owner April 30, 2026 08:42
@mintlify
Copy link
Copy Markdown
Contributor

mintlify Bot commented Apr 30, 2026

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
kosli 🟢 Ready View Preview Apr 30, 2026, 8:42 AM

@dangrondahl
Copy link
Copy Markdown
Contributor

Code review

Found 1 issue:

  1. The event listener on line 39 listens for "termly.consent" (dot separator), but Termly's documented consent event name is "termly:consent" (colon separator). Browser custom event names must match exactly. If the event name is wrong, the listener will never fire, kosli_consent will never be set in localStorage, and Mintlify telemetry will remain permanently blocked regardless of the user's consent choice. Worth verifying in a browser that localStorage["kosli_consent"] is actually set after accepting cookies.

docs/termly.js

Lines 38 to 46 in c480f64

}
window.addEventListener("termly.consent", function (e) {
const analytics = e && e.detail && e.detail.analytics;
if (analytics) {
localStorage.setItem("kosli_consent", "accepted");
} else {
localStorage.removeItem("kosli_consent");
}

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

The previous code used window.addEventListener("termly.consent", ...)
which is not part of Termly's API and never fires. Switched to the
documented Termly.on("consent", callback) pattern with an onload
handler, and updated the embed script from the deprecated embed.min.js
(2021) to the current resource-blocker URL (2023).
@dangrondahl
Copy link
Copy Markdown
Contributor

Fix: use current Termly Event API (01dffa9)

The original code used window.addEventListener("termly.consent", ...) which is not part of Termly's API — the event never fires, so kosli_consent was never set in localStorage.

Changes:

  • Updated embed script from deprecated embed.min.js (2021) to current resource-blocker/UUID?autoBlock=on
  • Replaced the non-functional event listener with Termly's documented Termly.on("consent", callback) pattern via an onload handler
  • Fixed consent data check from e.detail.analytics to data.categories.includes("analytics")

Tested on preview environment:

  1. Loaded docs preview — Termly banner appears, no more "outdated CMP script" console warning
  2. Before accepting: localStorage.getItem("kosli_consent") returns null
  3. After accepting cookies: localStorage.getItem("kosli_consent") returns "accepted"

Ref: Termly Event API docs, Embed script versions

@dangrondahl
Copy link
Copy Markdown
Contributor

dangrondahl commented Apr 30, 2026

Remaining console warnings after fix

Two warnings remain in the preview environment:

  1. "Termly ResourceBlocker is not the first script on the page" — Mintlify injects its own scripts before termly.js loads, so Termly's auto-blocking may miss scripts that run earlier. This is a Termly best-practice warning, not an error. Not actionable on a Mintlify-hosted site since we don't control script injection order.

  2. "frame-ancestors 'self'" / "Unsafe attempt to load consent-sync.html" — Expected until the companion PR (https://github.com/kosli-dev/www.kosli.com/pull/1787) is merged and deployed. That PR relaxes X-Frame-Options on /consent-sync.html to allow cross-subdomain framing from docs.kosli.com.

Neither warning affects the core consent flow — localStorage.kosli_consent is correctly set after accepting cookies.

Screenshot 2026-04-30 at 11 41 29

unpkg.com and youtube.com are not used on the docs site — they were
carried over from the www.kosli.com config.
@dangrondahl dangrondahl mentioned this pull request Apr 30, 2026
1 task
@AlexKantor87 AlexKantor87 merged commit 66dcf74 into main Apr 30, 2026
4 checks passed
@AlexKantor87 AlexKantor87 deleted the feat/cookie-consent-banner branch April 30, 2026 13:34
dangrondahl added a commit that referenced this pull request Apr 30, 2026
## Summary

- Adds MixPanel integration to `docs.json` with project token for docs
analytics tracking

## Blocked

This PR should not be merged until #188 has been merged. The Termly
cookie consent banner must be in place before enabling MixPanel
analytics, so that user consent is collected before any tracking fires.

## Test plan

- [ ] Verify MixPanel events appear in the MixPanel dashboard after
merging
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: add cookie consent to docs site

3 participants