feat: replace iubenda with an own cookie banner and privacy pages, analytics only after consent
All checks were successful
Deploy to OVH VPS / deploy (push) Successful in 1m9s
All checks were successful
Deploy to OVH VPS / deploy (push) Successful in 1m9s
This commit is contained in:
parent
43cb271077
commit
0e4d4617ab
6 changed files with 259 additions and 124 deletions
81
src/components/CookieBanner.astro
Normal file
81
src/components/CookieBanner.astro
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
---
|
||||
// Own cookie banner (no iubenda). Not a cookie wall: a floating card, the site
|
||||
// stays usable behind it. Reject and Accept have the same weight. The only
|
||||
// thing that needs consent is Google Analytics, loaded by window.loadAnalytics
|
||||
// (BaseLayout) only after "Accept".
|
||||
---
|
||||
|
||||
<section
|
||||
id="cookie-banner"
|
||||
aria-label="Cookie consent"
|
||||
class="hidden fixed bottom-4 left-4 right-4 sm:right-auto sm:max-w-sm bg-white dark:bg-gray-800 shadow-2xl rounded-xl p-5 border border-gray-200 dark:border-gray-700 z-50"
|
||||
style="animation: slideUp 0.3s ease-out;"
|
||||
>
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-gray-100 mb-2">Cookies, in short</p>
|
||||
<p class="text-xs leading-relaxed text-gray-700 dark:text-gray-300 mb-4">
|
||||
The site works without cookies. If you accept, Google Analytics tells me which pages people
|
||||
read. If you reject, nothing changes for you.
|
||||
<a href="/cookie-policy" class="underline hover:text-gray-900 dark:hover:text-white"
|
||||
>Cookie Policy</a
|
||||
>
|
||||
</p>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
id="cookie-reject"
|
||||
class="flex-1 px-3 py-2 bg-gray-900 text-white dark:bg-gray-100 dark:text-gray-900 text-xs font-medium rounded-lg hover:opacity-90 transition-opacity"
|
||||
>
|
||||
Reject
|
||||
</button>
|
||||
<button
|
||||
id="cookie-accept"
|
||||
class="flex-1 px-3 py-2 bg-gray-900 text-white dark:bg-gray-100 dark:text-gray-900 text-xs font-medium rounded-lg hover:opacity-90 transition-opacity"
|
||||
>
|
||||
Accept
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script is:inline>
|
||||
(function () {
|
||||
var banner = document.getElementById("cookie-banner");
|
||||
if (!banner) return;
|
||||
|
||||
var choose = function (value) {
|
||||
try {
|
||||
localStorage.setItem("cookieConsent", value);
|
||||
localStorage.setItem("cookieConsentDate", String(Date.now()));
|
||||
} catch (e) {}
|
||||
banner.classList.add("hidden");
|
||||
if (value === "accepted") {
|
||||
window.loadAnalytics && window.loadAnalytics();
|
||||
} else {
|
||||
// Consent withdrawn: drop the Google Analytics cookies already set
|
||||
document.cookie.split(";").forEach(function (raw) {
|
||||
var name = raw.split("=")[0].trim();
|
||||
if (name.indexOf("_ga") !== 0) return;
|
||||
var host = location.hostname.replace(/^www\./, "");
|
||||
["", "; domain=" + host, "; domain=." + host].forEach(function (domain) {
|
||||
document.cookie = name + "=; Max-Age=0; path=/" + domain;
|
||||
});
|
||||
});
|
||||
}
|
||||
window.dispatchEvent(new Event("cookie-consent"));
|
||||
};
|
||||
|
||||
document.getElementById("cookie-accept").addEventListener("click", function () {
|
||||
choose("accepted");
|
||||
});
|
||||
document.getElementById("cookie-reject").addEventListener("click", function () {
|
||||
choose("rejected");
|
||||
});
|
||||
|
||||
// Footer "Cookie settings" reopens the banner
|
||||
window.openCookieSettings = function () {
|
||||
banner.classList.remove("hidden");
|
||||
};
|
||||
|
||||
if (!window.getCookieConsent || !window.getCookieConsent()) {
|
||||
banner.classList.remove("hidden");
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
|
@ -31,52 +31,21 @@ const maxWidth = width === "7xl" ? "max-w-7xl" : "max-w-4xl";
|
|||
title="Sitemap">Sitemap</a
|
||||
>
|
||||
<span class="text-gray-800 dark:text-gray-400">•</span>
|
||||
<a
|
||||
href="https://www.iubenda.com/privacy-policy/98687046"
|
||||
class="transition-colors duration-200 iubenda-white iubenda-noiframe iubenda-embed iubenda-noiframe hover:text-gray-600 dark:hover:text-gray-300"
|
||||
title="Privacy Policy">Privacy Policy</a
|
||||
<a href="/privacy-policy" class="transition-colors duration-200 hover:text-gray-600 dark:hover:text-gray-300" title="Privacy Policy"
|
||||
>Privacy Policy</a
|
||||
>
|
||||
<script is:inline type="text/javascript">
|
||||
(function (w, d) {
|
||||
var loader = function () {
|
||||
var s = d.createElement("script"),
|
||||
tag = d.getElementsByTagName("script")[0];
|
||||
s.src = "https://cdn.iubenda.com/iubenda.js";
|
||||
tag.parentNode.insertBefore(s, tag);
|
||||
};
|
||||
if (w.addEventListener) {
|
||||
w.addEventListener("load", loader, false);
|
||||
} else if (w.attachEvent) {
|
||||
w.attachEvent("onload", loader);
|
||||
} else {
|
||||
w.onload = loader;
|
||||
}
|
||||
})(window, document);
|
||||
</script>
|
||||
<span class="text-gray-800 dark:text-gray-400">•</span>
|
||||
|
||||
<a
|
||||
href="https://www.iubenda.com/privacy-policy/98687046/cookie-policy"
|
||||
class="transition-colors duration-200 iubenda-white iubenda-noiframe iubenda-embed iubenda-noiframe hover:text-gray-600 dark:hover:text-gray-300"
|
||||
title="Cookie Policy">Cookie Policy</a
|
||||
<a href="/cookie-policy" class="transition-colors duration-200 hover:text-gray-600 dark:hover:text-gray-300" title="Cookie Policy"
|
||||
>Cookie Policy</a
|
||||
>
|
||||
<script is:inline type="text/javascript">
|
||||
(function (w, d) {
|
||||
var loader = function () {
|
||||
var s = d.createElement("script"),
|
||||
tag = d.getElementsByTagName("script")[0];
|
||||
s.src = "https://cdn.iubenda.com/iubenda.js";
|
||||
tag.parentNode.insertBefore(s, tag);
|
||||
};
|
||||
if (w.addEventListener) {
|
||||
w.addEventListener("load", loader, false);
|
||||
} else if (w.attachEvent) {
|
||||
w.attachEvent("onload", loader);
|
||||
} else {
|
||||
w.onload = loader;
|
||||
}
|
||||
})(window, document);
|
||||
</script>
|
||||
<span class="text-gray-800 dark:text-gray-400">•</span>
|
||||
<button
|
||||
type="button"
|
||||
onclick="window.openCookieSettings && window.openCookieSettings()"
|
||||
class="transition-colors duration-200 hover:text-gray-600 dark:hover:text-gray-300"
|
||||
>
|
||||
Cookie settings
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center sm:text-right text-gray-800 dark:text-gray-200 text-xs">
|
||||
|
|
|
|||
|
|
@ -32,74 +32,41 @@ const fullImageUrl = image
|
|||
: `${siteUrl}${image}`
|
||||
: `${siteUrl}${defaultImage}`;
|
||||
const fullCanonicalUrl = canonicalUrl || `${siteUrl}${Astro.url.pathname}`;
|
||||
import CookieBanner from "../components/CookieBanner.astro";
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<script is:inline type="text/javascript">
|
||||
var _iub = _iub || [];
|
||||
_iub.csConfiguration = {
|
||||
askConsentAtCookiePolicyUpdate: true,
|
||||
cookiePolicyInOtherWindow: true,
|
||||
enableFadp: true,
|
||||
fadpApplies: true,
|
||||
floatingPreferencesButtonDisplay: "anchored-center-right",
|
||||
lang: "en",
|
||||
perPurposeConsent: true,
|
||||
siteId: 3452245,
|
||||
whitelabel: false,
|
||||
cookiePolicyId: 98687046,
|
||||
banner: {
|
||||
acceptButtonCaptionColor: "#FFFFFF",
|
||||
acceptButtonColor: "#0073CE",
|
||||
acceptButtonDisplay: true,
|
||||
backgroundColor: "#FFFFFF",
|
||||
closeButtonDisplay: false,
|
||||
customizeButtonCaptionColor: "#4D4D4D",
|
||||
customizeButtonColor: "#DADADA",
|
||||
customizeButtonDisplay: true,
|
||||
explicitWithdrawal: true,
|
||||
listPurposes: true,
|
||||
position: "float-bottom-left",
|
||||
rejectButtonCaptionColor: "#FFFFFF",
|
||||
rejectButtonColor: "#0073CE",
|
||||
rejectButtonDisplay: true,
|
||||
showPurposesToggles: true,
|
||||
showTitle: false,
|
||||
textColor: "#000000",
|
||||
},
|
||||
// Let the dark mode toast wait until the cookie banner is gone
|
||||
callback: {
|
||||
onBannerShown: function () {
|
||||
window.__iubBannerOpen = true;
|
||||
},
|
||||
onPreferenceExpressed: function () {
|
||||
window.__iubBannerOpen = false;
|
||||
window.dispatchEvent(new Event("iub-consent"));
|
||||
},
|
||||
onReady: function () {
|
||||
window.__iubReady = true;
|
||||
window.dispatchEvent(new Event("iub-ready"));
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<script
|
||||
is:inline
|
||||
type="text/javascript"
|
||||
src="//cdn.iubenda.com/cs/iubenda_cs.js"
|
||||
charset="UTF-8"
|
||||
async></script>
|
||||
|
||||
<script is:inline async src="https://www.googletagmanager.com/gtag/js?id=G-SPLW5RY3HD"></script>
|
||||
<script is:inline>
|
||||
// Google Analytics only after consent (CookieBanner). The choice lasts
|
||||
// 6 months, then the banner asks again.
|
||||
window.getCookieConsent = function () {
|
||||
try {
|
||||
var value = localStorage.getItem("cookieConsent");
|
||||
var date = Number(localStorage.getItem("cookieConsentDate") || 0);
|
||||
if (!value || Date.now() - date > 180 * 24 * 60 * 60 * 1000) return null;
|
||||
return value;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag() {
|
||||
dataLayer.push(arguments);
|
||||
}
|
||||
window.loadAnalytics = function () {
|
||||
if (window.__gaLoaded) return;
|
||||
window.__gaLoaded = true;
|
||||
var script = document.createElement("script");
|
||||
script.async = true;
|
||||
script.src = "https://www.googletagmanager.com/gtag/js?id=G-SPLW5RY3HD";
|
||||
document.head.appendChild(script);
|
||||
gtag("js", new Date());
|
||||
gtag("config", "G-SPLW5RY3HD");
|
||||
};
|
||||
if (window.getCookieConsent() === "accepted") window.loadAnalytics();
|
||||
</script>
|
||||
|
||||
<meta charset="UTF-8" />
|
||||
|
|
@ -240,6 +207,8 @@ const fullCanonicalUrl = canonicalUrl || `${siteUrl}${Astro.url.pathname}`;
|
|||
<body class="bg-white dark:bg-gray-900 min-h-screen transition-colors duration-200">
|
||||
<slot />
|
||||
|
||||
<CookieBanner />
|
||||
|
||||
<!-- Dark Mode Suggestion Toast -->
|
||||
<div
|
||||
id="dark-mode-toast"
|
||||
|
|
@ -324,11 +293,16 @@ const fullCanonicalUrl = canonicalUrl || `${siteUrl}${Astro.url.pathname}`;
|
|||
const hasBeenDismissed = localStorage.getItem(TOAST_DISMISSED_KEY);
|
||||
|
||||
// Only show toast if user is in light mode and hasn't dismissed it
|
||||
const bannerOpen = () =>
|
||||
window.__iubBannerOpen || !!document.getElementById("iubenda-cs-banner");
|
||||
// Never on top of the cookie banner: if it's open, wait for the answer
|
||||
const bannerOpen = () => {
|
||||
const banner = document.getElementById("cookie-banner");
|
||||
return !!banner && !banner.classList.contains("hidden");
|
||||
};
|
||||
const showToast = () => {
|
||||
if (bannerOpen()) {
|
||||
window.addEventListener("iub-consent", () => setTimeout(showToast, 1500), { once: true });
|
||||
window.addEventListener("cookie-consent", () => setTimeout(showToast, 1500), {
|
||||
once: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const isDarkMode = document.documentElement.classList.contains("dark");
|
||||
|
|
@ -336,27 +310,7 @@ const fullCanonicalUrl = canonicalUrl || `${siteUrl}${Astro.url.pathname}`;
|
|||
toast.classList.remove("hidden");
|
||||
}
|
||||
};
|
||||
|
||||
// Never on top of the cookie banner: wait for iubenda, and if the banner
|
||||
// is open wait for the visitor's answer. If iubenda never loads
|
||||
// (blocked), show it anyway after a few seconds.
|
||||
let scheduled = false;
|
||||
const scheduleToast = (delay) => {
|
||||
if (scheduled) return;
|
||||
scheduled = true;
|
||||
setTimeout(showToast, delay);
|
||||
};
|
||||
const afterIubenda = () => {
|
||||
scheduleToast(2000);
|
||||
};
|
||||
if (window.__iubReady) {
|
||||
afterIubenda();
|
||||
} else {
|
||||
window.addEventListener("iub-ready", afterIubenda, { once: true });
|
||||
setTimeout(() => {
|
||||
if (!window.__iubReady) scheduleToast(0);
|
||||
}, 6000);
|
||||
}
|
||||
setTimeout(showToast, 2000);
|
||||
|
||||
// Enable dark mode
|
||||
if (tryButton) {
|
||||
|
|
|
|||
32
src/layouts/LegalLayout.astro
Normal file
32
src/layouts/LegalLayout.astro
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
---
|
||||
import Footer from "../components/Footer.astro";
|
||||
import Navbar from "../components/Navbar.astro";
|
||||
import BaseLayout from "./BaseLayout.astro";
|
||||
import "../styles/prose.css";
|
||||
|
||||
const { frontmatter } = Astro.props;
|
||||
---
|
||||
|
||||
<BaseLayout title={`${frontmatter.title} | Lorenzo Iovino`} description={frontmatter.description}>
|
||||
<Navbar />
|
||||
<div class="min-h-screen pt-16 bg-secondary dark:bg-gray-900 transition-colors duration-200">
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12 md:py-20">
|
||||
<div class="mb-16 text-center">
|
||||
<h1 class="text-gray-800 dark:text-gray-100 mb-4 pb-2">{frontmatter.title}</h1>
|
||||
<p class="text-xl text-gray-700 dark:text-gray-300 max-w-2xl mx-auto">
|
||||
Last updated: {frontmatter.updated}
|
||||
</p>
|
||||
<div class="h-1 w-20 bg-gray-600 dark:bg-primary rounded-full mx-auto mt-6"></div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="bg-white dark:bg-gray-800 rounded-2xl shadow-soft-lg p-8 md:p-12 lg:p-16 transition-colors duration-200"
|
||||
>
|
||||
<div class="prose prose-lg max-w-none">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
</BaseLayout>
|
||||
43
src/pages/cookie-policy.md
Normal file
43
src/pages/cookie-policy.md
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
---
|
||||
layout: ../layouts/LegalLayout.astro
|
||||
title: "Cookie Policy"
|
||||
description: "Which cookies and local storage lorenzoiovino.com uses and how to change your choice."
|
||||
updated: "September 29, 2026"
|
||||
---
|
||||
|
||||
The site works without cookies. Analytics cookies are set only if you accept them.
|
||||
|
||||
## Technical storage (always on)
|
||||
|
||||
The site saves a few small values in your browser's local storage. They don't track you and are never sent to anyone. They only remember your choices:
|
||||
|
||||
| Name | What it does | Duration |
|
||||
|---|---|---|
|
||||
| `theme` | light or dark mode | until you clear it |
|
||||
| `darkModeToastDismissed` | don't show the "Try Dark Mode?" hint again | until you clear it |
|
||||
| `cookieConsent`, `cookieConsentDate` | your answer to the cookie banner | 6 months, then the banner asks again |
|
||||
|
||||
These are strictly necessary for the features you use, so they don't need consent.
|
||||
|
||||
## Analytics cookies (only if you accept)
|
||||
|
||||
If you click **Accept**, Google Analytics 4 sets these cookies to count visits:
|
||||
|
||||
| Name | Provider | Duration |
|
||||
|---|---|---|
|
||||
| `_ga` | Google | 2 years |
|
||||
| `_ga_<container id>` | Google | 2 years |
|
||||
|
||||
More details on how Google uses this data are in the [Privacy Policy](/privacy-policy) and in [Google's documentation](https://policies.google.com/technologies/cookies).
|
||||
|
||||
## Third-party content
|
||||
|
||||
Some blog posts embed videos from YouTube and GIFs from Giphy. These services may set their own cookies when you open those posts. They are ruled by the cookie policies of [Google](https://policies.google.com/technologies/cookies) and [Giphy](https://support.giphy.com/hc/en-us/articles/360032872931-GIPHY-Privacy-Policy).
|
||||
|
||||
## Change your choice
|
||||
|
||||
Use **Cookie settings** in the footer to open the banner again. If you switch from Accept to Reject, the Google Analytics cookies are deleted. You can also delete cookies and local storage from your browser settings at any time.
|
||||
|
||||
## Contact
|
||||
|
||||
[thisloke@proton.me](mailto:thisloke@proton.me)
|
||||
56
src/pages/privacy-policy.md
Normal file
56
src/pages/privacy-policy.md
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
---
|
||||
layout: ../layouts/LegalLayout.astro
|
||||
title: "Privacy Policy"
|
||||
description: "What personal data lorenzoiovino.com processes, why and what your rights are."
|
||||
updated: "September 29, 2026"
|
||||
---
|
||||
|
||||
This is a personal website with a blog. It has no accounts, no forms and no newsletter, so it collects very little. This page explains what.
|
||||
|
||||
## Who is responsible
|
||||
|
||||
**Lorenzo Iovino** is the data controller.
|
||||
<br/>
|
||||
Contact: [thisloke@proton.me](mailto:thisloke@proton.me)
|
||||
|
||||
## What data and why
|
||||
|
||||
### Server logs
|
||||
|
||||
The site is hosted on a server of **OVH Srl** in Milan, Italy. Like any web server, it records technical data for every request: IP address, date and time, requested page, browser and operating system. These logs are used only to keep the site running and secure (errors, abuse, attacks) and are kept for a limited time.
|
||||
|
||||
Legal basis: legitimate interest in running a secure website (art. 6.1.f GDPR).
|
||||
|
||||
### Google Analytics (only if you accept)
|
||||
|
||||
If you click **Accept** on the cookie banner, the site loads **Google Analytics 4** (Google Ireland Limited / Google LLC) to count visits and see which pages people read. It uses cookies and collects usage data: pages visited, time on page, device, browser, approximate location. Google Analytics 4 does not store the full IP address.
|
||||
|
||||
Data may be transferred to the United States. Google LLC is certified under the EU-US Data Privacy Framework.
|
||||
|
||||
If you click **Reject**, Google Analytics is never loaded. You can change your choice at any time with **Cookie settings** in the footer.
|
||||
|
||||
Legal basis: your consent (art. 6.1.a GDPR).
|
||||
|
||||
### Google Fonts
|
||||
|
||||
The pages load the Inter font from Google's servers (fonts.gstatic.com). To send the font, Google receives your IP address and browser information.
|
||||
|
||||
Legal basis: legitimate interest in displaying the site correctly (art. 6.1.f GDPR).
|
||||
|
||||
### Embedded content
|
||||
|
||||
Some blog posts embed videos from **YouTube** and GIFs from **Giphy**. When you open one of those posts, your browser connects to these services, which may collect data and set their own cookies, according to their privacy policies ([YouTube / Google](https://policies.google.com/privacy), [Giphy](https://support.giphy.com/hc/en-us/articles/360032872931-GIPHY-Privacy-Policy)).
|
||||
|
||||
### Email
|
||||
|
||||
If you write to me, I use your email address and what you write only to answer you.
|
||||
|
||||
## Your rights
|
||||
|
||||
You can ask me to access, correct or delete your data, to restrict or object to its processing and to receive it in a portable format. You can withdraw your consent to analytics at any time, without affecting what happened before.
|
||||
|
||||
Write to [thisloke@proton.me](mailto:thisloke@proton.me). If you think your data is processed unlawfully, you can also file a complaint with the Italian data protection authority, the [Garante per la protezione dei dati personali](https://www.garanteprivacy.it).
|
||||
|
||||
## Cookies
|
||||
|
||||
The details about cookies and local storage are in the [Cookie Policy](/cookie-policy).
|
||||
Loading…
Add table
Add a link
Reference in a new issue