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..c5850269ad 100644 --- a/package.json +++ b/package.json @@ -8,7 +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 --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:*", 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 e63c08770c..b10e72815e 100644 --- a/src/_includes/layouts/base.njk +++ b/src/_includes/layouts/base.njk @@ -154,70 +154,7 @@ eleventyComputed: {%- if not DEV_MODE -%} - - - - - - - + {% include "analytics/head.html" %} {%- endif -%}
@@ -486,155 +423,6 @@ eleventyComputed: {%- if not DEV_MODE -%} - - - - - - - - - - -{% set hubspotJS %}{% include "hubspot.js" %}{% endset %} - - - - - - - - - - +{% include "analytics/body.html" %} {%- endif -%} 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',