ADR-009: Cookieless analytics via GoatCounter
Status
Accepted (2026-08-29)
Context
The site shipped site-url and Open Graph metadata on 2026-08-27 (GH #47), leaving the analytics half of that issue open on an explicit owner decision: whether to measure usage at all, and if so with what.
The launch post on LinkedIn forced the question. It drew 90,201 impressions, 65,264 members reached, 516 saves and 729 click-throughs to dsp-workshop.nl, none of which were measurable here. Two consequences made that worth fixing:
- The grow-by-deepening policy has no input. Choosing the next unit of work by deepening existing pages assumes some knowledge of which pages are read. Across 34 topics, 12 basics chapters and 30 embedded pages, that was pure guess.
- The long tail is invisible. A teaching resource earns its readers over years through search and through other people linking it. Referrer data is the only way to see that happening, and it is the interesting question about a site like this one: not the launch spike, but whether anything comes after.
The site is on GitHub Pages (ADR-004), so there are no server logs to read. A client-side beacon is the only mechanism available.
The constraint that ruled the choice is that this is a gift, not a funnel. It is CC BY-SA 4.0 and asks nothing of its readers. Instrumenting it with anything that tracks people would contradict what it is.
Decision
Add GoatCounter as a one-tag beacon: analytics.html, included through _quarto.yml’s format: html: include-in-header, alongside the existing include-after-body: fix-svg.html.
Dashboard: https://dsp-workshop.goatcounter.com.
Why this one:
- No cookies, and the visitor IP is not retained. It is used transiently for a country lookup and a rotating-salt session hash, then discarded. No consent banner, and no personal data at rest. (Verify against GoatCounter’s own privacy page before restating this anywhere reader-facing; it is their documented behaviour, not something this repo can check.)
- Free for non-commercial use, which this site is.
- Open source and self-hostable (single Go binary plus SQLite). If the hosted service ever becomes unacceptable, moving is a one-URL change to the
data-goatcounterattribute inanalytics.html. That escape hatch is the reason a hosted third party was acceptable at all.
Rejected alternatives:
- Plausible: equivalent privacy posture, EUR 9/month hosted. Nothing here justifies a running cost.
- Umami: free but self-hosted, needing a database and somewhere to run it. Real infrastructure for a site whose whole build story is “no local tooling”.
- Cloudflare Web Analytics: free and cookieless, but the data lands with a large US company. Acceptable on the merits, less aligned with what this is.
- Google Analytics: not considered. Cookies, consent banner, and the exact posture this site should not have.
- Server-side counting: unavailable on GitHub Pages.
Consequences
- Positive: per-path read counts, so the deepening policy has evidence instead of intuition. Referrers, so an adopting course or an inbound link is visible.
- Positive: no consent banner, no mailing list, no personal data held.
- Positive: reversible in one commit. Deleting
analytics.htmland itsinclude-in-headerline removes the whole mechanism. - Negative: the numbers undercount, probably substantially.
count.jsis in the common blocklists and this audience runs blockers. Treat it as a trend instrument, never as a census, and never quote an absolute from it as a fact about readership. - Negative: a third-party request on every page, which the site did not previously make. Mitigated by
async(never blocks rendering) and by the self-host escape hatch above. - Neutral: the script skips
localhost, so the no-local-preview posture of ADR-004 means almost no risk of polluting the data from development anyway.
Related
- GH #47, the discoverability-config issue this closes.
- ADR-004 (no local Quarto install), which is why there are no server logs.
- The RSS question is deliberately not decided here. A feed needs a dated “what is new” listing page, which the site does not have, and launching a subscription channel with nothing to announce is worse than not having one. Revisit when the next chapter ships.