POSSE-Flavored Analytics for the Blog
Self-hosted Umami on my own hardware, no JavaScript beacon, server-side bot filters: analytics, POSSE-style.

Until yesterday, cto4.ai had no analytics at all. No Google Analytics, no beacon script, no cookies. That was deliberate, in line with both the POSSE philosophy this site runs on and Astro’s ship-less-JavaScript ethos: I publish on my own site, you read it, and nobody gets tracked for the privilege.
Then I killed the newsletter, and the little visibility I had into uptake went with it. (The subscriber count was mostly bots anyway, but still.) Analytics started to look like a good idea, if it could be done the right way.
The Rig
I worked with Fable to identify “the most POSSE-aligned analytics solution.” The requirements fall out of the philosophy: I own the data, readers don’t run third-party scripts, no cookies, no fingerprinting. The answer we landed on is Umami, self-hosted: Umami and Postgres in Docker on the same Mac Mini that runs my Hermes Agent knowledge refinery, reachable through a Cloudflare Tunnel. Once a page view is written, the data lives on my hardware in my basement and nowhere else.
The twist is on the collection side: there is no JavaScript beacon on the blog at all. Instead, a roughly 50-line Cloudflare Pages middleware reports page views server-side as it serves them: GET requests that return HTML with a 200, on the production hostname only, skipping prefetch and prerender loads. There’s nothing for an adblocker to block, because there’s nothing running in your browser. If you’re reading this, you’ve already been counted, and you gave up nothing for it.
Bots, Again
Server-side collection has a cost: a beacon script filters most bots for free, because crawlers don’t execute JavaScript. Count on the server and you count everything — curl included. Having just spent a day shoveling 2,089 bot subscribers out of a newsletter, I wasn’t going to stand up a dashboard full of the same species.
So the middleware applies two baseline filters before reporting anything. First: drop requests with no Accept-Language header. Real browsers always send one; most crawlers can’t be bothered. Second: drop traffic from datacenter networks. Cloudflare hands the middleware the network owner on every request, and a page view from AWS, Azure, Hetzner, or a dozen other cloud providers is not a human on a couch. Bots still get the page; they just stop being counted.
So how well do the filters work? The first full day of data says: partially. Roughly 40% of the sessions that got through were still bots — headless browsers on residential IPs, rotating user agents (four “different browsers” starting in the same second is one crawler, not four readers), walking the back catalog page by page. But here’s the flip side of owning the data: the raw sessions live in my Postgres, so whenever I want a clean read on human traffic, fingerprinting them like that takes minutes. The dashboard number runs hot; the truth is one query away. Trends and campaign attribution stay reliable regardless, because bots don’t carry UTM parameters. And yes, my own visits still count; self-exclusion is on the list.
So Far
I’m pretty happy with the result. A dashboard I own, on hardware I own, telling me things I can mostly trust — and the blog still serves zero tracking code.




