From c480f6479286aeca409d5e76c0dd759a08a2364d Mon Sep 17 00:00:00 2001 From: Alex Kantor Date: Thu, 30 Apr 2026 09:41:27 +0100 Subject: [PATCH 1/3] feat: add termly cookie consent banner with cross-subdomain sync 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) --- docs.json | 6 ++++++ termly.js | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 termly.js diff --git a/docs.json b/docs.json index 3cb76e6..99ef6d7 100644 --- a/docs.json +++ b/docs.json @@ -36,6 +36,12 @@ "metadata": { "timestamp": true }, + "integrations": { + "cookies": { + "key": "kosli_consent", + "value": "accepted" + } + }, "favicon": "/favicon-32x32.png", "redirects": { "$ref": "./config/redirects.json" diff --git a/termly.js b/termly.js new file mode 100644 index 0000000..c694124 --- /dev/null +++ b/termly.js @@ -0,0 +1,54 @@ +// Termly cookie consent banner + cross-subdomain consent sync from www.kosli.com. +// Mintlify auto-includes any .js file at the content root on every page, +// so this loads on every doc page on docs.kosli.com. +// +// Companion config: docs.json `integrations.cookies` gates Mintlify's own +// telemetry on the `kosli_consent` localStorage flag we set below. + +(function () { + if (window.__kosliTermlyLoaded) return; + window.__kosliTermlyLoaded = true; + + window.TERMLY_CUSTOM_BLOCKING_MAP = { + "kosli.com": "essential", + "unpkg.com": "essential", + "youtube.com": "essential" + }; + + function init() { + if (!document.getElementById("kosli-termly-embed")) { + const s = document.createElement("script"); + s.id = "kosli-termly-embed"; + s.src = "https://app.termly.io/embed.min.js"; + s.setAttribute("data-auto-block", "on"); + s.setAttribute("data-website-uuid", "c98bfcd6-2f30-4f3c-b53c-d6dbd9b8c40c"); + s.setAttribute("data-master-consents-origin", "https://www.kosli.com"); + document.head.appendChild(s); + } + + if (!document.getElementById("kosli-consent-sync")) { + const f = document.createElement("iframe"); + f.id = "kosli-consent-sync"; + f.src = "https://www.kosli.com/consent-sync.html"; + f.title = "consent sync"; + f.setAttribute("aria-hidden", "true"); + f.style.display = "none"; + (document.body || document.documentElement).appendChild(f); + } + } + + 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"); + } + }); + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", init, { once: true }); + } else { + init(); + } +})(); From 01dffa967d6e4a47556833f937484ddc91bd0776 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dan=20Gr=C3=B8ndahl?= Date: Thu, 30 Apr 2026 11:37:44 +0200 Subject: [PATCH 2/3] fix: use current Termly Event API for consent listener 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). --- termly.js | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/termly.js b/termly.js index c694124..0709a5d 100644 --- a/termly.js +++ b/termly.js @@ -15,14 +15,28 @@ "youtube.com": "essential" }; + function syncConsent(data) { + if (data.categories && data.categories.includes("analytics")) { + localStorage.setItem("kosli_consent", "accepted"); + } else { + localStorage.removeItem("kosli_consent"); + } + } + + // Called when the Termly script finishes loading. + window.onTermlyLoaded = function () { + if (window.Termly && window.Termly.on) { + Termly.on("consent", syncConsent); + } + }; + function init() { if (!document.getElementById("kosli-termly-embed")) { const s = document.createElement("script"); s.id = "kosli-termly-embed"; - s.src = "https://app.termly.io/embed.min.js"; - s.setAttribute("data-auto-block", "on"); - s.setAttribute("data-website-uuid", "c98bfcd6-2f30-4f3c-b53c-d6dbd9b8c40c"); + s.src = "https://app.termly.io/resource-blocker/c98bfcd6-2f30-4f3c-b53c-d6dbd9b8c40c?autoBlock=on"; s.setAttribute("data-master-consents-origin", "https://www.kosli.com"); + s.setAttribute("onload", "onTermlyLoaded()"); document.head.appendChild(s); } @@ -37,15 +51,6 @@ } } - 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"); - } - }); - if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init, { once: true }); } else { From 35c4b84cc83a88c559df2b9bb96dc652b6f3045a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dan=20Gr=C3=B8ndahl?= Date: Thu, 30 Apr 2026 11:56:48 +0200 Subject: [PATCH 3/3] fix: remove unused domains from Termly blocking map MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit unpkg.com and youtube.com are not used on the docs site — they were carried over from the www.kosli.com config. --- termly.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/termly.js b/termly.js index 0709a5d..61892b9 100644 --- a/termly.js +++ b/termly.js @@ -10,9 +10,7 @@ window.__kosliTermlyLoaded = true; window.TERMLY_CUSTOM_BLOCKING_MAP = { - "kosli.com": "essential", - "unpkg.com": "essential", - "youtube.com": "essential" + "kosli.com": "essential" }; function syncConsent(data) {