Add GoatCounter count.js component (as goat.js), MIT license logo
All checks were successful
Build And Deploy / build-and-deploy (push) Successful in 5m19s

(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.
This commit is contained in:
badblocks 2026-03-07 22:38:34 -08:00
parent 89c369d1cc
commit 2548a84e0b
No known key found for this signature in database
7 changed files with 437 additions and 30 deletions

38
LICENSE Normal file
View file

@ -0,0 +1,38 @@
Copyright 2026 badblocks
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the “Software”), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
This software also contains various permissively-licensed 3rd-party
components, the licenses for which are listed below.
public/goat.js; ISC license
Copyright © Martin Tournoij <martin@arp242.net>
Permission to use, copy, modify, and/or distribute this software for any purpose
with or without fee is hereby granted, provided that the above copyright notice
and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
THIS SOFTWARE.

321
public/goat.js Normal file
View file

@ -0,0 +1,321 @@
// 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();
});
})();

29
public/htmz.dev.js Normal file
View file

@ -0,0 +1,29 @@
function htmz(frame) {
// -------------------------------->1------------------------------------
// No history
// ----------------------------------------------------------------------
// This clears the iframe's history from the global history
// by removing the iframe from the DOM (but immediately adding it back
// for subsequent requests).
// ---------------------------------1<-----------------------------------
// -------------------------------->2-----------------------------------
// Repeat GETs
// ----------------------------------------------------------------------
// This clears the iframe URL for a fresh start on next load.
// ---------------------------------2<-----------------------------------
// ------------------------------->1&2-----------------------------------
if (frame.contentWindow.location.href === "about:blank") return;
// --------------------------------1&2<----------------------------------
setTimeout(() => {
document
.querySelector(frame.contentWindow.location.hash || null)
?.replaceWith(...frame.contentDocument.body.childNodes);
// ---------------------------------2<-----------------------------------
frame.contentWindow.location.replace("about:blank");
// -------------------------------->2------------------------------------
// ---------------------------------1<-----------------------------------
frame.remove();
document.body.appendChild(frame);
// -------------------------------->1------------------------------------
});
}

11
public/htmz.js Normal file
View file

@ -0,0 +1,11 @@
function htmz(frame) {
if (frame.contentWindow.location.href === "about:blank") return;
setTimeout(() => {
document
.querySelector(frame.contentWindow.location.hash || null)
?.replaceWith(...frame.contentDocument.body.childNodes);
frame.contentWindow.location.replace("about:blank");
frame.remove();
document.body.appendChild(frame);
});
}

11
public/mit.svg Normal file
View file

@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" role="img">
<defs>
<style>
path {
fill: #ffffff;
}
</style>
</defs>
<path d="M689.414 457.075l57.233-.196.837 243.787-57.233.197zM306.561 329.641h57.235v242.948h-57.235zm127.337-.775l57.235-.13.838 371.125-57.235.13zm-255.515-.064h57.237v371.963h-57.237z"/><path d="M512.005 0C228.872 0-1.475 229.68-1.48 511.996c-.002 282.316 230.343 512 513.474 512.004 283.133 0 513.482-229.68 513.484-511.996v-.008C1025.477 229.684 795.134.004 512.005 0zM963.56 512v.004c-.002 248.172-202.573 450.075-451.563 450.075-248.992-.004-451.559-201.91-451.555-450.083.003-248.172 202.573-450.075 451.563-450.075 248.986.004 451.553 201.907 451.555 450.075z"/>
<path d="M678.541 396.794l.233-57.234 206.924.84-.232 57.233zm-116.467 60.183h57.235v242.949h-57.235zm-.834-127.872l57.231-.602.837 79.582-57.232.602z"/>
</svg>

After

Width:  |  Height:  |  Size: 901 B

View file

@ -141,6 +141,20 @@ a {
text-decoration: none; text-decoration: none;
} }
} }
footer a {
text-decoration: none;
width: fit-content;
display: inline-block;
margin: 0 auto;
padding: 0;
&:hover::before {
content: none;
}
&:hover::after {
content: none;
}
}
ul { ul {
list-style-type: square; list-style-type: square;
} }
@ -159,8 +173,8 @@ label:has(> input[type="radio"]) {
display: flex; display: flex;
margin-block: var(--stack-large); margin-block: var(--stack-large);
} }
/* TODO: use textarea-wrapper strategy to implement invisible inputs idea, /* TODO: use textarea-wrapper strategy to implement invisible inputs idea,
but have the text-glow effect done on the ::after pseudo-element and hide but have the text-glow effect done on the ::after pseudo-element and hide
the input when it isnt focused, and swap when focused */ the input when it isnt focused, and swap when focused */
input[type="text"], input[type="text"],
input[type="email"], input[type="email"],

View file

@ -1,34 +1,23 @@
<!doctype html> <!doctype html>
<!--prettier-ignore-->
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link <link rel="preload" href="/unscii-16.woff" as="font" crossorigin="anonymous" />
rel="preload"
as="font"
href="/unscii-16.woff"
crossorigin="anonymous"
/>
<link rel="preload" href="reset.css" as="style" /> <link rel="preload" href="reset.css" as="style" />
<link rel="preload" href="theme.css" as="style" /> <link rel="preload" href="theme.css" as="style" />
<link rel="stylesheet" type="text/css" href="/style.css" /> <link rel="stylesheet" type="text/css" href="/style.css" />
<link <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' width='100px' height='100px'><rect x='0' y='0' width='100' height='100' rx='15' ry='15' style='fill: rgb(0,0,0);'/><foreignObject width='100' height='100'><div xmlns='http://www.w3.org/1999/xhtml' style='width:100px;height:100px;line-height:100px;text-align:center;vertical-align:middle;color:transparent;text-shadow: 0 0 rgb(0 255 0);font-size:80px;'>👾</div></foreignObject></svg>"/>
rel="icon"
href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' width='100px' height='100px'><rect x='0' y='0' width='100' height='100' rx='15' ry='15' style='fill: rgb(0,0,0);'/><foreignObject width='100' height='100'><div xmlns='http://www.w3.org/1999/xhtml' style='width:100px;height:100px;line-height:100px;text-align:center;vertical-align:middle;color:transparent;text-shadow: 0 0 rgb(0 255 0);font-size:80px;'>👾</div></foreignObject></svg>"
/>
<meta name="generator" content={Astro.generator} /> <meta name="generator" content={Astro.generator} />
<meta name="darkreader-lock" /> <meta name="darkreader-lock" />
<link rel="sitemap" href="/sitemap-index.xml" /> <link rel="sitemap" href="/sitemap-index.xml" />
<script <script is:inline src="/htmz.js"></script>
data-goatcounter="https://badblocks.goatcounter.com/count" <script is:inline async src="/goat.js" data-goatcounter="https://badblocks.goatcounter.com/count"></script>
async
src="//gc.zgo.at/count.js"></script>
<script>
import "iconify-icon";
</script>
<slot name="head" /> <slot name="head" />
</head> </head>
<body> <body>
<iframe hidden="" name="htmz" onload="window.htmz(this)"></iframe>
<header> <header>
<h1>badblocks.dev</h1> <h1>badblocks.dev</h1>
<nav> <nav>
@ -44,17 +33,11 @@
</main> </main>
<footer> <footer>
<p> <p>
<a <a href="https://opensource.org/license/mit" target="_blank">
href="https://creativecommons.org/licenses/by-nc-sa/4.0/" <img src="/mit.svg" title="MIT Licensed" alt="MIT License Logo" width="24" height="24" role="img">
target="_blank" </a>
><iconify-icon icon="fa7-brands:creative-commons" <br>
></iconify-icon><iconify-icon icon="fa7-brands:creative-commons-by" Made from scratch with BAHz: Bun, Astro, and Htmz!
></iconify-icon><iconify-icon icon="fa7-brands:creative-commons-nc"
></iconify-icon><iconify-icon icon="fa7-brands:creative-commons-sa"
></iconify-icon></a
>
<br />
Made from scratch with BAHA: Bun, Astro, Htmx, and Alpine!
</p> </p>
</footer> </footer>
</body> </body>