Selfhost goat counter, expand img tags

This commit is contained in:
Tom 2025-05-07 12:58:34 +01:00
parent bd1cdbb187
commit 1a3d9d0bee
4 changed files with 369 additions and 36 deletions

View File

@ -66,13 +66,16 @@
<!-- Privacy Conscious Hit Counter --> <!-- Privacy Conscious Hit Counter -->
{% unless jekyll.environment == "development" %} {% unless jekyll.environment == "development" %}
<script data-goatcounter="https://tomhodson.goatcounter.com/count" <script data-goatcounter="https://tomhodson.goatcounter.com/count"
async src="https://gc.zgo.at/count.js"></script> async src="/assets/js/goat_count.js"></script>
{% endunless %} {% endunless %}
<link rel="stylesheet" href="/assets/css/styles.css"> <link rel="stylesheet" href="/assets/css/styles.css">
<script src="/assets/js/pre_page_load.js"></script> <script src="/assets/js/pre_page_load.js"></script>
<script src="/assets/js/index.js" defer></script> <script src="/assets/js/index.js" defer></script>
<!-- Add JS to expand images on click -->
<script src="/assets/js/expand_img_tags.js" defer></script>
{% if page.mathjax %} {% if page.mathjax %}
<script> <script>
console.log("MathJax is enabled for this page. Enjoy the math!"); console.log("MathJax is enabled for this page. Enjoy the math!");

View File

@ -14,9 +14,6 @@ img:
social_image: /assets/projects/downlight_bikelight/thumbnail.png social_image: /assets/projects/downlight_bikelight/thumbnail.png
model: /assets/projects/downlight_bikelight/models model: /assets/projects/downlight_bikelight/models
head: |
<script src="/assets/js/expand_img_tags.js" defer></script>
--- ---
I pulled this dying LED downlight out of my kitchen ceiling. I pulled this dying LED downlight out of my kitchen ceiling.
<figure> <figure>

View File

@ -1,14 +1,48 @@
function isIOS() { // Add this style block once to your document (or in a CSS file)
return /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; const style = document.createElement("style");
} style.textContent = `
.fullscreen-overlay {
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
}
.fullscreen-overlay.active {
opacity: 1;
pointer-events: auto;
}
`;
document.head.appendChild(style);
// Create and style fullscreen overlay container
const fullscreenContainer = document.createElement("div");
fullscreenContainer.className = "fullscreen-overlay";
fullscreenContainer.style.position = "fixed";
fullscreenContainer.style.top = 0;
fullscreenContainer.style.left = 0;
fullscreenContainer.style.width = "100vw";
fullscreenContainer.style.height = "100vh";
fullscreenContainer.style.background = "var(--theme-bg-color)";
fullscreenContainer.style.display = "flex";
fullscreenContainer.style.alignItems = "center";
fullscreenContainer.style.justifyContent = "center";
fullscreenContainer.style.zIndex = 9999;
fullscreenContainer.style.cursor = "zoom-out";
const fullscreenImage = document.createElement("img");
fullscreenImage.style.maxWidth = "90vw";
fullscreenImage.style.maxHeight = "90vh";
fullscreenImage.style.boxShadow = "0 0 20px rgba(0,0,0,0.8)";
fullscreenContainer.appendChild(fullscreenImage);
document.body.appendChild(fullscreenContainer);
// Fullscreen API helpers
function enterFullscreen(element) { function enterFullscreen(element) {
if (element.requestFullscreen) { if (element.requestFullscreen) {
element.requestFullscreen(); element.requestFullscreen();
} else if (element.webkitRequestFullscreen) { } else if (element.webkitRequestFullscreen) {
element.webkitRequestFullscreen(); element.webkitRequestFullscreen();
} else {
console.warn("Fullscreen API not supported on this browser");
} }
} }
@ -20,43 +54,19 @@ function exitFullscreen() {
} }
} }
// Create and style fullscreen overlay container // Attach listeners
const fullscreenContainer = document.createElement("div");
fullscreenContainer.style.position = "fixed";
fullscreenContainer.style.top = 0;
fullscreenContainer.style.left = 0;
fullscreenContainer.style.width = "100vw";
fullscreenContainer.style.height = "100vh";
fullscreenContainer.style.background = "rgba(0, 0, 0, 0.95)";
fullscreenContainer.style.display = "flex";
fullscreenContainer.style.alignItems = "center";
fullscreenContainer.style.justifyContent = "center";
fullscreenContainer.style.zIndex = 9999;
fullscreenContainer.style.cursor = "zoom-out";
fullscreenContainer.style.visibility = "hidden";
const fullscreenImage = document.createElement("img");
fullscreenImage.style.maxWidth = "90vw";
fullscreenImage.style.maxHeight = "90vh";
fullscreenImage.style.boxShadow = "0 0 20px rgba(0,0,0,0.8)";
fullscreenContainer.appendChild(fullscreenImage);
document.body.appendChild(fullscreenContainer);
document.querySelectorAll("img").forEach((img) => { document.querySelectorAll("img").forEach((img) => {
img.style.cursor = "zoom-in"; img.style.cursor = "zoom-in";
img.addEventListener("click", () => { img.addEventListener("click", () => {
console.log("Image clicked:", img.src); console.log("Image clicked:", img.src);
fullscreenImage.src = img.src; fullscreenImage.src = img.src;
fullscreenContainer.style.visibility = "visible"; fullscreenContainer.classList.add("active");
// Only attempt fullscreen if supported
enterFullscreen(fullscreenContainer); enterFullscreen(fullscreenContainer);
}); });
}); });
fullscreenContainer.addEventListener("click", () => { fullscreenContainer.addEventListener("click", () => {
console.log("Exiting fullscreen"); console.log("Exiting fullscreen");
fullscreenContainer.style.visibility = "hidden"; fullscreenContainer.classList.remove("active");
exitFullscreen(); exitFullscreen();
}); });

323
assets/js/goat_count.js Normal file
View File

@ -0,0 +1,323 @@
// GoatCounter: https://www.goatcounter.com
// This file is released under the ISC license: https://opensource.org/licenses/ISC
(function () {
"use strict";
if (window.goatcounter && window.goatcounter.vars)
// Compatibility with very old version; do not use.
window.goatcounter = window.goatcounter.vars;
else window.goatcounter = window.goatcounter || {};
// Load settings from data-goatcounter-settings.
var s = document.querySelector("script[data-goatcounter]");
if (s && s.dataset.goatcounterSettings) {
try {
var set = JSON.parse(s.dataset.goatcounterSettings);
} catch (err) {
console.error("invalid JSON in data-goatcounter-settings: " + err);
}
for (var k in set)
if (
[
"no_onload",
"no_events",
"allow_local",
"allow_frame",
"path",
"title",
"referrer",
"event",
].indexOf(k) > -1
)
window.goatcounter[k] = set[k];
}
var enc = encodeURIComponent;
// Get all data we're going to send off to the counter endpoint.
var get_data = function (vars) {
var data = {
p: vars.path === undefined ? goatcounter.path : vars.path,
r: vars.referrer === undefined ? goatcounter.referrer : vars.referrer,
t: vars.title === undefined ? goatcounter.title : vars.title,
e: !!(vars.event || goatcounter.event),
s: [
window.screen.width,
window.screen.height,
window.devicePixelRatio || 1,
],
b: is_bot(),
q: location.search,
};
var rcb, pcb, tcb; // Save callbacks to apply later.
if (typeof data.r === "function") rcb = data.r;
if (typeof data.t === "function") tcb = data.t;
if (typeof data.p === "function") pcb = data.p;
if (is_empty(data.r)) data.r = document.referrer;
if (is_empty(data.t)) data.t = document.title;
if (is_empty(data.p)) data.p = get_path();
if (rcb) data.r = rcb(data.r);
if (tcb) data.t = tcb(data.t);
if (pcb) data.p = pcb(data.p);
return data;
};
// Check if a value is "empty" for the purpose of get_data().
var is_empty = function (v) {
return v === null || v === undefined || typeof v === "function";
};
// See if this looks like a bot; there is some additional filtering on the
// backend, but these properties can't be fetched from there.
var is_bot = function () {
// Headless browsers are probably a bot.
var w = window,
d = document;
if (w.callPhantom || w._phantom || w.phantom) return 150;
if (w.__nightmare) return 151;
if (d.__selenium_unwrapped || d.__webdriver_evaluate || d.__driver_evaluate)
return 152;
if (navigator.webdriver) return 153;
return 0;
};
// Object to urlencoded string, starting with a ?.
var urlencode = function (obj) {
var p = [];
for (var k in obj)
if (
obj[k] !== "" &&
obj[k] !== null &&
obj[k] !== undefined &&
obj[k] !== false
)
p.push(enc(k) + "=" + enc(obj[k]));
return "?" + p.join("&");
};
// Show a warning in the console.
var warn = function (msg) {
if (console && "warn" in console) console.warn("goatcounter: " + msg);
};
// Get the endpoint to send requests to.
var get_endpoint = function () {
var s = document.querySelector("script[data-goatcounter]");
if (s && s.dataset.goatcounter) return s.dataset.goatcounter;
return goatcounter.endpoint || window.counter; // counter is for compat; don't use.
};
// Get current path.
var get_path = function () {
var loc = location,
c = document.querySelector('link[rel="canonical"][href]');
if (c) {
// May be relative or point to different domain.
var a = document.createElement("a");
a.href = c.href;
if (
a.hostname.replace(/^www\./, "") ===
location.hostname.replace(/^www\./, "")
)
loc = a;
}
return loc.pathname + loc.search || "/";
};
// Run function after DOM is loaded.
var on_load = function (f) {
if (document.body === null)
document.addEventListener(
"DOMContentLoaded",
function () {
f();
},
false
);
else f();
};
// Filter some requests that we (probably) don't want to count.
goatcounter.filter = function () {
if (
"visibilityState" in document &&
document.visibilityState === "prerender"
)
return "visibilityState";
if (!goatcounter.allow_frame && location !== parent.location)
return "frame";
if (
!goatcounter.allow_local &&
location.hostname.match(
/(localhost$|^127\.|^10\.|^172\.(1[6-9]|2[0-9]|3[0-1])\.|^192\.168\.|^0\.0\.0\.0$)/
)
)
return "localhost";
if (!goatcounter.allow_local && location.protocol === "file:")
return "localfile";
if (localStorage && localStorage.getItem("skipgc") === "t")
return "disabled with #toggle-goatcounter";
return false;
};
// Get URL to send to GoatCounter.
window.goatcounter.url = function (vars) {
var data = get_data(vars || {});
if (data.p === null)
// null from user callback.
return;
data.rnd = Math.random().toString(36).substr(2, 5); // Browsers don't always listen to Cache-Control.
var endpoint = get_endpoint();
if (!endpoint) return warn("no endpoint found");
return endpoint + urlencode(data);
};
// Count a hit.
window.goatcounter.count = function (vars) {
var f = goatcounter.filter();
if (f) return warn("not counting because of: " + f);
var url = goatcounter.url(vars);
if (!url) return warn("not counting because path callback returned null");
if (!navigator.sendBeacon(url)) {
// This mostly fails due to being blocked by CSP; try again with an
// image-based fallback.
var img = document.createElement("img");
img.src = url;
img.style.position = "absolute"; // Affect layout less.
img.style.bottom = "0px";
img.style.width = "1px";
img.style.height = "1px";
img.loading = "eager";
img.setAttribute("alt", "");
img.setAttribute("aria-hidden", "true");
var rm = function () {
if (img && img.parentNode) img.parentNode.removeChild(img);
};
img.addEventListener("load", rm, false);
document.body.appendChild(img);
}
};
// Get a query parameter.
window.goatcounter.get_query = function (name) {
var s = location.search.substr(1).split("&");
for (var i = 0; i < s.length; i++)
if (s[i].toLowerCase().indexOf(name.toLowerCase() + "=") === 0)
return s[i].substr(name.length + 1);
};
// Track click events.
window.goatcounter.bind_events = function () {
if (!document.querySelectorAll)
// Just in case someone uses an ancient browser.
return;
var send = function (elem) {
return function () {
goatcounter.count({
event: true,
path: elem.dataset.goatcounterClick || elem.name || elem.id || "",
title:
elem.dataset.goatcounterTitle ||
elem.title ||
(elem.innerHTML || "").substr(0, 200) ||
"",
referrer:
elem.dataset.goatcounterReferrer ||
elem.dataset.goatcounterReferral ||
"",
});
};
};
Array.prototype.slice
.call(document.querySelectorAll("*[data-goatcounter-click]"))
.forEach(function (elem) {
if (elem.dataset.goatcounterBound) return;
var f = send(elem);
elem.addEventListener("click", f, false);
elem.addEventListener("auxclick", f, false); // Middle click.
elem.dataset.goatcounterBound = "true";
});
};
// Add a "visitor counter" frame or image.
window.goatcounter.visit_count = function (opt) {
on_load(function () {
opt = opt || {};
opt.type = opt.type || "html";
opt.append = opt.append || "body";
opt.path = opt.path || get_path();
opt.attr = opt.attr || {
width: "200",
height: opt.no_branding ? "60" : "80",
};
opt.attr["src"] =
get_endpoint() + "er/" + enc(opt.path) + "." + enc(opt.type) + "?";
if (opt.no_branding) opt.attr["src"] += "&no_branding=1";
if (opt.style) opt.attr["src"] += "&style=" + enc(opt.style);
if (opt.start) opt.attr["src"] += "&start=" + enc(opt.start);
if (opt.end) opt.attr["src"] += "&end=" + enc(opt.end);
var tag = { png: "img", svg: "img", html: "iframe" }[opt.type];
if (!tag) return warn("visit_count: unknown type: " + opt.type);
if (opt.type === "html") {
opt.attr["frameborder"] = "0";
opt.attr["scrolling"] = "no";
}
var d = document.createElement(tag);
for (var k in opt.attr) d.setAttribute(k, opt.attr[k]);
var p = document.querySelector(opt.append);
if (!p) return warn("visit_count: append not found: " + opt.append);
p.appendChild(d);
});
};
// Make it easy to skip your own views.
if (location.hash === "#toggle-goatcounter") {
if (localStorage.getItem("skipgc") === "t") {
localStorage.removeItem("skipgc", "t");
alert("GoatCounter tracking is now ENABLED in this browser.");
} else {
localStorage.setItem("skipgc", "t");
alert(
"GoatCounter tracking is now DISABLED in this browser until " +
location +
" is loaded again."
);
}
}
if (!goatcounter.no_onload)
on_load(function () {
// 1. Page is visible, count request.
// 2. Page is not yet visible; wait until it switches to 'visible' and count.
// See #487
if (
!("visibilityState" in document) ||
document.visibilityState === "visible"
)
goatcounter.count();
else {
var f = function (e) {
if (document.visibilityState !== "visible") return;
document.removeEventListener("visibilitychange", f);
goatcounter.count();
};
document.addEventListener("visibilitychange", f);
}
if (!goatcounter.no_events) goatcounter.bind_events();
});
})();