--- title: "A Tiny 'Views' Badge Sent Me Down a Rabbit Hole (PaperMod + Busuanzi + Debugging --> swapped with GoatCounter the GOAT)" date: 2025-10-18T10:45:00 slug: papermod-views-debugging-story summary: "I tried to add a simple 'views' counter to my PaperMod blog. It worked—until it didn’t. Here’s the story of blank numbers, a mysterious Chinese message, and the final fix." tags: ["Hugo", "PaperMod", "Analytics", "Debugging", "Busuanzi", "Vercount", "GoatCounter"] categories: ["Stories"] draft: false --- I only wanted a tiny **“views”** badge next to my RSS link—just a friendly nudge that real humans were reading my words. PaperMod makes it easy to tuck little UI bits in the footer, so I dropped in Busuanzi (that neat, no-account page/site view counter) and called it a day. The thing is, I don't think the counter even works correctly. Or maybe I'm undermining my blog popularity! But regardless, it was a small stiching task that I did on this lovely Friday evening. The day before my next term of German class starts. The day before playing deadface CTF that I've been counting days for. ### The neat footer row First I got the layout right. PaperMod supports extension partials, so I used a simple flex row to keep things side by side: ```html
{ partial "svg.html" (dict "name" "rss") } RSS Site views: Visitors:
``` So far, so good. The labels showed up. The numbers, though? **Nothing.** Just an em dash. I shrugged—maybe it was a caching thing. ### The first round of checks I added the script site‑wide in `layouts/partials/extend_head.html`: ```html ``` Refreshed. Still blank. **DevTools time.** Network tab: the script **does** load: - **Request URL:** `https://cdn.busuanzi.cc/busuanzi/3.6.9/busuanzi.min.js` - **Request Method:** GET - **Status Code:** 304 Not Modified - **Referrer Policy:** strict-origin-when-cross-origin - **User-Agent:** a normal Chromium build (nothing exotic) No red flags. Just a perfectly normal 304. Maybe my IDs were wrong? Busuanzi has a couple of flavors: `busuanzi_value_*` (with container wrappers) or shorthand like `busuanzi_site_pv`. I tried **both**, carefully, one at a time. Still… **—**. ### The odd message Then it flashed for a moment—the Chinese line that made me google twice to be sure I saw it right: > **“域名过长,已被禁用 views”** That’s “domain name too long, disabled.” Wait, what? I checked my hostname: **`blog.alipourimjourneys.ir`**. I counted: **27 characters**. Busuanzi’s limit? **22.** Suddenly, everything clicked. The script loads, but the service refuses to count for long domains, so the placeholders never fill. ### Two ways out At this point I had two clean options: 1. **Keep Busuanzi** and move the site to the **apex domain** (short enough). 2. Keep my beloved `blog.` subdomain and swap in **Vercount**, a Busuanzi‑style drop‑in without the 22‑char limit. I liked the subdomain (habit, mostly), so I tried **Vercount**. ### The swap (five-minute fix) I removed the Busuanzi script and added Vercount instead: ```html ``` I kept the same tidy footer row, but switched the IDs to Vercount’s: ```html
{ partial "svg.html" (dict "name" "rss") } RSS Site views: Visitors:
``` For per‑post counts (in the post meta), I added: ```html views ``` Deploy. Refresh. **Numbers.** ### But if you want to stay with Busuanzi… If your apex domain is short enough, pointing the site there works fine. The gist: - Set `baseURL` in Hugo to the apex: ```toml baseURL = "https://alipourimjourneys.ir/" ``` - Add a 301 redirect from the long subdomain to the apex (Cloudflare, Netlify, Nginx—pick your tool). - Keep your Busuanzi spans as you had them; once the host fits the limit, they’ll populate. ### Post‑mortem checklist (a.k.a. things I learned) - **If the script loads but you get blanks**, it’s not always IDs or caching. Sometimes it’s a **service rule**. - **Busuanzi refuses long hostnames** (>22 chars) and ignores localhost. On those hosts, the placeholders stay empty or show the Chinese “disabled” note. - **Don’t mix providers** on the same page. Load exactly one script (Busuanzi *or* Vercount). - **CSP and blockers matter.** If you run a strict Content‑Security‑Policy or heavy ad blockers, they can block the script. Test in a clean profile and allow the script origin. ### Happy ending Now my footer shows RSS, site views, and visitors on a single neat line, and each post has a tiny “views” chip that quietly ticks upward. I got the tiny badge I wanted—and a reminder that the smallest features can hide the most interesting puzzles. If you hit the same wall, check the hostname length first. It might save you an afternoon—and lead you to a solution you’ll feel weirdly proud of. ### Final notes Yea... I just felt this feature was missing, and it's good to have it there. I hope I stop adding things to the blog, and just keep writing in it. I've spent too much time on it, and it feels like wasted time. --- ## Change of plans: Self‑hosting GoatCounter for PaperMod (Docker + Cloudflare + Onion) This is the **self‑host** part I ended up with: Docker for GoatCounter, Cloudflare (orange‑cloud) in front, Nginx on the server as reverse proxy, plus my `.onion` mirror counting to the same GoatCounter site. Paste this whole section at the end of the post. --- ### 1) Clean up old counters (optional, once) ```bash # From your Hugo site root grep -RinE "busuanzi|vercount|vercount_value_|busuanzi_value_" . ``` Remove any matching ` ``` Style so meta items stay inline with middle dots (PaperMod‑like). Put this in `assets/css/extended/goatcounter.css`: ```css .post-single .post-header .post-meta, .post-single .post-header .entry-meta, .post-entry .entry-footer, .post-entry .post-meta { display:flex; align-items:center; flex-wrap:wrap; gap:.35rem; } .post-single .post-header .post-meta > *, .post-single .post-header .entry-meta > *, .post-entry .entry-footer > *, .post-entry .post-meta > * { display:inline-flex; align-items:center; white-space:nowrap; } .post-single .post-header .post-meta > * + *::before, .post-single .post-header .entry-meta > * + *::before, .post-entry .entry-footer > * + *::before, .post-entry .post-meta > * + *::before { content:"·"; margin:0 .5rem; opacity:.6; } .post-views-bottom { margin:.6rem 0 0; opacity:.75; font-size:.95em; } ``` --- ### 6) Count Tor (.onion) visits **into the same site** In the **.onion** server block, proxy GoatCounter endpoints to the same site by forcing the Host header: ```nginx # inside: server { ... } for fjthpp2h3mj2r....onion location = /count { proxy_pass http://127.0.0.1:8081/count; proxy_set_header Host statsblog.alipourimjourneys.ir; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto http; # onion is http } location /counter/ { proxy_pass http://127.0.0.1:8081/counter/; proxy_set_header Host statsblog.alipourimjourneys.ir; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto http; add_header Cache-Control "no-store"; proxy_no_cache 1; proxy_cache_bypass 1; } ``` Because the loader above sets `window.goatcounter.endpoint` to `/count` on `.onion`, both **hits** and **reads** stay on the onion origin and are tallied into the same GoatCounter site. > **Note on DNT/Tor:** Tor Browser sends `DNT: 1`. GoatCounter respects DNT by default, so Tor users won’t be counted unless you disable “Respect Do Not Track” in GoatCounter settings. Your choice; I left the code compatible either way. --- ### 7) Troubleshooting quick notes - **400 “no site at this domain”** → create the site with `-vhost=statsblog.alipourimjourneys.ir` or fix your proxy `Host` header. - **Counts don’t change on onion** → verify Tor path via `torsocks`, watch logs: ```bash docker compose logs -f goatcounter # look for “ignored: bot” (curl UA), or DNT being respected ``` - **DevTools sanity** → on clearnet you should see `https://statsblog…/count` and `…/counter/*.json`; on onion you should see `/count` and `/counter/*.json` (same‑origin). That’s it. One dashboard; clearnet + onion both increment; tiny inline counters everywhere. Honestly, self-hosting GoatCounter wasn't as straightforward as I thought it would be. It almost made me cry a few times. I definitly thought about my life choices a few times along the way. But oh well, now it's done and dusted, and I'm a happy puppy! :D --- {{< sigdl >}}