0.163.3 · Blowfish 2.104
Publishing into the void #
The site’s hugo.toml had a googleAnalytics line commented out since roughly the first commit. I never uncommented it. GA4 wants a cookie disclosure, ships a chunky client, and ad blockers eat it anyway, which felt like a lot of ceremony for a personal blog whose one open question was “does anybody visit.”
What I wanted was small: page views, referrers, no cookies, no banner. The site already deploys to Cloudflare Pages, and Cloudflare Web Analytics is free, cookie-less, and one script tag. Decision made in about a minute.
Then I went looking for where the tag goes.
What the theme hands you #
Blowfish has first-class analytics support. Set one param and you are done:
# config/_default/params.toml
[umamiAnalytics]
websiteid = "..."
[fathomAnalytics]
site = "..."
[selineAnalytics]
token = "..."Plus GA4 via the top-level googleAnalytics key. Four providers, one line each, and the theme handles script injection for all of them.
Cloudflare Web Analytics is not on the list. No param, no partial. If you want it, you write the tag yourself into layouts/partials/extend-head.html, the theme’s designated extension point for exactly this kind of thing. The comment in the theme source even says “eg. for custom analytics scripts”.
So far this looks like a copy-paste job.
The part the copy-paste misses #
Before pasting, I read how the theme injects its own analytics. From themes/blowfish/layouts/partials/head.html:
{{/* Analytics */}}
{{ if hugo.IsProduction }}
{{ partial "analytics/main.html" . }}
{{ end }}
{{/* Extend head - eg. for custom analytics scripts, etc. */}}
{{ if templates.Exists "partials/extend-head.html" }}
{{ partialCached "extend-head.html" .Site }}
{{ end }}Two things in ten lines.
First: the theme’s own analytics sit behind hugo.IsProduction. Your extension point does not. Whatever you put in extend-head.html renders in every environment, including hugo server on your laptop and the hugo server instance my Playwright suite boots for every E2E run. An unguarded beacon means the dashboard counts my own dev sessions and every CI run. With traffic as modest as a personal blog’s, CI would outnumber the actual readers in the charts.
Second: partialCached with .Site as the cache key. The partial renders once per build and gets reused on every page. For a static script tag that is exactly what you want. For anything page-dependent it is a subtle trap, because your per-page logic would evaluate once, for whichever page happened to render first.
Hugo’s environment model does the work #
The guard is cheap because Hugo already distinguishes environments: plain hugo builds default to production, hugo server defaults to development. Cloudflare Pages runs hugo --gc --minify, so production builds get the beacon with no extra configuration, and local dev plus CI never see it.
The final version of my extend-head.html addition:
{{ if hugo.IsProduction }}
<!-- Cloudflare Web Analytics -->
<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js"
data-cf-beacon='{"token": "YOUR_TOKEN"}'></script>
{{ end }}The token comes from the Cloudflare dashboard under Web Analytics after registering the site. So I went to register the site.
The twist in the dashboard #
nick-liu.com was already there. Created two months earlier, marked “automatic setup”, showing real visits for the past 24 hours. Cloudflare had been injecting its beacon at the proxy the whole time, into HTML I never touched. I had analytics all along and did not know where to look.
Running both is worse than running either: two beacons per page, two dashboards each holding half the story. I kept the proxy injection and reverted my hand-wired tag. Two months of history beats a fresh counter, and zero lines in the repo beats three.
The manual route still matters when your HTML serves from a host Cloudflare does not proxy. This site has a GitHub Pages mirror the proxy never sees; if I ever care about that hostname’s numbers, the guarded snippet above is exactly what goes back in.
One process note survives the revert: never commit a placeholder token. A guarded beacon with a fake token is invisible in dev, which means the one environment where you would notice the mistake is production.
Lessons #
- If your theme gates analytics behind
hugo.IsProduction, your extend-head partial needs the same guard. The theme’s guard does not cover your code. - CI is a traffic source. An unguarded beacon on a low-traffic site measures your test suite, not your readers.
partialCachedis a contract: content renders once per cache key. Static tags are fine. Per-page logic is not.- Before wiring any analytics snippet, check whether your platform already injected one. Proxy-level features leave no trace in the repo.
- Manual injection covers hosts the proxy never sees; proxy injection keeps the repo clean. Pick one. Running both double-counts.
References #
- Blowfish partials documentation (extend-head and analytics)
- Hugo: hugo.IsProduction
- Cloudflare Web Analytics documentation
- Theme source:
themes/blowfish/layouts/partials/head.html(Blowfish v2.104.0)