From cb07db14dc063e469f9042dc64c404abcc9bbf8e Mon Sep 17 00:00:00 2001 From: Daniel Kraus Date: Sun, 14 Jun 2026 11:14:34 +0200 Subject: [PATCH] First draft. --- README.md | 84 +++++++++++++++++++++++- assets/js/cookie-consent.js | 51 ++++++++++++++ assets/js/tracker-code.js | 24 +++++++ assets/scss/cookie-consent.scss | 39 +++++++++++ config/_default/params.yaml | 5 ++ go.mod | 0 i18n/de.yaml | 18 +++++ i18n/en.yaml | 18 +++++ layouts/_partials/cookie-consent.html | 19 ++++++ layouts/_shortcodes/cookie-settings.html | 12 ++++ 10 files changed, 269 insertions(+), 1 deletion(-) create mode 100644 assets/js/cookie-consent.js create mode 100644 assets/js/tracker-code.js create mode 100644 assets/scss/cookie-consent.scss create mode 100644 config/_default/params.yaml create mode 100644 go.mod create mode 100644 i18n/de.yaml create mode 100644 i18n/en.yaml create mode 100644 layouts/_partials/cookie-consent.html create mode 100644 layouts/_shortcodes/cookie-settings.html diff --git a/README.md b/README.md index 9451887..10bdba6 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,85 @@ # hugo-cookie-consent -Hugo module that implements a cookie-consent message and functionality to change the cookie-consent decision. \ No newline at end of file +**Hugo module that implements a cookie-consent message and functionality to change +the cookie-consent decision.** + +The purpose of this module is to help make a site [GDPR][] compliant. It can +optionally insert tracking code that calls a [Matomo][] instance. This will only +happen after visitors have accepted the use of a cookie -- no logging will take +place before visitors have interacted with the cookie information message. Their +decision is stored in the browser's Local Storage, and it is _not_ personal data +as defined by the GDPR. + +This Hugo Module requires Hugo [v0.146.0][]+ which uses the new layout directory +structure with `_partials` and `_shortcode` directories with leading underscores. + +[Anthropic Claude][] helped in creating this module (paid subscription). + +## Installation in your Hugo site + +1. Turn the Hugo site into a Hugo module: + + ```bash + hugo mod init + ``` + + Where `` may be the path to the site's Git repository or anything + else; see [Hugo's docs][mod-doc]. + +2. Add the hugo-cookie-consent module by adding this `module.imports.path` to your + site configuration file: + + ```yaml + # config/_default/hugo.yaml + module: + imports: + - path: git.bovender.de/daniel/hugo-cookie-consent + ``` + +3. Add configuration keys (see [below](#configuration)). + +4. If your site's language is neither English nor German, add a few terms in the + desired language to the language tables. + +5. Make your pages load the JavaScript code, e. g. by adding this to a `script.html` + partial: + + ```html + {{- $js := resources.Get "js/cookie-consent.js" | minify -}} + + {{- $js := resources.Get "js/tracker-code.js" | minify -}} + + ``` + +6. Optionally, insert a `cookie-settings` partial somewhere on your site to enable + visitors to review and revise their choice. The privacy statement page might + be a good place for this. + + ```md + {{< cookie-settings >}} + ``` + +## Configuration + +You can configure the module by defining site parameters in your +`hugo.yaml` or `hugo.toml` configuration file. The following +snippet shows the default values. + +```yaml +# config/_default/params.yaml +hugo_cookie_consent: + privacy_policy_url: + enable_matomo: false + matomo_host: + matomo_site_id: +``` + +| Parameter | Description | +|-----------|-------------| +| `privacy_policy_url` | URL of the privacy policy page + +[anthropic claude]: https://claude.ai +[gdpr]: https://en.wikipedia.org/wiki/GDPR +[matomo]: https://matomo.org +[mod-doc]: https://gohugo.io/hugo-modules/use-modules/#import +[v0.146.0]: https://github.com/gohugoio/hugo/releases/tag/v0.146.0 \ No newline at end of file diff --git a/assets/js/cookie-consent.js b/assets/js/cookie-consent.js new file mode 100644 index 0000000..fd06fe4 --- /dev/null +++ b/assets/js/cookie-consent.js @@ -0,0 +1,51 @@ +(function () { + var KEY = 'cookie_consent'; + var banner = document.getElementById('cookie-banner'); + if (!banner) return; + updateStatus(); + + var consent = localStorage.getItem(KEY); + if (!consent) banner.style.display = 'flex'; + + function dismiss() { + banner.classList.add('fade-out'); + setTimeout(function () { banner.style.display = 'none'; }, 500); + } + + function updateStatus() { + var el = document.getElementById('cookie-status'); + if (!el) return; + var consent = localStorage.getItem(KEY); + el.textContent = consent === 'accepted' ? el.dataset.accepted + : consent === 'declined' ? el.dataset.declined + : el.dataset.none; + } + + document.getElementById('cookie-accept').onclick = function () { + localStorage.setItem(KEY, 'accepted'); + updateStatus(); + dismiss(); + window.dispatchEvent(new Event('cookieAccepted')); + }; + + document.getElementById('cookie-decline').onclick = function () { + localStorage.setItem(KEY, 'declined'); + updateStatus(); + dismiss(); + }; + + window.reopenCookieBanner = function () { + localStorage.removeItem(KEY); + updateStatus(); + banner.classList.remove('fade-out'); + banner.style.display = 'flex'; + }; + + window.addEventListener('cookieAccepted', function () { + if (typeof window.trackVisit === 'function') window.trackVisit(); + }); + + if (localStorage.getItem(KEY) === 'accepted') { + if (typeof window.trackVisit === 'function') window.trackVisit(); + } +})(); \ No newline at end of file diff --git a/assets/js/tracker-code.js b/assets/js/tracker-code.js new file mode 100644 index 0000000..a9086c8 --- /dev/null +++ b/assets/js/tracker-code.js @@ -0,0 +1,24 @@ +(function () { + function trackVisit() { + var banner = document.getElementById('cookie_banner'); + if (!banner) return; + var matomoUrl = banner.dataset.matomoUrl; + var siteId = banner.dataset.matomoSiteId; + if (!matomoUrl || !siteId) return; + + var _paq = window._paq = window._paq || []; + _paq.push(['trackPageView']); + _paq.push(['enableLinkTracking']); + _paq.push(['setTrackerUrl', matomoUrl + 'matomo.php']); + _paq.push(['setSiteId', siteId]); + var d = document, g = d.createElement('script'), + s = d.getElementsByTagName('script')[0]; + g.async = true; g.src = matomoUrl + 'matomo.js'; + s.parentNode.insertBefore(g, s); + } + + window.addEventListener('cookieAccepted', trackVisit); + if (localStorage.getItem('cookie_consent') === 'accepted') { + trackVisit(); + } +})(); \ No newline at end of file diff --git a/assets/scss/cookie-consent.scss b/assets/scss/cookie-consent.scss new file mode 100644 index 0000000..14795b8 --- /dev/null +++ b/assets/scss/cookie-consent.scss @@ -0,0 +1,39 @@ +#cookie-banner { + position: fixed; + top: 2.5rem; + left: 50%; + transform: translateX(-50%); + width: min(480px, calc(100vw - 2rem)); + background: #fff; + border: 1px solid #e2e2e2; + border-radius: 12px; + padding: 1rem 1.25rem; + box-shadow: 0 4px 24px rgba(0,0,0,.08); + display: flex; + align-items: center; + gap: 1rem; + z-index: 9999; + transition: opacity 0.5s ease; +} + +#cookie-banner.fade-out { + opacity: 0; + pointer-events: none; +} + +#cookie-banner p { margin: 0; font-size: 14px; flex: 1; } + +#cookie-banner button { + margin: 0.1rem 0; + padding: .45rem 1rem; + border-radius: 8px; + border: 1px solid #ccc; + cursor: pointer; + font-size: 13px; + white-space: nowrap; +} + +#cookie-accept { + background: #0b913a; color: #fff; border-color: #1a1a1a; +} + diff --git a/config/_default/params.yaml b/config/_default/params.yaml new file mode 100644 index 0000000..5977a03 --- /dev/null +++ b/config/_default/params.yaml @@ -0,0 +1,5 @@ +hugo_cookie_consent: + privacy_policy_url: + enable_matomo: false + matomo_host: + matomo_site_id: diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e69de29 diff --git a/i18n/de.yaml b/i18n/de.yaml new file mode 100644 index 0000000..3b83b1b --- /dev/null +++ b/i18n/de.yaml @@ -0,0 +1,18 @@ +site_wants_cookie: + other: Diese Website möchte ein Cookie speichern. +cookie_more_info: + other: Mehr darüber. +cookie_button_accept: + other: Akzeptieren +cookie_button_decline: + other: Ablehnen +cookie_status: + other: Aktueller Status +cookie_accepted: + other: Akzeptiert +cookie_declined: + other: Abgelehnt +cookie_none: + other: Noch keine Entscheidung +cookie_change: + other: Cookie-Einstellungen ändern \ No newline at end of file diff --git a/i18n/en.yaml b/i18n/en.yaml new file mode 100644 index 0000000..08cedaa --- /dev/null +++ b/i18n/en.yaml @@ -0,0 +1,18 @@ +site_wants_cookie: + other: This site would like to store a cookie. +cookie_more_info: + other: More about this. +cookie_button_accept: + other: Accept +cookie_button_decline: + other: Decline +cookie_status: + other: Current status +cookie_accepted: + other: Accepted +cookie_declined: + other: Declined +cookie_none: + other: No decision yet +cookie_change: + other: Change cookie preferences \ No newline at end of file diff --git a/layouts/_partials/cookie-consent.html b/layouts/_partials/cookie-consent.html new file mode 100644 index 0000000..d7e0996 --- /dev/null +++ b/layouts/_partials/cookie-consent.html @@ -0,0 +1,19 @@ + diff --git a/layouts/_shortcodes/cookie-settings.html b/layouts/_shortcodes/cookie-settings.html new file mode 100644 index 0000000..85a863f --- /dev/null +++ b/layouts/_shortcodes/cookie-settings.html @@ -0,0 +1,12 @@ +

+ {{ i18n "cookie_status" }}: + + — + +

+ \ No newline at end of file