diff --git a/_config.yml b/_config.yml index 259ae0534f86a..83b8b58c8959d 100644 --- a/_config.yml +++ b/_config.yml @@ -403,6 +403,7 @@ enable_pirsch_analytics: false # enables Pirsch analytics (https://pirsch.io/) enable_openpanel_analytics: false # enables Openpanel analytics (https://openpanel.dev/) enable_google_verification: false # enables google site verification enable_bing_verification: false # enables bing site verification +enable_cookie_consent: false # enables GDPR-compliant cookie consent dialog (https://github.com/orestbida/cookieconsent) enable_masonry: true # enables automatic project cards arrangement enable_math: true # enables math typesetting (uses MathJax) enable_tooltips: false # enables automatic tooltip links generated for each section titles on pages and posts @@ -609,6 +610,14 @@ third_party_libraries: url: js: "https://cdn.jsdelivr.net/npm/swiper@11.1.0/swiper-element-bundle.min.js.map" version: "11.0.5" + vanilla-cookieconsent: + integrity: + css: "sha256-ygRrixsQlBByBZiOcJamh7JByO9fP+/l5UPtKNJmRsE=" + js: "sha256-vG4vLmOB/AJbJ6awr7Wg4fxonG+fxAp4cIrbIFTvRXU=" + url: + css: "https://cdn.jsdelivr.net/npm/vanilla-cookieconsent@{{version}}/dist/cookieconsent.css" + js: "https://cdn.jsdelivr.net/npm/vanilla-cookieconsent@{{version}}/dist/cookieconsent.umd.js" + version: "3.1.0" vega: integrity: js: "sha256-Yot/cfgMMMpFwkp/5azR20Tfkt24PFqQ6IQS+80HIZs=" diff --git a/_includes/distill_scripts.liquid b/_includes/distill_scripts.liquid index 44d1d5e56f96f..24d80d2f18a6d 100644 --- a/_includes/distill_scripts.liquid +++ b/_includes/distill_scripts.liquid @@ -189,20 +189,58 @@ {% endif %} +{% if site.enable_cookie_consent %} + + + +{% endif %} + {% if site.enable_google_analytics %} - - + + {% endif %} {% if site.enable_cronitor_analytics %} - - + + {% endif %} {% if site.enable_pirsch_analytics %} {% endif %} {% if site.enable_openpanel_analytics %} - - + + {% endif %} {% if site.enable_progressbar %} diff --git a/_includes/head.liquid b/_includes/head.liquid index 2d53a28675097..d79bea9ee3d6a 100644 --- a/_includes/head.liquid +++ b/_includes/head.liquid @@ -195,3 +195,14 @@ {% if page.tikzjax %} {% endif %} + +{% if site.enable_cookie_consent %} + + +{% endif %} diff --git a/_includes/scripts.liquid b/_includes/scripts.liquid index be5b0ffa17dee..e6b21ae57437b 100644 --- a/_includes/scripts.liquid +++ b/_includes/scripts.liquid @@ -223,20 +223,58 @@ {% endunless %} {% endif %} +{% if site.enable_cookie_consent %} + + + +{% endif %} + {% if site.enable_google_analytics %} - - + + {% endif %} {% if site.enable_cronitor_analytics %} - - + + {% endif %} {% if site.enable_pirsch_analytics %} {% endif %} {% if site.enable_openpanel_analytics %} - - + + {% endif %} {% if site.enable_progressbar %} diff --git a/_scripts/cookie-consent-setup.js b/_scripts/cookie-consent-setup.js new file mode 100644 index 0000000000000..37f45fd29abed --- /dev/null +++ b/_scripts/cookie-consent-setup.js @@ -0,0 +1,145 @@ +--- +permalink: /assets/js/cookie-consent-setup.js +--- +/** + * Cookie Consent Configuration + * Documentation: https://cookieconsent.orestbida.com/ + * + * GDPR-Compliant Approach: + * - Analytics scripts use type="text/plain" data-category="analytics" + * - The library blocks all marked scripts until user consents + * - Scripts NEVER run until explicit consent is given + * - Google Consent Mode is used for Google Analytics privacy mode before consent + * - Other analytics (Cronitor, Pirsch, OpenPanel) are blocked until consent given + * + * Supported Analytics Providers: + * - Cronitor RUM + * - Google Analytics (GA4) + * - OpenPanel Analytics + * - Pirsch Analytics + */ + +// Initialize Google Consent Mode BEFORE any tracking +// This tells Google services to operate in privacy mode until user consents +window.dataLayer = window.dataLayer || []; +function gtag() { + window.dataLayer.push(arguments); +} +gtag('consent', 'default', { + 'ad_storage': 'denied', + 'analytics_storage': 'denied', + 'functionality_storage': 'denied', + 'personalization_storage': 'denied' +}); + +// Wait for the library to be available +function initializeCookieConsent() { + // Check if CookieConsent is available + if (!window.CookieConsent) { + // Library not yet loaded, try again after a short delay + setTimeout(initializeCookieConsent, 100); + return; + } + + window.CookieConsent.run({ + categories: { + necessary: { + enabled: true, + readOnly: true + }, + analytics: {} + }, + + language: { + default: 'en', + translations: { + en: { + consentModal: { + title: 'We use cookies', + description: 'This website uses cookies to improve your experience and analyze site traffic. By clicking "Accept all", you consent to our use of cookies.', + acceptAllBtn: 'Accept all', + acceptNecessaryBtn: 'Reject all', + showPreferencesBtn: 'Manage Individual preferences' + }, + preferencesModal: { + title: 'Manage cookie preferences', + acceptAllBtn: 'Accept all', + acceptNecessaryBtn: 'Reject all', + savePreferencesBtn: 'Accept current selection', + closeIconLabel: 'Close modal', + sections: [ + { + title: 'Cookie usage', + description: 'We use cookies to ensure the basic functionalities of the website and to enhance your online experience. You can choose for each category to opt-in/out whenever you want.' + }, + { + title: 'Strictly Necessary cookies', + description: 'These cookies are essential for the proper functioning of the website. Without these cookies, the website would not work properly.', + linkedCategory: 'necessary' + }, + { + title: 'Analytics cookies', + description: 'These cookies allow us to measure traffic and analyze your behavior to improve our service.', + linkedCategory: 'analytics' + }, + { + title: 'More information', + description: 'For any queries in relation to our policy on cookies and your choices, please contact us.' + } + ] + } + } + } + }, + + // Callback when user accepts/rejects consent + onFirstConsent: function(consentData) { + updateConsentMode(consentData); + }, + + // Callback when user changes preferences + onChange: function(consentData) { + updateConsentMode(consentData); + } + }); + + /** + * Update Google Consent Mode based on user preferences + * This ensures Google services respect user choices + */ + function updateConsentMode(consentData) { + // Handle both callback data structures + var categories = consentData.categories || consentData; + + // Ensure categories is an object + if (!categories || typeof categories !== 'object') { + console.warn('Invalid consent data structure:', consentData); + return; + } + + gtag('consent', 'update', { + 'analytics_storage': categories.analytics ? 'granted' : 'denied', + 'ad_storage': 'denied', + 'functionality_storage': 'denied', + 'personalization_storage': 'denied' + }); + + if (categories.analytics) { + console.debug('✓ Analytics consent granted - tracking enabled for all providers'); + // Analytics scripts with data-category="analytics" will automatically run + // when the library re-evaluates them after this consent update + } else { + console.debug('✗ Analytics consent denied - no tracking data collected'); + // Analytics scripts are already blocked by the library (type="text/plain") + // No tracking will occur for: + // - Cronitor RUM + // - Google Analytics (GA4) + // - OpenPanel Analytics + // - Pirsch Analytics + } + } +} + +// Initialize when the library is available +initializeCookieConsent(); + diff --git a/assets/js/theme.js b/assets/js/theme.js index 0ac6983727242..1033906d5e3db 100644 --- a/assets/js/theme.js +++ b/assets/js/theme.js @@ -29,6 +29,7 @@ let applyTheme = () => { setHighlight(theme); setGiscusTheme(theme); setSearchTheme(theme); + setCookieConsentTheme(theme); updateCalendarUrl(); // if mermaid is not defined, do nothing @@ -245,6 +246,18 @@ let setSearchTheme = (theme) => { } }; +let setCookieConsentTheme = (theme) => { + // Sync cookie consent modal with site's theme + // The cookie consent library supports dark mode via the cc--darkmode class + var htmlElement = document.documentElement; + + if (theme === "dark") { + htmlElement.classList.add("cc--darkmode"); + } else { + htmlElement.classList.remove("cc--darkmode"); + } +}; + let transTheme = () => { document.documentElement.classList.add("transition"); window.setTimeout(() => {