From b5d913f3b4ef34b39aafd916db3733881c322302 Mon Sep 17 00:00:00 2001 From: Yndira-E Date: Tue, 26 May 2026 15:55:43 +0200 Subject: [PATCH 1/4] Clean up duplicated PostHog cleaning logic --- src/_includes/layouts/base.njk | 30 +++++------------------------- 1 file changed, 5 insertions(+), 25 deletions(-) diff --git a/src/_includes/layouts/base.njk b/src/_includes/layouts/base.njk index e63c08770c..8b1e2d295c 100644 --- a/src/_includes/layouts/base.njk +++ b/src/_includes/layouts/base.njk @@ -153,7 +153,7 @@ eleventyComputed: - {%- if not DEV_MODE -%} + {# {%- if not DEV_MODE -%} #} - {%- endif -%} + {# {%- endif -%} #} Skip to main content @@ -485,7 +485,7 @@ eleventyComputed: -{%- if not DEV_MODE -%} +{# {%- if not DEV_MODE -%} #} @@ -569,29 +569,9 @@ eleventyComputed: posthog.opt_out_capturing(); posthog.reset(); // Switch to memory mode to avoid any new persistence after withdrawal. + // Cookies are cleared by autoClear in cookieconsent-config.js. posthog.set_config({ persistence: 'memory' }); - // Purge PostHog browser persistence for GDPR-compliant withdrawal. - // This includes cross-subdomain cookies and opt-in/out markers. - var posthogCookiePattern = /^(ph_[^=\s]+_posthog|ph_phc_[^=\s]+_posthog|__ph_opt_in_out_[^=\s]+)$/; - var cookieNames = document.cookie.split(';').map(function (part) { - return part.split('=')[0].trim(); - }).filter(function (name) { - return posthogCookiePattern.test(name); - }); - - var host = window.location.hostname; - var hostParts = host.split('.'); - var rootDomain = hostParts.length > 2 ? hostParts.slice(1).join('.') : host; - var domains = ['', host, '.' + host, rootDomain, '.' + rootDomain]; - - cookieNames.forEach(function (name) { - domains.forEach(function (domain) { - var domainPart = domain ? '; domain=' + domain : ''; - document.cookie = name + '=; Max-Age=0; path=/' + domainPart + '; SameSite=Lax'; - }); - }); - var posthogStorageKeyPattern = /^(ph_[^\s]+_posthog|__ph_opt_in_out_[^\s]+)$/; try { for (var i = localStorage.length - 1; i >= 0; i--) { @@ -636,5 +616,5 @@ eleventyComputed: addHubSpotConsentListener(); }); -{%- endif -%} +{# {%- endif -%} #} From 6b91c73efd62848094fb9a3a76daa438932132f5 Mon Sep 17 00:00:00 2001 From: Yndira-E Date: Tue, 26 May 2026 16:07:04 +0200 Subject: [PATCH 2/4] clear LinkedIn ad cookies across all domain variants on consent withdrawal --- src/js/cookieconsent-config.js | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/js/cookieconsent-config.js b/src/js/cookieconsent-config.js index 4043c847ac..9c58da555d 100644 --- a/src/js/cookieconsent-config.js +++ b/src/js/cookieconsent-config.js @@ -91,6 +91,27 @@ function applyAnalyticsConsent (options) { } } +function clearLinkedInCookiesFallback () { + var linkedInCookiePattern = /^(_li_dcdm_c|_li_ss|li_sugr|lidc|bcookie|bscookie|UserMatchHistory)$/; + var cookieNames = document.cookie.split(';').map(function (part) { + return part.split('=')[0].trim(); + }).filter(function (name) { + return linkedInCookiePattern.test(name); + }); + + var host = window.location.hostname; + var hostParts = host.split('.'); + var rootDomain = hostParts.length > 2 ? hostParts.slice(1).join('.') : host; + var domains = ['', host, '.' + host, rootDomain, '.' + rootDomain]; + + cookieNames.forEach(function (name) { + domains.forEach(function (domain) { + var domainPart = domain ? '; domain=' + domain : ''; + document.cookie = name + '=; Max-Age=0; path=/' + domainPart + '; SameSite=Lax'; + }); + }); +} + function applyAdsConsent (options) { var accepted = options.accepted; var emitEvent = !!options.emitEvent; @@ -102,6 +123,10 @@ function applyAdsConsent (options) { 'personalization_storage': accepted ? 'granted' : 'denied' }); + if (!accepted) { + clearLinkedInCookiesFallback(); + } + if (emitEvent) { gtag('event', 'cookie_consent', { 'event_category': 'ads', From 2c4e5f00dd3197b2ab2ee55abc60f36ea7367996 Mon Sep 17 00:00:00 2001 From: Yndira-E Date: Tue, 26 May 2026 19:28:37 +0200 Subject: [PATCH 3/4] Create single source of truth for analytics scripts across Eleventy and Nuxt --- nuxt/server/plugins/analytics.ts | 17 +++ package.json | 3 +- src/_includes/analytics/body.html | 143 +++++++++++++++++++++ src/_includes/analytics/head.html | 61 +++++++++ src/_includes/hubspot.js | 44 ------- src/_includes/layouts/base.njk | 204 +----------------------------- 6 files changed, 229 insertions(+), 243 deletions(-) create mode 100644 nuxt/server/plugins/analytics.ts create mode 100644 src/_includes/analytics/body.html create mode 100644 src/_includes/analytics/head.html delete mode 100644 src/_includes/hubspot.js diff --git a/nuxt/server/plugins/analytics.ts b/nuxt/server/plugins/analytics.ts new file mode 100644 index 0000000000..5a200b74a3 --- /dev/null +++ b/nuxt/server/plugins/analytics.ts @@ -0,0 +1,17 @@ +import { readFileSync } from 'fs' +import { resolve } from 'path' + +const headHtml = readFileSync(resolve(process.cwd(), '../src/_includes/analytics/head.html'), 'utf-8') +const bodyHtml = readFileSync(resolve(process.cwd(), '../src/_includes/analytics/body.html'), 'utf-8') + .replace('{{ POSTHOG_APIKEY }}', process.env.POSTHOG_APIKEY || '') + +export default defineNitroPlugin((nitroApp) => { + if (process.env.NODE_ENV !== 'production') return + + nitroApp.hooks.hook('render:html', (html) => { + // Guard against double injection from dev mode plugin reloads + if (html.bodyAppend.some(s => s.includes('cc.min.js'))) return + html.head.push(headHtml) + html.bodyAppend.push(bodyHtml) + }) +}) diff --git a/package.json b/package.json index 5b70d9ec71..c4e8d50897 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "author": "Nick O'Leary", "workspaces": ["nuxt"], "scripts": { - "dev": "concurrently \"npm run dev:eleventy\" \"npm run dev:postcss\" \"npm run dev:postcss-nuxt\" \"npm run dev --workspace=nuxt\"", + "dev": "concurrently \"npm run dev:eleventy\" \"npm run dev:postcss\" \"npm run dev:postcss-nuxt\" \"npm run dev:nuxt\"", + "dev:nuxt": "dotenv -- npm run dev --workspace=nuxt", "start": "npm-run-all2 clean:dev build:js docs blueprints --parallel dev:*", "build:js": "terser -c -m -o _site/js/cc.min.js node_modules/vanilla-cookieconsent/dist/cookieconsent.umd.js src/js/cookieconsent-config.js && cp node_modules/@flowfuse/flow-renderer/index.min.js _site/js/flowrenderer.min.js", "build": "dotenv -v NODE_ENV=production -- npm-run-all2 clean build:js --parallel prod:*", diff --git a/src/_includes/analytics/body.html b/src/_includes/analytics/body.html new file mode 100644 index 0000000000..e90e273409 --- /dev/null +++ b/src/_includes/analytics/body.html @@ -0,0 +1,143 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/_includes/analytics/head.html b/src/_includes/analytics/head.html new file mode 100644 index 0000000000..2bd7eee56f --- /dev/null +++ b/src/_includes/analytics/head.html @@ -0,0 +1,61 @@ + + + + + + + diff --git a/src/_includes/hubspot.js b/src/_includes/hubspot.js deleted file mode 100644 index 48bd6c73ab..0000000000 --- a/src/_includes/hubspot.js +++ /dev/null @@ -1,44 +0,0 @@ -// Flag to load HubSpot chat widget when hs-scripts.com is ready. -// Set to true when the user accepts analytics cookies (cookieconsent-config.js) -// or when the user opens the chat widget before hs-scripts.com has finished loading. -window._ffLoadChat = false - -// Set HubSpot Chat to wait a bit so it does not block page load if user has never started a conversation -if ( - window.sessionStorage?.getItem("chatInProgress") === null && - window.location.hash !== "#hs-chat-open" -) { - window.hsConversationsSettings = { - loadImmediately: false, - } - - window.addEventListener("load", (event) => { - let loaded = false - function loadHubSpotChat(event) { - if (loaded) return - loaded = true - // Mark that user has interacted — if hs-scripts.com isn't loaded yet, - // hsConversationsOnReady will pick this up and load the widget when ready. - window._ffLoadChat = true - setTimeout(() => { - window.HubSpotConversations?.widget?.load() - }, 1) - } - - window.addEventListener("mousemove", loadHubSpotChat, { once: true }) - window.addEventListener("scroll", loadHubSpotChat, { once: true }) - }) -} - -window.hsConversationsOnReady = [ - () => { - // Load widget if user already interacted before hs-scripts.com finished loading, - // or if this is a non-privacy-region user with chat set to auto-load. - if (window._ffLoadChat) { - window.HubSpotConversations.widget.load() - } - window.HubSpotConversations.on("conversationStarted", () => { - window.sessionStorage?.setItem("chatInProgress", "true") - }) - }, -] diff --git a/src/_includes/layouts/base.njk b/src/_includes/layouts/base.njk index 8b1e2d295c..b10e72815e 100644 --- a/src/_includes/layouts/base.njk +++ b/src/_includes/layouts/base.njk @@ -153,72 +153,9 @@ eleventyComputed: - {# {%- if not DEV_MODE -%} #} - - - - - - - - {# {%- endif -%} #} + {%- if not DEV_MODE -%} + {% include "analytics/head.html" %} + {%- endif -%} Skip to main content @@ -485,136 +422,7 @@ eleventyComputed: -{# {%- if not DEV_MODE -%} #} - - - - - - - - - - -{% set hubspotJS %}{% include "hubspot.js" %}{% endset %} - - - - - - - - - - -{# {%- endif -%} #} +{%- if not DEV_MODE -%} +{% include "analytics/body.html" %} +{%- endif -%} From bfd003a13a8d99e632d5b3cc68a998c3caa56e95 Mon Sep 17 00:00:00 2001 From: Yndira-E Date: Fri, 29 May 2026 10:09:45 +0200 Subject: [PATCH 4/4] inline nuxt dev command into dev script --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index c4e8d50897..c5850269ad 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,7 @@ "author": "Nick O'Leary", "workspaces": ["nuxt"], "scripts": { - "dev": "concurrently \"npm run dev:eleventy\" \"npm run dev:postcss\" \"npm run dev:postcss-nuxt\" \"npm run dev:nuxt\"", - "dev:nuxt": "dotenv -- npm run dev --workspace=nuxt", + "dev": "concurrently \"npm run dev:eleventy\" \"npm run dev:postcss\" \"npm run dev:postcss-nuxt\" \"dotenv -- npm run dev --workspace=nuxt\"", "start": "npm-run-all2 clean:dev build:js docs blueprints --parallel dev:*", "build:js": "terser -c -m -o _site/js/cc.min.js node_modules/vanilla-cookieconsent/dist/cookieconsent.umd.js src/js/cookieconsent-config.js && cp node_modules/@flowfuse/flow-renderer/index.min.js _site/js/flowrenderer.min.js", "build": "dotenv -v NODE_ENV=production -- npm-run-all2 clean build:js --parallel prod:*",