From d8bc79ad2dfb4d70bfe64fa2f349398073b1b0a2 Mon Sep 17 00:00:00 2001
From: Frank Blechschmidt <contact@frank-blechschmidt.com>
Date: Mon, 22 May 2023 15:58:05 -0700
Subject: Add support for cookie consent and with GA4

---
 layouts/partials/consent.html         | 170 ++++++++++++++++++++++++++++++++++
 layouts/partials/googleanalytics.html |  18 ++++
 layouts/partials/head.html            |  22 ++---
 3 files changed, 198 insertions(+), 12 deletions(-)
 create mode 100644 layouts/partials/consent.html
 create mode 100644 layouts/partials/googleanalytics.html

(limited to 'layouts/partials')

diff --git a/layouts/partials/consent.html b/layouts/partials/consent.html
new file mode 100644
index 0000000..2f9181d
--- /dev/null
+++ b/layouts/partials/consent.html
@@ -0,0 +1,170 @@
+{{/*  Based on https://hugocodex.org/add-ons/cookie-consent/  */}}
+
+<style>
+    #consent-notice {padding: 0.25rem 0; display: none; text-align: center; position: fixed; bottom: 0; width: 100%; background: #222;}
+    #consent-notice span {margin-right: 1rem; color: rgba(255,255,255,0.8);}
+    #consent-notice button {cursor: pointer; display: inline-block; width: auto; }
+    #consent-notice span a {color: inherit; text-decoration: underline; text-decoration-color: rgba(255,255,255,0.5);}
+    #consent-notice button.btn {margin-left: 0.5rem;}
+    #consent-notice button.btn.manage-consent {background: rgba(255,255,255,0.8); font-weight: normal;}
+    #consent-notice button.btn.deny-consent, #consent-notice button.btn.approve-consent {background: rgba(125,125,125,0.8); font-weight: normal; color: rgba(255,255,255,0.8);}
+
+    #consent-overlay {position: fixed; left: 0; top: 0; width: 100%; height: 100vh; display: none; background: rgba(0,0,0,0.75); z-index: 999999; overflow: auto; cursor: pointer;}
+    #consent-overlay.active {display: flex;}
+    #consent-overlay > div {background: white; width: 100%; max-width: 30rem; padding: 1.75rem; margin: auto; cursor: initial;}
+    #consent-overlay > div > div {display: flex; align-items: flex-start; margin-bottom: 1rem;}
+    #consent-overlay > div > div:last-child {margin: 0;}
+    #consent-overlay h5 {padding-top: 0;}
+    #consent-overlay input {margin: 0.4rem;}
+    #consent-overlay label {display: block;}
+    #consent-overlay .btn {margin-right: 0.5rem;}
+    #consent-overlay button.btn.save-consent {background: rgba(125,125,125,0.8); font-weight: normal; color: rgba(255,255,255,0.8);}
+    #consent-overlay button.btn.approve-all-consent {background: rgba(0,0,0,0.8); font-weight: normal; color: rgba(255,255,255,0.8);}
+
+    @media (max-width: 767px) {
+        #consent-overlay > div {padding: 1.75rem 1rem;}
+        #consent-notice span {display: block; padding-top: 3px; margin-bottom: 1.5rem;}
+        #consent-notice button.btn {position: relative; bottom: 4px;}
+    }
+</style>
+<div id="consent-notice"><span>This website would like to use <a class="manage-consent" href="#manage-consent">third party code</a> to improve its functionality.</span><button class="btn manage-consent">Manage preferences</button><button class="btn deny-consent">Deny</button><button class="btn approve-consent">Allow</button></div>
+<div id="consent-overlay">
+    <div>
+        {{ range $index, $item := .Site.Data.consent.items }}
+            <div>
+                <input type="checkbox" id="item{{ $index }}" value="1" name="item{{ $index }}" {{ if $item.is_functional }}checked disabled{{ end }} />
+                <label for="item{{ $index }}">
+                    <h5>{{ $item.title }}</h5>
+                    <p>{{ $item.description }}</p>
+                </label>
+            </div>
+        {{ end }}
+        <div>
+            <button id="save-consent" class="btn save-consent" data-consentvalue="{{ range $index, $item := .Site.Data.consent.items }}{{ if $item.is_functional}}{{ else }}0{{ end }}{{ end }}">Save preferences</button>
+            <button class="btn approve-all-consent">Allow all</button>
+        </div>
+    </div>
+</div>
+<script>
+
+    const scripts = [];{{ range $index, $item := (where .Site.Data.consent.items "is_functional" false) }}
+    scripts[{{ $index }}] = {{ cond (hasPrefix $item.script "http") $item.script (printf "/js/%s" $item.script) }};{{ end }}
+
+    function createCookie(name,value,days) {
+        var expires = "";
+        if (days) {
+            var date = new Date();
+            date.setTime(date.getTime() + (days*24*60*60*1000));
+            expires = "; expires=" + date.toUTCString();
+        }
+        document.cookie = name + "=" + value + expires + "; path=/";
+    }
+    function readCookie(name) {
+        var nameEQ = name + "=";
+        var ca = document.cookie.split(';');
+        for(var i=0;i < ca.length;i++) {
+            var c = ca[i];
+            while (c.charAt(0)==' ') c = c.substring(1,c.length);
+            if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length);
+        }
+        return null;
+    }
+    function eraseCookie(name) {
+        createCookie(name,"",-1);
+    }
+    function denyAllConsentScripts() {
+        var consentValue = "";
+        scripts.forEach(function(){
+            consentValue = consentValue + "0";
+        });
+        acceptSomeConsentScripts(consentValue);
+    }
+    function acceptAllConsentScripts() {
+        var consentValue = "";
+        scripts.forEach(function(){
+            consentValue = consentValue + "1";
+        });
+        acceptSomeConsentScripts(consentValue);
+    }
+    function acceptSomeConsentScripts(consentValue) {
+        setConsentInputs(consentValue);
+        createCookie('consent-settings',consentValue,31);
+        document.getElementById('consent-notice').style.display = 'none';
+        document.getElementById('consent-overlay').classList.remove('active');
+        loadConsentScripts(consentValue);
+    }
+    function loadConsentScripts(consentValue) {
+        scripts.forEach(function(value,key){
+            //console.log('script'+key+' is set to ' +consentValue[key]+' and is file '+value);
+            if(consentValue[key]=="1") {
+                var s = document.createElement('script');
+                s.type = 'text/javascript';
+                s.src = value;
+                document.body.appendChild(s);
+            }
+        });
+    }
+    function setConsentInputs(consentValue) {
+        var elements = document.querySelectorAll('#consent-overlay input:not([disabled])');
+        elements.forEach(function(el,index) {
+            if(consentValue[index]=="1") el.checked = true;
+            else el.checked = false;
+        });
+    }
+    function setConsentValue() {
+        var elements = document.querySelectorAll('#consent-overlay input:not([disabled])');
+        var consentValue = "";
+        elements.forEach(function(el) {
+            if(el.checked) consentValue = consentValue + "1";
+            else consentValue = consentValue + "0";
+        });
+        document.getElementById("save-consent").dataset.consentvalue = consentValue;
+    }
+
+    var elements = document.querySelectorAll('#consent-overlay input:not([disabled])');
+    elements.forEach(function(el) {
+        el.checked = false;
+    });
+
+    if(readCookie('consent-settings')) {
+        var consentValue = readCookie('consent-settings').toString();
+        //console.log(consentValue);
+        setConsentInputs(consentValue);
+        loadConsentScripts(consentValue);
+    } else {
+        document.getElementById('consent-notice').style.display = 'block';
+    }
+    var elements = document.querySelectorAll('.manage-consent');
+    elements.forEach(function(el) {
+        el.addEventListener("click",function() {
+            document.getElementById('consent-overlay').classList.toggle('active');
+        });
+    });
+    var elements = document.querySelectorAll('.deny-consent');
+    elements.forEach(function(el) {
+        el.addEventListener("click",function() {
+            denyAllConsentScripts();
+        });
+    });
+    var elements = document.querySelectorAll('.approve-consent, .approve-all-consent');
+    elements.forEach(function(el) {
+        el.addEventListener("click",function() {
+            acceptAllConsentScripts();
+        });
+    });
+    document.getElementById("save-consent").addEventListener("click",function() {
+        setConsentValue();
+        acceptSomeConsentScripts(this.dataset.consentvalue);
+    });
+    document.getElementById("consent-overlay").addEventListener("click",function(e) {
+        if (!document.querySelector("#consent-overlay > div").contains(e.target)){
+            this.classList.toggle('active');
+        }
+    });
+</script>
+
+{{ range $index, $item := .Site.Data.consent.items }}
+    {{ if $item.is_functional }}
+        <script type="text/javascript" src="{{ cond (hasPrefix $item.script "http") $item.script (printf "/js/%s" $item.script) }}"></script>
+    {{ end }}
+{{ end }}
diff --git a/layouts/partials/googleanalytics.html b/layouts/partials/googleanalytics.html
new file mode 100644
index 0000000..2d72d71
--- /dev/null
+++ b/layouts/partials/googleanalytics.html
@@ -0,0 +1,18 @@
+{{- $pc := .Site.Config.Privacy.GoogleAnalytics -}}
+{{- if not $pc.Disable }}{{ with .Site.GoogleAnalytics -}}
+<script>
+{{- if not $pc.RespectDoNotTrack -}}
+var doNotTrack = false;
+{{- else -}}
+var dnt = (navigator.doNotTrack || window.doNotTrack || navigator.msDoNotTrack);
+var doNotTrack = (dnt == "1" || dnt == "yes");
+{{- end -}}
+if (!doNotTrack) {
+	window.dataLayer = window.dataLayer || [];
+	function gtag(){dataLayer.push(arguments);}
+	gtag('js', new Date());
+	gtag('config', '{{ site.Config.Services.GoogleAnalytics.ID }}');
+}
+</script>
+{{- end -}}
+{{- end -}}
diff --git a/layouts/partials/head.html b/layouts/partials/head.html
index f01e36b..f658d68 100644
--- a/layouts/partials/head.html
+++ b/layouts/partials/head.html
@@ -10,23 +10,21 @@
     <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css" integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">
     <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css" integrity="sha512-iBBXm8fW90+nuLcSKlbmrPcLa0OT92xO1BIsZ+ywDWZCvqsWgccV3gFoRBv0z+8dLJgyAHIhR35VZc2oM/gI1w==" crossorigin="anonymous">
     <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/jpswalsh/academicons@1/css/academicons.min.css">
-
-    {{ $style := resources.Get "sass/researcher.scss" | resources.ExecuteAsTemplate "sass/researcher.scss" . | toCSS | minify }}
+    {{ $style := resources.Get "sass/researcher.scss" | resources.ExecuteAsTemplate "sass/researcher.scss" . | toCSS | minify -}}
     <link rel="stylesheet" href="{{ $style.RelPermalink }}">
-
-    {{ with .Site.Params.favicon }}
-        <link rel="icon" type="image/ico" href="{{ . | absURL }}">
-    {{ end }}
+    {{ with .Site.Params.favicon -}}
+    <link rel="icon" type="image/ico" href="{{ . | absURL }}">
+    {{ end -}}
 
     {{ with .OutputFormats.Get "rss" -}}
         {{ printf `<link rel="%s" type="%s" href="%s" title="%s" />` .Rel .MediaType.Type .Permalink $.Site.Title | safeHTML }}
     {{ end -}}
 
-    {{ if not .Site.IsServer }}
-        {{ template "_internal/google_analytics.html" . }}
-    {{ end }}
+    {{- if not .Site.IsServer -}}
+        {{- partial "googleanalytics.html" . -}}
+    {{- end -}}
 
-    {{ if .Params.noindex }}
-        <meta name="robots" content="noindex">
-    {{ end }}
+    {{ if .Params.noindex -}}
+    <meta name="robots" content="noindex">
+    {{ end -}}
 </head>
-- 
cgit v1.2.3