personal-site/public/goat.js
badbl0cks 2548a84e0b
All checks were successful
Build And Deploy / build-and-deploy (push) Successful in 5m19s
Add GoatCounter count.js component (as goat.js), MIT license logo
(mit.svg), and htmz assets (htmz.js and htmz.dev.js). Add LICENSE file
containing this project's MIT license and 3rd-party component licenses.
2026-03-07 22:38:34 -08:00

321 lines
9.8 KiB
JavaScript

// GoatCounter: https://www.goatcounter.com
// This file is released under the ISC license: https://opensource.org/licenses/ISC
(function () {
"use strict";
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.
window.goatcounter.get_data = function (vars) {
vars = 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,
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]");
return s && s.dataset.goatcounter
? s.dataset.goatcounter
: goatcounter.endpoint;
};
// 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.
window.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 = window.goatcounter.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 || !navigator.sendBeacon || !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: element to append to 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();
});
})();