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(() => {