diff --git a/_includes/consent_banner.html b/_includes/consent_banner.html new file mode 100644 index 0000000..afe4d21 --- /dev/null +++ b/_includes/consent_banner.html @@ -0,0 +1,17 @@ +
diff --git a/_includes/footer.html b/_includes/footer.html index 11625ba..ff1df46 100644 --- a/_includes/footer.html +++ b/_includes/footer.html @@ -6,10 +6,19 @@ + {% if site.google_analytics_id %} + + {% endif %} +{% if site.google_analytics_id %} +{% include consent_banner.html %} +{% endif %} + @@ -21,13 +30,12 @@ -{% if jekyll.environment == "production" and site.google_analytics_id %} - - +{% if site.google_analytics_id %} + {% endif %} diff --git a/assets/css/custom.css b/assets/css/custom.css index 138da50..b50f469 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -268,3 +268,48 @@ p { [data-theme="dark"] .highlight pre { background-color: transparent; } + +/* + * GDPR cookie/analytics consent banner. Hidden by default via the [hidden] + * attribute; consent.js shows it when no choice is recorded and an + * analytics ID is configured. + */ +.consent-banner { + position: fixed; + bottom: 1rem; + left: 1rem; + right: 1rem; + max-width: 32rem; + margin: 0 auto; + padding: 1rem 1.25rem; + border-radius: 6px; + background: var(--bulma-scheme-main-bis); + color: var(--bulma-text); + border: 1px solid var(--bulma-border); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + z-index: 1000; + font-family: "Open Sans", sans-serif; + font-size: 0.875rem; +} + +.consent-banner-title { + font-weight: 700; + margin: 0 0 0.5rem 0; +} + +.consent-banner-text { + margin: 0 0 0.75rem 0; + line-height: 1.4; +} + +.consent-banner-buttons { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + justify-content: flex-end; +} + +.consent-footer-link { + font-size: 0.75rem; + margin-top: 0.5rem !important; +} diff --git a/assets/js/consent.js b/assets/js/consent.js new file mode 100644 index 0000000..f30a186 --- /dev/null +++ b/assets/js/consent.js @@ -0,0 +1,114 @@ +--- +layout: null +--- +(function () { + var STORAGE_KEY = 'analytics-consent'; + var ACCEPTED = 'accepted'; + var DECLINED = 'declined'; + + function getStored() { + try { + return localStorage.getItem(STORAGE_KEY); + } catch (e) { + return null; + } + } + + function setStored(value) { + try { + localStorage.setItem(STORAGE_KEY, value); + } catch (e) {} + } + + function loadAnalytics() { + if (!window.GA_MEASUREMENT_ID) return; + if (document.getElementById('gtag-script')) return; + + if (window.ANALYTICS_DRY_RUN) { + console.log('[consent] dry run: would load GA4 with id ' + window.GA_MEASUREMENT_ID); + return; + } + + var script = document.createElement('script'); + script.async = true; + script.id = 'gtag-script'; + script.src = 'https://www.googletagmanager.com/gtag/js?id=' + encodeURIComponent(window.GA_MEASUREMENT_ID); + document.head.appendChild(script); + + window.dataLayer = window.dataLayer || []; + function gtag() { window.dataLayer.push(arguments); } + window.gtag = gtag; + gtag('js', new Date()); + gtag('config', window.GA_MEASUREMENT_ID); + } + + var previousFocus = null; + + function onKeydown(e) { + if (e.key === 'Escape' || e.keyCode === 27) { + e.preventDefault(); + hideBanner(); + } + } + + function showBanner() { + var banner = document.getElementById('consent-banner'); + if (!banner) return; + previousFocus = document.activeElement; + banner.hidden = false; + var declineBtn = banner.querySelector('.consent-decline'); + if (declineBtn && declineBtn.focus) declineBtn.focus(); + document.addEventListener('keydown', onKeydown); + } + + function hideBanner() { + var banner = document.getElementById('consent-banner'); + if (banner) banner.hidden = true; + document.removeEventListener('keydown', onKeydown); + if (previousFocus && previousFocus.focus) { + try { previousFocus.focus(); } catch (e) {} + } + previousFocus = null; + } + + function accept() { + setStored(ACCEPTED); + hideBanner(); + loadAnalytics(); + } + + function decline() { + setStored(DECLINED); + hideBanner(); + } + + function init() { + if (!window.GA_MEASUREMENT_ID) return; + + var current = getStored(); + if (current === ACCEPTED) { + loadAnalytics(); + } else if (current !== DECLINED) { + showBanner(); + } + + var acceptBtn = document.querySelector('#consent-banner .consent-accept'); + var declineBtn = document.querySelector('#consent-banner .consent-decline'); + if (acceptBtn) acceptBtn.addEventListener('click', accept); + if (declineBtn) declineBtn.addEventListener('click', decline); + + var prefLink = document.getElementById('cookie-preferences-link'); + if (prefLink) { + prefLink.addEventListener('click', function (e) { + e.preventDefault(); + showBanner(); + }); + } + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } +})();