mysite/public-clearnet/posts/papermod-views-debugging-story/index.html

228 lines
No EOL
69 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html><html lang=en dir=auto><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge"><meta name=viewport content="width=device-width,initial-scale=1,shrink-to-fit=no"><meta name=robots content="noindex, nofollow"><title>A Tiny 'Views' Badge Sent Me Down a Rabbit Hole (PaperMod + Busuanzi + Debugging --> swapped with GoatCounter the GOAT) | AlipourIm journeys</title>
<meta name=keywords content="Hugo,PaperMod,Analytics,Debugging,Busuanzi,Vercount,GoatCounter"><meta name=description content="I tried to add a simple &lsquo;views&rsquo; counter to my PaperMod blog. It worked—until it didnt. Heres the story of blank numbers, a mysterious Chinese message, and the final fix."><meta name=author content="Iman Alipour"><link rel=canonical href=https://blog.alipour.eu/posts/papermod-views-debugging-story/><link crossorigin=anonymous href=/assets/css/stylesheet.31b150d909186c48d6dbbf653acc2489945fa9ac2c8f8cd3fe8448e89e40fccf.css integrity="sha256-MbFQ2QkYbEjW279lOswkiZRfqawsj4zT/oRI6J5A/M8=" rel="preload stylesheet" as=style><link rel=icon href=https://blog.alipour.eu/favicon.ico><link rel=icon type=image/png sizes=16x16 href=https://blog.alipour.eu/favicon-16x16.png><link rel=icon type=image/png sizes=32x32 href=https://blog.alipour.eu/favicon-32x32.png><link rel=apple-touch-icon href=https://blog.alipour.eu/apple-touch-icon.png><link rel=mask-icon href=https://blog.alipour.eu/safari-pinned-tab.svg><meta name=theme-color content="#2e2e33"><meta name=msapplication-TileColor content="#2e2e33"><link rel=alternate hreflang=en href=https://blog.alipour.eu/posts/papermod-views-debugging-story/><noscript><style>#theme-toggle,.top-link{display:none}</style><style>@media(prefers-color-scheme:dark){:root{--theme:rgb(29, 30, 32);--entry:rgb(46, 46, 51);--primary:rgb(218, 218, 219);--secondary:rgb(155, 156, 157);--tertiary:rgb(65, 66, 68);--content:rgb(196, 196, 197);--code-block-bg:rgb(46, 46, 51);--code-bg:rgb(55, 56, 62);--border:rgb(51, 51, 51)}.list{background:var(--theme)}.list:not(.dark)::-webkit-scrollbar-track{background:0 0}.list:not(.dark)::-webkit-scrollbar-thumb{border-color:var(--theme)}}</style></noscript><script>(function(){window.goatcounter={endpoint:"/count"};const e=document.createElement("script");e.async=!0,e.src="/js/count.js",document.head.appendChild(e)})()</script><script>(function(){const s="",t=new Intl.NumberFormat,o="visit",i="visits";function a(e){document.readyState==="complete"||document.readyState==="interactive"?setTimeout(e,0):document.addEventListener("DOMContentLoaded",e)}function r(){try{return window.goatcounter&&window.goatcounter.get_data?window.goatcounter.get_data().p:location.pathname}catch{return location.pathname}}async function e(e,t){try{const o=new URLSearchParams({t:String(Date.now())});t&&o.set("start",t);const a=`${s}/counter/${encodeURIComponent(e)}.json?${o}`,n=await fetch(a,{credentials:"omit"});if(n.status===404)return 0;if(!n.ok)throw 0;const r=await n.json(),i=Number(String(r.count).replace(/,/g,""));return Number.isFinite(i)?i:0}catch{return null}}const n=e=>e===null?"—":`${t.format(e)} ${e===1?o:i}`;a(async function(){const s=document.querySelector(".post-single");if(s){const t=s.querySelector(".post-header .post-meta, .post-header .entry-meta, .post-meta"),o=await e(r());if(t){let e=t.querySelector(".post-views");e||(e=document.createElement("span"),e.className="post-views",t.appendChild(e)),e.textContent=n(o??0)}}const i=document.querySelectorAll("article.post-entry");await Promise.all(Array.from(i).map(async t=>{const s=t.querySelector(".entry-footer, .post-meta"),o=t.querySelector("a.entry-link, h2 a, .entry-title a");if(!s||!o)return;try{const i=new URL(o.getAttribute("href"),location.origin);if(i.origin!==location.origin)return;let t=s.querySelector(".post-views");t||(t=document.createElement("span"),t.className="post-views",s.appendChild(t)),t.textContent=n(await e(i.pathname)??0)}catch{}}));const o=document.getElementById("gc_site_visitors_week");if(o){const n=await e("TOTAL","week");o.textContent=n===null?"—":t.format(n)}})})()</script></head><body id=top><script>localStorage.getItem("pref-theme")==="dark"?document.body.classList.add("dark"):localStorage.getItem("pref-theme")==="light"?document.body.classList.remove("dark"):window.matchMedia("(prefers-color-scheme: dark)").matches&&document.body.classList.add("dark")</script><header class=header><nav class=nav><div class=logo><a href=https://blog.alipour.eu/ accesskey=h title="AlipourIm journeys (Alt + H)">AlipourIm journeys</a><div class=logo-switches><button id=theme-toggle accesskey=t title="(Alt + T)" aria-label="Toggle theme"><svg id="moon" width="24" height="18" viewBox="0 0 24 24" fill="none" stroke="currentcolor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"/></svg><svg id="sun" width="24" height="18" viewBox="0 0 24 24" fill="none" stroke="currentcolor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg></button></div></div><ul id=menu><li><a href=https://blog.alipour.eu/posts/ title=Posts><span>Posts</span></a></li><li><a href=https://blog.alipour.eu/phd_journey/ title=PhD_journey><span>PhD_journey</span></a></li><li><a href=https://blog.alipour.eu/about/ title=About><span>About</span></a></li></ul></nav></header><main class=main><article class=post-single><header class=post-header><div class=breadcrumbs><a href=https://blog.alipour.eu/>Home</a>&nbsp;»&nbsp;<a href=https://blog.alipour.eu/posts/>Posts</a></div><h1 class="post-title entry-hint-parent">A Tiny 'Views' Badge Sent Me Down a Rabbit Hole (PaperMod + Busuanzi + Debugging --> swapped with GoatCounter the GOAT)</h1><div class=post-meta><span title='2025-10-18 10:45:00 +0000 UTC'>October 18, 2025</span>&nbsp;·&nbsp;9 min&nbsp;·&nbsp;Iman Alipour
<span class=post-meta-item><a href=https://blog.alipour.eu/posts/papermod-views-debugging-story/#isso-thread class=isso-comments-link>Comments</a></span></div></header><div class=post-content><p>I only wanted a tiny <strong>“views”</strong> 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.</p><p>The thing is, I don&rsquo;t think the counter even works correctly.
Or maybe I&rsquo;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&rsquo;ve been counting days for.</p><h3 id=the-neat-footer-row>The neat footer row<a hidden class=anchor aria-hidden=true href=#the-neat-footer-row>#</a></h3><p>First I got the layout right. PaperMod supports extension partials, so I used a simple flex row to keep things side by side:</p><div class=highlight><pre tabindex=0 style=color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-html data-lang=html><span style=display:flex><span>&lt;<span style=color:#f92672>div</span> <span style=color:#a6e22e>class</span><span style=color:#f92672>=</span><span style=color:#e6db74>&#34;site-meta-row&#34;</span> <span style=color:#a6e22e>style</span><span style=color:#f92672>=</span><span style=color:#e6db74>&#34;display:flex;align-items:center;gap:.75rem;flex-wrap:wrap;&#34;</span>&gt;
</span></span><span style=display:flex><span> &lt;<span style=color:#f92672>a</span> <span style=color:#a6e22e>href</span><span style=color:#f92672>=</span><span style=color:#e6db74>&#34;/index.xml&#34;</span> <span style=color:#a6e22e>rel</span><span style=color:#f92672>=</span><span style=color:#e6db74>&#34;alternate&#34;</span> <span style=color:#a6e22e>type</span><span style=color:#f92672>=</span><span style=color:#e6db74>&#34;application/rss+xml&#34;</span> <span style=color:#a6e22e>title</span><span style=color:#f92672>=</span><span style=color:#e6db74>&#34;RSS&#34;</span>
</span></span><span style=display:flex><span> <span style=color:#a6e22e>class</span><span style=color:#f92672>=</span><span style=color:#e6db74>&#34;rss-link&#34;</span> <span style=color:#a6e22e>style</span><span style=color:#f92672>=</span><span style=color:#e6db74>&#34;display:inline-flex;align-items:center;gap:.35rem;&#34;</span>&gt;
</span></span><span style=display:flex><span> { partial &#34;svg.html&#34; (dict &#34;name&#34; &#34;rss&#34;) }
</span></span><span style=display:flex><span> &lt;<span style=color:#f92672>span</span>&gt;RSS&lt;/<span style=color:#f92672>span</span>&gt;
</span></span><span style=display:flex><span> &lt;/<span style=color:#f92672>a</span>&gt;
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span> &lt;<span style=color:#f92672>span</span> <span style=color:#a6e22e>aria-hidden</span><span style=color:#f92672>=</span><span style=color:#e6db74>&#34;true&#34;</span>&gt;·&lt;/<span style=color:#f92672>span</span>&gt;
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span> &lt;<span style=color:#f92672>span</span> <span style=color:#a6e22e>id</span><span style=color:#f92672>=</span><span style=color:#e6db74>&#34;busuanzi_container_site_pv&#34;</span>&gt;
</span></span><span style=display:flex><span> Site views: &lt;<span style=color:#f92672>span</span> <span style=color:#a6e22e>id</span><span style=color:#f92672>=</span><span style=color:#e6db74>&#34;busuanzi_value_site_pv&#34;</span>&gt;&lt;/<span style=color:#f92672>span</span>&gt;
</span></span><span style=display:flex><span> &lt;/<span style=color:#f92672>span</span>&gt;
</span></span><span style=display:flex><span> &lt;<span style=color:#f92672>span</span> <span style=color:#a6e22e>aria-hidden</span><span style=color:#f92672>=</span><span style=color:#e6db74>&#34;true&#34;</span>&gt;·&lt;/<span style=color:#f92672>span</span>&gt;
</span></span><span style=display:flex><span> &lt;<span style=color:#f92672>span</span> <span style=color:#a6e22e>id</span><span style=color:#f92672>=</span><span style=color:#e6db74>&#34;busuanzi_container_site_uv&#34;</span>&gt;
</span></span><span style=display:flex><span> Visitors: &lt;<span style=color:#f92672>span</span> <span style=color:#a6e22e>id</span><span style=color:#f92672>=</span><span style=color:#e6db74>&#34;busuanzi_value_site_uv&#34;</span>&gt;&lt;/<span style=color:#f92672>span</span>&gt;
</span></span><span style=display:flex><span> &lt;/<span style=color:#f92672>span</span>&gt;
</span></span><span style=display:flex><span>&lt;/<span style=color:#f92672>div</span>&gt;
</span></span></code></pre></div><p>So far, so good. The labels showed up. The numbers, though? <strong>Nothing.</strong> Just an em dash. I shrugged—maybe it was a caching thing.</p><h3 id=the-first-round-of-checks>The first round of checks<a hidden class=anchor aria-hidden=true href=#the-first-round-of-checks>#</a></h3><p>I added the script sitewide in <code>layouts/partials/extend_head.html</code>:</p><div class=highlight><pre tabindex=0 style=color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-html data-lang=html><span style=display:flex><span>&lt;<span style=color:#f92672>script</span> <span style=color:#a6e22e>src</span><span style=color:#f92672>=</span><span style=color:#e6db74>&#34;//cdn.busuanzi.cc/busuanzi/3.6.9/busuanzi.min.js&#34;</span> <span style=color:#a6e22e>defer</span>&gt;&lt;/<span style=color:#f92672>script</span>&gt;
</span></span></code></pre></div><p>Refreshed. Still blank.</p><p><strong>DevTools time.</strong> Network tab: the script <strong>does</strong> load:</p><ul><li><strong>Request URL:</strong> <code>https://cdn.busuanzi.cc/busuanzi/3.6.9/busuanzi.min.js</code></li><li><strong>Request Method:</strong> GET</li><li><strong>Status Code:</strong> 304 Not Modified</li><li><strong>Referrer Policy:</strong> strict-origin-when-cross-origin</li><li><strong>User-Agent:</strong> a normal Chromium build (nothing exotic)</li></ul><p>No red flags. Just a perfectly normal 304.</p><p>Maybe my IDs were wrong? Busuanzi has a couple of flavors: <code>busuanzi_value_*</code> (with container wrappers) or shorthand like <code>busuanzi_site_pv</code>. I tried <strong>both</strong>, carefully, one at a time. Still… <strong></strong>.</p><h3 id=the-odd-message>The odd message<a hidden class=anchor aria-hidden=true href=#the-odd-message>#</a></h3><p>Then it flashed for a moment—the Chinese line that made me google twice to be sure I saw it right:</p><blockquote><p><strong>“域名过长,已被禁用 views”</strong></p></blockquote><p>Thats “domain name too long, disabled.” Wait, what?</p><p>I checked my hostname: <strong><code>blog.alipourimjourneys.ir</code></strong>. I counted: <strong>27 characters</strong>. Busuanzis limit? <strong>22.</strong> Suddenly, everything clicked. The script loads, but the service refuses to count for long domains, so the placeholders never fill.</p><h3 id=two-ways-out>Two ways out<a hidden class=anchor aria-hidden=true href=#two-ways-out>#</a></h3><p>At this point I had two clean options:</p><ol><li><strong>Keep Busuanzi</strong> and move the site to the <strong>apex domain</strong> (short enough).</li><li>Keep my beloved <code>blog.</code> subdomain and swap in <strong>Vercount</strong>, a Busuanzistyle dropin without the 22char limit.</li></ol><p>I liked the subdomain (habit, mostly), so I tried <strong>Vercount</strong>.</p><h3 id=the-swap-five-minute-fix>The swap (five-minute fix)<a hidden class=anchor aria-hidden=true href=#the-swap-five-minute-fix>#</a></h3><p>I removed the Busuanzi script and added Vercount instead:</p><div class=highlight><pre tabindex=0 style=color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-html data-lang=html><span style=display:flex><span><span style=color:#75715e>&lt;!-- extend_head.html --&gt;</span>
</span></span><span style=display:flex><span>&lt;<span style=color:#f92672>script</span> <span style=color:#a6e22e>defer</span> <span style=color:#a6e22e>src</span><span style=color:#f92672>=</span><span style=color:#e6db74>&#34;https://events.vercount.one/js&#34;</span>&gt;&lt;/<span style=color:#f92672>script</span>&gt;
</span></span></code></pre></div><p>I kept the same tidy footer row, but switched the IDs to Vercounts:</p><div class=highlight><pre tabindex=0 style=color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-html data-lang=html><span style=display:flex><span>&lt;<span style=color:#f92672>div</span> <span style=color:#a6e22e>class</span><span style=color:#f92672>=</span><span style=color:#e6db74>&#34;site-meta-row&#34;</span> <span style=color:#a6e22e>style</span><span style=color:#f92672>=</span><span style=color:#e6db74>&#34;display:flex;align-items:center;gap:.75rem;flex-wrap:wrap;&#34;</span>&gt;
</span></span><span style=display:flex><span> &lt;<span style=color:#f92672>a</span> <span style=color:#a6e22e>href</span><span style=color:#f92672>=</span><span style=color:#e6db74>&#34;/index.xml&#34;</span> <span style=color:#a6e22e>rel</span><span style=color:#f92672>=</span><span style=color:#e6db74>&#34;alternate&#34;</span> <span style=color:#a6e22e>type</span><span style=color:#f92672>=</span><span style=color:#e6db74>&#34;application/rss+xml&#34;</span> <span style=color:#a6e22e>title</span><span style=color:#f92672>=</span><span style=color:#e6db74>&#34;RSS&#34;</span>
</span></span><span style=display:flex><span> <span style=color:#a6e22e>class</span><span style=color:#f92672>=</span><span style=color:#e6db74>&#34;rss-link&#34;</span> <span style=color:#a6e22e>style</span><span style=color:#f92672>=</span><span style=color:#e6db74>&#34;display:inline-flex;align-items:center;gap:.35rem;&#34;</span>&gt;
</span></span><span style=display:flex><span> { partial &#34;svg.html&#34; (dict &#34;name&#34; &#34;rss&#34;) }
</span></span><span style=display:flex><span> &lt;<span style=color:#f92672>span</span>&gt;RSS&lt;/<span style=color:#f92672>span</span>&gt;
</span></span><span style=display:flex><span> &lt;/<span style=color:#f92672>a</span>&gt;
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span> &lt;<span style=color:#f92672>span</span> <span style=color:#a6e22e>aria-hidden</span><span style=color:#f92672>=</span><span style=color:#e6db74>&#34;true&#34;</span>&gt;·&lt;/<span style=color:#f92672>span</span>&gt;
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span> &lt;<span style=color:#f92672>span</span>&gt;Site views: &lt;<span style=color:#f92672>span</span> <span style=color:#a6e22e>id</span><span style=color:#f92672>=</span><span style=color:#e6db74>&#34;vercount_value_site_pv&#34;</span>&gt;&lt;/<span style=color:#f92672>span</span>&gt;&lt;/<span style=color:#f92672>span</span>&gt;
</span></span><span style=display:flex><span> &lt;<span style=color:#f92672>span</span> <span style=color:#a6e22e>aria-hidden</span><span style=color:#f92672>=</span><span style=color:#e6db74>&#34;true&#34;</span>&gt;·&lt;/<span style=color:#f92672>span</span>&gt;
</span></span><span style=display:flex><span> &lt;<span style=color:#f92672>span</span>&gt;Visitors: &lt;<span style=color:#f92672>span</span> <span style=color:#a6e22e>id</span><span style=color:#f92672>=</span><span style=color:#e6db74>&#34;vercount_value_site_uv&#34;</span>&gt;&lt;/<span style=color:#f92672>span</span>&gt;&lt;/<span style=color:#f92672>span</span>&gt;
</span></span><span style=display:flex><span>&lt;/<span style=color:#f92672>div</span>&gt;
</span></span></code></pre></div><p>For perpost counts (in the post meta), I added:</p><div class=highlight><pre tabindex=0 style=color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-html data-lang=html><span style=display:flex><span>&lt;<span style=color:#f92672>span</span> <span style=color:#a6e22e>id</span><span style=color:#f92672>=</span><span style=color:#e6db74>&#34;vercount_value_page_pv&#34;</span>&gt;&lt;/<span style=color:#f92672>span</span>&gt; views
</span></span></code></pre></div><p>Deploy. Refresh. <strong>Numbers.</strong></p><h3 id=but-if-you-want-to-stay-with-busuanzi>But if you want to stay with Busuanzi…<a hidden class=anchor aria-hidden=true href=#but-if-you-want-to-stay-with-busuanzi>#</a></h3><p>If your apex domain is short enough, pointing the site there works fine. The gist:</p><ul><li><p>Set <code>baseURL</code> in Hugo to the apex:</p><div class=highlight><pre tabindex=0 style=color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-toml data-lang=toml><span style=display:flex><span><span style=color:#a6e22e>baseURL</span> = <span style=color:#e6db74>&#34;https://alipourimjourneys.ir/&#34;</span>
</span></span></code></pre></div></li><li><p>Add a 301 redirect from the long subdomain to the apex (Cloudflare, Netlify, Nginx—pick your tool).</p></li><li><p>Keep your Busuanzi spans as you had them; once the host fits the limit, theyll populate.</p></li></ul><h3 id=postmortem-checklist-aka-things-i-learned>Postmortem checklist (a.k.a. things I learned)<a hidden class=anchor aria-hidden=true href=#postmortem-checklist-aka-things-i-learned>#</a></h3><ul><li><strong>If the script loads but you get blanks</strong>, its not always IDs or caching. Sometimes its a <strong>service rule</strong>.</li><li><strong>Busuanzi refuses long hostnames</strong> (>22 chars) and ignores localhost. On those hosts, the placeholders stay empty or show the Chinese “disabled” note.</li><li><strong>Dont mix providers</strong> on the same page. Load exactly one script (Busuanzi <em>or</em> Vercount).</li><li><strong>CSP and blockers matter.</strong> If you run a strict ContentSecurityPolicy or heavy ad blockers, they can block the script. Test in a clean profile and allow the script origin.</li></ul><h3 id=happy-ending>Happy ending<a hidden class=anchor aria-hidden=true href=#happy-ending>#</a></h3><p>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.</p><p>If you hit the same wall, check the hostname length first. It might save you an afternoon—and lead you to a solution youll feel weirdly proud of.</p><h3 id=final-notes>Final notes<a hidden class=anchor aria-hidden=true href=#final-notes>#</a></h3><p>Yea&mldr; I just felt this feature was missing, and it&rsquo;s good to have it there.
I hope I stop adding things to the blog, and just keep writing in it.
I&rsquo;ve spent too much time on it, and it feels like wasted time.</p><hr><h2 id=change-of-plans-selfhosting-goatcounter-for-papermod-docker--cloudflare--onion>Change of plans: Selfhosting GoatCounter for PaperMod (Docker + Cloudflare + Onion)<a hidden class=anchor aria-hidden=true href=#change-of-plans-selfhosting-goatcounter-for-papermod-docker--cloudflare--onion>#</a></h2><p>This is the <strong>selfhost</strong> part I ended up with: Docker for GoatCounter, Cloudflare (orangecloud) in front, Nginx on the server as reverse proxy, plus my <code>.onion</code> mirror counting to the same GoatCounter site. Paste this whole section at the end of the post.</p><hr><h3 id=1-clean-up-old-counters-optional-once>1) Clean up old counters (optional, once)<a hidden class=anchor aria-hidden=true href=#1-clean-up-old-counters-optional-once>#</a></h3><div class=highlight><pre tabindex=0 style=color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-bash data-lang=bash><span style=display:flex><span><span style=color:#75715e># From your Hugo site root</span>
</span></span><span style=display:flex><span>grep -RinE <span style=color:#e6db74>&#34;busuanzi|vercount|vercount_value_|busuanzi_value_&#34;</span> .
</span></span></code></pre></div><p>Remove any matching <code>&lt;script></code> and leftover <code>*_value_*</code> spans you no longer want.</p><hr><h3 id=2-run-goatcounter-with-docker-compose>2) Run GoatCounter with Docker Compose<a hidden class=anchor aria-hidden=true href=#2-run-goatcounter-with-docker-compose>#</a></h3><p>Minimal <code>docker-compose.yml</code> in <code>~/goatcounter</code>:</p><div class=highlight><pre tabindex=0 style=color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-yaml data-lang=yaml><span style=display:flex><span><span style=color:#f92672>services</span>:
</span></span><span style=display:flex><span> <span style=color:#f92672>goatcounter</span>:
</span></span><span style=display:flex><span> <span style=color:#f92672>image</span>: <span style=color:#ae81ff>arp242/goatcounter:2.6</span>
</span></span><span style=display:flex><span> <span style=color:#f92672>container_name</span>: <span style=color:#ae81ff>goatcounter</span>
</span></span><span style=display:flex><span> <span style=color:#f92672>ports</span>:
</span></span><span style=display:flex><span> - <span style=color:#e6db74>&#34;127.0.0.1:8081:8080&#34;</span> <span style=color:#75715e># bind only to localhost; nginx will proxy</span>
</span></span><span style=display:flex><span> <span style=color:#f92672>volumes</span>:
</span></span><span style=display:flex><span> - <span style=color:#ae81ff>goatcounter-data:/home/goatcounter/goatcounter-data</span>
</span></span><span style=display:flex><span> <span style=color:#f92672>restart</span>: <span style=color:#ae81ff>unless-stopped</span>
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span><span style=color:#f92672>volumes</span>:
</span></span><span style=display:flex><span> <span style=color:#f92672>goatcounter-data</span>: {}
</span></span></code></pre></div><p>Start:</p><div class=highlight><pre tabindex=0 style=color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-bash data-lang=bash><span style=display:flex><span>docker compose pull <span style=color:#f92672>&amp;&amp;</span> docker compose up -d
</span></span></code></pre></div><p>Initialize the site (replace email if needed):</p><div class=highlight><pre tabindex=0 style=color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-bash data-lang=bash><span style=display:flex><span>docker compose exec goatcounter goatcounter db create site -vhost<span style=color:#f92672>=</span>statsblog.alipourimjourneys.ir -user.email<span style=color:#f92672>=</span>iman.alip2001@gmail.com
</span></span></code></pre></div><blockquote><p>If you ever see “no site at this domain” on localhost: use the exact <strong>vhost</strong> above.</p></blockquote><hr><h3 id=3-cloudflare--origin-certificate-cf--origin-tls>3) Cloudflare + Origin certificate (CF → origin TLS)<a hidden class=anchor aria-hidden=true href=#3-cloudflare--origin-certificate-cf--origin-tls>#</a></h3><ul><li>DNS: <code>statsblog.alipourimjourneys.ir</code> → server IP (<strong>proxied/orange</strong>).</li><li>SSL/TLS mode: <strong>Full (strict)</strong>.</li><li>Issue a <strong>Cloudflare Origin Certificate</strong> for <code>statsblog.alipourimjourneys.ir</code> and save on the server:</li></ul><div class=highlight><pre tabindex=0 style=color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-bash data-lang=bash><span style=display:flex><span>sudo mkdir -p /etc/ssl/cloudflare
</span></span><span style=display:flex><span>sudo nano /etc/ssl/cloudflare/origin.crt <span style=color:#75715e># paste PEM</span>
</span></span><span style=display:flex><span>sudo nano /etc/ssl/cloudflare/origin.key <span style=color:#75715e># paste key</span>
</span></span><span style=display:flex><span>sudo chown root:root /etc/ssl/cloudflare/origin.<span style=color:#f92672>{</span>crt,key<span style=color:#f92672>}</span>
</span></span><span style=display:flex><span>sudo chmod <span style=color:#ae81ff>644</span> /etc/ssl/cloudflare/origin.crt
</span></span><span style=display:flex><span>sudo chmod <span style=color:#ae81ff>600</span> /etc/ssl/cloudflare/origin.key
</span></span></code></pre></div><hr><h3 id=4-nginx-vhost-for-the-stats-subdomain>4) Nginx vhost for the stats subdomain<a hidden class=anchor aria-hidden=true href=#4-nginx-vhost-for-the-stats-subdomain>#</a></h3><div class=highlight><pre tabindex=0 style=color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-nginx data-lang=nginx><span style=display:flex><span><span style=color:#75715e># /etc/nginx/sites-available/statsblog.alipourimjourneys.ir.conf
</span></span></span><span style=display:flex><span><span style=color:#75715e></span><span style=color:#66d9ef>server</span> {
</span></span><span style=display:flex><span> <span style=color:#f92672>listen</span> <span style=color:#ae81ff>80</span>;
</span></span><span style=display:flex><span> <span style=color:#f92672>server_name</span> <span style=color:#e6db74>statsblog.alipourimjourneys.ir</span>;
</span></span><span style=display:flex><span> <span style=color:#f92672>return</span> <span style=color:#ae81ff>301</span> <span style=color:#e6db74>https://</span>$host$request_uri;
</span></span><span style=display:flex><span>}
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span><span style=color:#66d9ef>server</span> {
</span></span><span style=display:flex><span> <span style=color:#f92672>listen</span> <span style=color:#ae81ff>443</span> <span style=color:#e6db74>ssl</span> <span style=color:#e6db74>http2</span>;
</span></span><span style=display:flex><span> <span style=color:#f92672>server_name</span> <span style=color:#e6db74>statsblog.alipourimjourneys.ir</span>;
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span> <span style=color:#f92672>ssl_certificate</span> <span style=color:#e6db74>/etc/ssl/cloudflare/origin.crt</span>;
</span></span><span style=display:flex><span> <span style=color:#f92672>ssl_certificate_key</span> <span style=color:#e6db74>/etc/ssl/cloudflare/origin.key</span>;
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span> <span style=color:#75715e># Proxy to the container on localhost
</span></span></span><span style=display:flex><span><span style=color:#75715e></span> <span style=color:#f92672>proxy_set_header</span> <span style=color:#e6db74>Host</span> $host;
</span></span><span style=display:flex><span> <span style=color:#f92672>proxy_set_header</span> <span style=color:#e6db74>X-Real-IP</span> $remote_addr;
</span></span><span style=display:flex><span> <span style=color:#f92672>proxy_set_header</span> <span style=color:#e6db74>X-Forwarded-For</span> $proxy_add_x_forwarded_for;
</span></span><span style=display:flex><span> <span style=color:#f92672>proxy_set_header</span> <span style=color:#e6db74>X-Forwarded-Proto</span> $scheme;
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span> <span style=color:#f92672>location</span> <span style=color:#e6db74>/</span> { <span style=color:#f92672>proxy_pass</span> <span style=color:#e6db74>http://127.0.0.1:8081</span>; }
</span></span><span style=display:flex><span>}
</span></span></code></pre></div><p>Enable & reload:</p><div class=highlight><pre tabindex=0 style=color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-bash data-lang=bash><span style=display:flex><span>sudo ln -s /etc/nginx/sites-available/statsblog.alipourimjourneys.ir.conf /etc/nginx/sites-enabled/
</span></span><span style=display:flex><span>sudo nginx -t <span style=color:#f92672>&amp;&amp;</span> sudo systemctl reload nginx
</span></span></code></pre></div><hr><h3 id=5-hugo-integration-clearnet--onion-singularplural--0-views>5) Hugo integration (clearnet + onion, singular/plural + “0 views”)<a hidden class=anchor aria-hidden=true href=#5-hugo-integration-clearnet--onion-singularplural--0-views>#</a></h3><p>Selfhost <code>count.js</code> so the onion mirror never fetches a thirdparty script:</p><ul><li>Save the official <code>count.js</code> as <code>static/js/count.js</code> in your Hugo repo.</li></ul><p>Then add this to <code>layouts/partials/extend_head.html</code> <strong>(loader + helper)</strong>:</p><div class=highlight><pre tabindex=0 style=color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-html data-lang=html><span style=display:flex><span><span style=color:#75715e>&lt;!-- Loader: choose endpoint based on hostname --&gt;</span>
</span></span><span style=display:flex><span>&lt;<span style=color:#f92672>script</span>&gt;
</span></span><span style=display:flex><span>(<span style=color:#66d9ef>function</span> () {
</span></span><span style=display:flex><span> <span style=color:#66d9ef>var</span> <span style=color:#a6e22e>ONION</span> <span style=color:#f92672>=</span> <span style=color:#e6db74>/\.onion$/i</span>.<span style=color:#a6e22e>test</span>(<span style=color:#a6e22e>location</span>.<span style=color:#a6e22e>hostname</span>);
</span></span><span style=display:flex><span> window.<span style=color:#a6e22e>goatcounter</span> <span style=color:#f92672>=</span> { <span style=color:#a6e22e>endpoint</span><span style=color:#f92672>:</span> <span style=color:#a6e22e>ONION</span> <span style=color:#f92672>?</span> <span style=color:#e6db74>&#39;/count&#39;</span> <span style=color:#f92672>:</span> <span style=color:#e6db74>&#39;https://statsblog.alipourimjourneys.ir/count&#39;</span> };
</span></span><span style=display:flex><span> <span style=color:#66d9ef>var</span> <span style=color:#a6e22e>s</span> <span style=color:#f92672>=</span> document.<span style=color:#a6e22e>createElement</span>(<span style=color:#e6db74>&#39;script&#39;</span>);
</span></span><span style=display:flex><span> <span style=color:#a6e22e>s</span>.<span style=color:#66d9ef>async</span> <span style=color:#f92672>=</span> <span style=color:#66d9ef>true</span>; <span style=color:#a6e22e>s</span>.<span style=color:#a6e22e>src</span> <span style=color:#f92672>=</span> <span style=color:#e6db74>&#39;/js/count.js&#39;</span>;
</span></span><span style=display:flex><span> document.<span style=color:#a6e22e>head</span>.<span style=color:#a6e22e>appendChild</span>(<span style=color:#a6e22e>s</span>);
</span></span><span style=display:flex><span>})();
</span></span><span style=display:flex><span>&lt;/<span style=color:#f92672>script</span>&gt;
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span><span style=color:#75715e>&lt;!-- Helper: inline meta counts (post pages + lists) and totals --&gt;</span>
</span></span><span style=display:flex><span>&lt;<span style=color:#f92672>script</span>&gt;
</span></span><span style=display:flex><span>(<span style=color:#66d9ef>function</span> () {
</span></span><span style=display:flex><span> <span style=color:#66d9ef>const</span> <span style=color:#a6e22e>ONION</span> <span style=color:#f92672>=</span> <span style=color:#e6db74>/\.onion$/i</span>.<span style=color:#a6e22e>test</span>(<span style=color:#a6e22e>location</span>.<span style=color:#a6e22e>hostname</span>);
</span></span><span style=display:flex><span> <span style=color:#66d9ef>const</span> <span style=color:#a6e22e>GC_HOST</span> <span style=color:#f92672>=</span> <span style=color:#a6e22e>ONION</span> <span style=color:#f92672>?</span> <span style=color:#e6db74>&#39;&#39;</span> <span style=color:#f92672>:</span> <span style=color:#e6db74>&#39;https://statsblog.alipourimjourneys.ir&#39;</span>; <span style=color:#75715e>// &#39;&#39; = same-origin on .onion
</span></span></span><span style=display:flex><span><span style=color:#75715e></span> <span style=color:#66d9ef>const</span> <span style=color:#a6e22e>SING</span><span style=color:#f92672>=</span><span style=color:#e6db74>&#39;view&#39;</span>, <span style=color:#a6e22e>PLUR</span><span style=color:#f92672>=</span><span style=color:#e6db74>&#39;views&#39;</span>;
</span></span><span style=display:flex><span> <span style=color:#66d9ef>const</span> <span style=color:#a6e22e>nf</span> <span style=color:#f92672>=</span> <span style=color:#66d9ef>new</span> <span style=color:#a6e22e>Intl</span>.<span style=color:#a6e22e>NumberFormat</span>();
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span> <span style=color:#66d9ef>function</span> <span style=color:#a6e22e>ready</span>(<span style=color:#a6e22e>fn</span>){ <span style=color:#66d9ef>if</span> (document.<span style=color:#a6e22e>readyState</span> <span style=color:#f92672>!==</span> <span style=color:#e6db74>&#39;loading&#39;</span>) <span style=color:#a6e22e>fn</span>(); <span style=color:#66d9ef>else</span> document.<span style=color:#a6e22e>addEventListener</span>(<span style=color:#e6db74>&#39;DOMContentLoaded&#39;</span>, <span style=color:#a6e22e>fn</span>); }
</span></span><span style=display:flex><span> <span style=color:#66d9ef>function</span> <span style=color:#a6e22e>gcPath</span>(){ <span style=color:#66d9ef>try</span> { <span style=color:#66d9ef>return</span> (window.<span style=color:#a6e22e>goatcounter</span> <span style=color:#f92672>&amp;&amp;</span> window.<span style=color:#a6e22e>goatcounter</span>.<span style=color:#a6e22e>get_data</span>) <span style=color:#f92672>?</span> window.<span style=color:#a6e22e>goatcounter</span>.<span style=color:#a6e22e>get_data</span>().<span style=color:#a6e22e>p</span> <span style=color:#f92672>:</span> <span style=color:#a6e22e>location</span>.<span style=color:#a6e22e>pathname</span>; } <span style=color:#66d9ef>catch</span> { <span style=color:#66d9ef>return</span> <span style=color:#a6e22e>location</span>.<span style=color:#a6e22e>pathname</span>; } }
</span></span><span style=display:flex><span> <span style=color:#66d9ef>async</span> <span style=color:#66d9ef>function</span> <span style=color:#a6e22e>fetchCount</span>(<span style=color:#a6e22e>path</span>){
</span></span><span style=display:flex><span> <span style=color:#66d9ef>try</span> {
</span></span><span style=display:flex><span> <span style=color:#66d9ef>const</span> <span style=color:#a6e22e>r</span> <span style=color:#f92672>=</span> <span style=color:#66d9ef>await</span> <span style=color:#a6e22e>fetch</span>(<span style=color:#e6db74>`</span><span style=color:#e6db74>${</span><span style=color:#a6e22e>GC_HOST</span><span style=color:#e6db74>}</span><span style=color:#e6db74>/counter/</span><span style=color:#e6db74>${</span>encodeURIComponent(<span style=color:#a6e22e>path</span>)<span style=color:#e6db74>}</span><span style=color:#e6db74>.json?t=</span><span style=color:#e6db74>${</span>Date.<span style=color:#a6e22e>now</span>()<span style=color:#e6db74>}</span><span style=color:#e6db74>`</span>, { <span style=color:#a6e22e>credentials</span><span style=color:#f92672>:</span><span style=color:#e6db74>&#39;omit&#39;</span> });
</span></span><span style=display:flex><span> <span style=color:#66d9ef>if</span> (<span style=color:#a6e22e>r</span>.<span style=color:#a6e22e>status</span> <span style=color:#f92672>===</span> <span style=color:#ae81ff>404</span>) <span style=color:#66d9ef>return</span> <span style=color:#ae81ff>0</span>;
</span></span><span style=display:flex><span> <span style=color:#66d9ef>if</span> (<span style=color:#f92672>!</span><span style=color:#a6e22e>r</span>.<span style=color:#a6e22e>ok</span>) <span style=color:#66d9ef>throw</span> <span style=color:#ae81ff>0</span>;
</span></span><span style=display:flex><span> <span style=color:#66d9ef>const</span> <span style=color:#a6e22e>j</span> <span style=color:#f92672>=</span> <span style=color:#66d9ef>await</span> <span style=color:#a6e22e>r</span>.<span style=color:#a6e22e>json</span>();
</span></span><span style=display:flex><span> <span style=color:#66d9ef>const</span> <span style=color:#a6e22e>n</span> <span style=color:#f92672>=</span> Number(String(<span style=color:#a6e22e>j</span>.<span style=color:#a6e22e>count</span>).<span style=color:#a6e22e>replace</span>(<span style=color:#e6db74>/,/g</span>,<span style=color:#e6db74>&#39;&#39;</span>));
</span></span><span style=display:flex><span> <span style=color:#66d9ef>return</span> Number.isFinite(<span style=color:#a6e22e>n</span>) <span style=color:#f92672>?</span> <span style=color:#a6e22e>n</span> <span style=color:#f92672>:</span> <span style=color:#ae81ff>0</span>;
</span></span><span style=display:flex><span> } <span style=color:#66d9ef>catch</span> { <span style=color:#66d9ef>return</span> <span style=color:#66d9ef>null</span>; }
</span></span><span style=display:flex><span> }
</span></span><span style=display:flex><span> <span style=color:#66d9ef>const</span> <span style=color:#a6e22e>label</span> <span style=color:#f92672>=</span> (<span style=color:#a6e22e>n</span>) =&gt; (<span style=color:#a6e22e>n</span> <span style=color:#f92672>===</span> <span style=color:#66d9ef>null</span> <span style=color:#f92672>?</span> <span style=color:#e6db74>&#39;&#39;</span> <span style=color:#f92672>:</span> <span style=color:#e6db74>`</span><span style=color:#e6db74>${</span><span style=color:#a6e22e>nf</span>.<span style=color:#a6e22e>format</span>(<span style=color:#a6e22e>n</span>)<span style=color:#e6db74>}</span><span style=color:#e6db74> </span><span style=color:#e6db74>${</span><span style=color:#a6e22e>n</span> <span style=color:#f92672>===</span> <span style=color:#ae81ff>1</span> <span style=color:#f92672>?</span> <span style=color:#a6e22e>SING</span> <span style=color:#f92672>:</span> <span style=color:#a6e22e>PLUR</span><span style=color:#e6db74>}</span><span style=color:#e6db74>`</span>);
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span> <span style=color:#a6e22e>ready</span>(<span style=color:#66d9ef>async</span> <span style=color:#66d9ef>function</span> () {
</span></span><span style=display:flex><span> <span style=color:#75715e>// A) Single post: add to existing meta row + small bottom
</span></span></span><span style=display:flex><span><span style=color:#75715e></span> <span style=color:#66d9ef>const</span> <span style=color:#a6e22e>post</span> <span style=color:#f92672>=</span> document.<span style=color:#a6e22e>querySelector</span>(<span style=color:#e6db74>&#39;.post-single&#39;</span>);
</span></span><span style=display:flex><span> <span style=color:#66d9ef>if</span> (<span style=color:#a6e22e>post</span>) {
</span></span><span style=display:flex><span> <span style=color:#66d9ef>const</span> <span style=color:#a6e22e>meta</span> <span style=color:#f92672>=</span> <span style=color:#a6e22e>post</span>.<span style=color:#a6e22e>querySelector</span>(<span style=color:#e6db74>&#39;.post-header .post-meta, .post-header .entry-meta, .post-meta&#39;</span>);
</span></span><span style=display:flex><span> <span style=color:#66d9ef>const</span> <span style=color:#a6e22e>path</span> <span style=color:#f92672>=</span> <span style=color:#a6e22e>gcPath</span>();
</span></span><span style=display:flex><span> <span style=color:#66d9ef>const</span> <span style=color:#a6e22e>n</span> <span style=color:#f92672>=</span> <span style=color:#66d9ef>await</span> <span style=color:#a6e22e>fetchCount</span>(<span style=color:#a6e22e>path</span>);
</span></span><span style=display:flex><span> <span style=color:#66d9ef>if</span> (<span style=color:#a6e22e>meta</span>) {
</span></span><span style=display:flex><span> <span style=color:#66d9ef>let</span> <span style=color:#a6e22e>span</span> <span style=color:#f92672>=</span> <span style=color:#a6e22e>meta</span>.<span style=color:#a6e22e>querySelector</span>(<span style=color:#e6db74>&#39;.post-views&#39;</span>); <span style=color:#66d9ef>if</span> (<span style=color:#f92672>!</span><span style=color:#a6e22e>span</span>) { <span style=color:#a6e22e>span</span> <span style=color:#f92672>=</span> document.<span style=color:#a6e22e>createElement</span>(<span style=color:#e6db74>&#39;span&#39;</span>); <span style=color:#a6e22e>span</span>.<span style=color:#a6e22e>className</span><span style=color:#f92672>=</span><span style=color:#e6db74>&#39;post-views&#39;</span>; <span style=color:#a6e22e>meta</span>.<span style=color:#a6e22e>appendChild</span>(<span style=color:#a6e22e>span</span>); }
</span></span><span style=display:flex><span> <span style=color:#a6e22e>span</span>.<span style=color:#a6e22e>textContent</span> <span style=color:#f92672>=</span> <span style=color:#a6e22e>label</span>(<span style=color:#a6e22e>n</span> <span style=color:#f92672>??</span> <span style=color:#ae81ff>0</span>);
</span></span><span style=display:flex><span> }
</span></span><span style=display:flex><span> <span style=color:#66d9ef>const</span> <span style=color:#a6e22e>after</span> <span style=color:#f92672>=</span> <span style=color:#a6e22e>post</span>.<span style=color:#a6e22e>querySelector</span>(<span style=color:#e6db74>&#39;.post-content&#39;</span>) <span style=color:#f92672>||</span> <span style=color:#a6e22e>post</span>;
</span></span><span style=display:flex><span> <span style=color:#66d9ef>let</span> <span style=color:#a6e22e>bottom</span> <span style=color:#f92672>=</span> <span style=color:#a6e22e>post</span>.<span style=color:#a6e22e>querySelector</span>(<span style=color:#e6db74>&#39;.post-views-bottom&#39;</span>); <span style=color:#66d9ef>if</span> (<span style=color:#f92672>!</span><span style=color:#a6e22e>bottom</span>) { <span style=color:#a6e22e>bottom</span> <span style=color:#f92672>=</span> document.<span style=color:#a6e22e>createElement</span>(<span style=color:#e6db74>&#39;div&#39;</span>); <span style=color:#a6e22e>bottom</span>.<span style=color:#a6e22e>className</span><span style=color:#f92672>=</span><span style=color:#e6db74>&#39;post-views-bottom&#39;</span>; <span style=color:#a6e22e>after</span>.<span style=color:#a6e22e>insertAdjacentElement</span>(<span style=color:#e6db74>&#39;afterend&#39;</span>, <span style=color:#a6e22e>bottom</span>); }
</span></span><span style=display:flex><span> <span style=color:#a6e22e>bottom</span>.<span style=color:#a6e22e>textContent</span> <span style=color:#f92672>=</span> <span style=color:#a6e22e>label</span>(<span style=color:#a6e22e>n</span> <span style=color:#f92672>??</span> <span style=color:#ae81ff>0</span>);
</span></span><span style=display:flex><span> }
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span> <span style=color:#75715e>// B) Lists (/posts, home, sections): show “0 views” instead of hiding
</span></span></span><span style=display:flex><span><span style=color:#75715e></span> <span style=color:#66d9ef>const</span> <span style=color:#a6e22e>cards</span> <span style=color:#f92672>=</span> document.<span style=color:#a6e22e>querySelectorAll</span>(<span style=color:#e6db74>&#39;article.post-entry&#39;</span>);
</span></span><span style=display:flex><span> <span style=color:#66d9ef>await</span> Promise.<span style=color:#a6e22e>all</span>(Array.<span style=color:#a6e22e>from</span>(<span style=color:#a6e22e>cards</span>).<span style=color:#a6e22e>map</span>(<span style=color:#66d9ef>async</span> (<span style=color:#a6e22e>card</span>) =&gt; {
</span></span><span style=display:flex><span> <span style=color:#66d9ef>const</span> <span style=color:#a6e22e>meta</span> <span style=color:#f92672>=</span> <span style=color:#a6e22e>card</span>.<span style=color:#a6e22e>querySelector</span>(<span style=color:#e6db74>&#39;.entry-footer, .post-meta&#39;</span>);
</span></span><span style=display:flex><span> <span style=color:#66d9ef>const</span> <span style=color:#a6e22e>link</span> <span style=color:#f92672>=</span> <span style=color:#a6e22e>card</span>.<span style=color:#a6e22e>querySelector</span>(<span style=color:#e6db74>&#39;a.entry-link, h2 a, .entry-title a&#39;</span>);
</span></span><span style=display:flex><span> <span style=color:#66d9ef>if</span> (<span style=color:#f92672>!</span><span style=color:#a6e22e>meta</span> <span style=color:#f92672>||</span> <span style=color:#f92672>!</span><span style=color:#a6e22e>link</span>) <span style=color:#66d9ef>return</span>;
</span></span><span style=display:flex><span> <span style=color:#66d9ef>try</span> {
</span></span><span style=display:flex><span> <span style=color:#66d9ef>const</span> <span style=color:#a6e22e>u</span> <span style=color:#f92672>=</span> <span style=color:#66d9ef>new</span> <span style=color:#a6e22e>URL</span>(<span style=color:#a6e22e>link</span>.<span style=color:#a6e22e>getAttribute</span>(<span style=color:#e6db74>&#39;href&#39;</span>), <span style=color:#a6e22e>location</span>.<span style=color:#a6e22e>origin</span>);
</span></span><span style=display:flex><span> <span style=color:#66d9ef>if</span> (<span style=color:#a6e22e>u</span>.<span style=color:#a6e22e>origin</span> <span style=color:#f92672>!==</span> <span style=color:#a6e22e>location</span>.<span style=color:#a6e22e>origin</span>) <span style=color:#66d9ef>return</span>;
</span></span><span style=display:flex><span> <span style=color:#66d9ef>let</span> <span style=color:#a6e22e>span</span> <span style=color:#f92672>=</span> <span style=color:#a6e22e>meta</span>.<span style=color:#a6e22e>querySelector</span>(<span style=color:#e6db74>&#39;.post-views&#39;</span>); <span style=color:#66d9ef>if</span> (<span style=color:#f92672>!</span><span style=color:#a6e22e>span</span>) { <span style=color:#a6e22e>span</span> <span style=color:#f92672>=</span> document.<span style=color:#a6e22e>createElement</span>(<span style=color:#e6db74>&#39;span&#39;</span>); <span style=color:#a6e22e>span</span>.<span style=color:#a6e22e>className</span><span style=color:#f92672>=</span><span style=color:#e6db74>&#39;post-views&#39;</span>; <span style=color:#a6e22e>meta</span>.<span style=color:#a6e22e>appendChild</span>(<span style=color:#a6e22e>span</span>); }
</span></span><span style=display:flex><span> <span style=color:#66d9ef>const</span> <span style=color:#a6e22e>n</span> <span style=color:#f92672>=</span> <span style=color:#66d9ef>await</span> <span style=color:#a6e22e>fetchCount</span>(<span style=color:#a6e22e>u</span>.<span style=color:#a6e22e>pathname</span>);
</span></span><span style=display:flex><span> <span style=color:#a6e22e>span</span>.<span style=color:#a6e22e>textContent</span> <span style=color:#f92672>=</span> <span style=color:#a6e22e>label</span>(<span style=color:#a6e22e>n</span> <span style=color:#f92672>??</span> <span style=color:#ae81ff>0</span>);
</span></span><span style=display:flex><span> } <span style=color:#66d9ef>catch</span> {}
</span></span><span style=display:flex><span> }));
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span> <span style=color:#75715e>// C) Site totals (optional placeholders)
</span></span></span><span style=display:flex><span><span style=color:#75715e></span> <span style=color:#66d9ef>const</span> <span style=color:#a6e22e>pvEl</span> <span style=color:#f92672>=</span> document.<span style=color:#a6e22e>getElementById</span>(<span style=color:#e6db74>&#39;vercount_value_site_pv&#39;</span>);
</span></span><span style=display:flex><span> <span style=color:#66d9ef>if</span> (<span style=color:#a6e22e>pvEl</span>) { <span style=color:#66d9ef>const</span> <span style=color:#a6e22e>n</span> <span style=color:#f92672>=</span> <span style=color:#66d9ef>await</span> <span style=color:#a6e22e>fetchCount</span>(<span style=color:#e6db74>&#39;TOTAL&#39;</span>); <span style=color:#a6e22e>pvEl</span>.<span style=color:#a6e22e>textContent</span> <span style=color:#f92672>=</span> (<span style=color:#a6e22e>n</span><span style=color:#f92672>===</span><span style=color:#66d9ef>null</span>) <span style=color:#f92672>?</span> <span style=color:#e6db74>&#39;&#39;</span> <span style=color:#f92672>:</span> <span style=color:#a6e22e>nf</span>.<span style=color:#a6e22e>format</span>(<span style=color:#a6e22e>n</span>); }
</span></span><span style=display:flex><span> });
</span></span><span style=display:flex><span>})();
</span></span><span style=display:flex><span>&lt;/<span style=color:#f92672>script</span>&gt;
</span></span></code></pre></div><p>Style so meta items stay inline with middle dots (PaperModlike). Put this in <code>assets/css/extended/goatcounter.css</code>:</p><div class=highlight><pre tabindex=0 style=color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-css data-lang=css><span style=display:flex><span>.<span style=color:#a6e22e>post-single</span> .<span style=color:#a6e22e>post-header</span> .<span style=color:#a6e22e>post-meta</span><span style=color:#f92672>,</span>
</span></span><span style=display:flex><span>.<span style=color:#a6e22e>post-single</span> .<span style=color:#a6e22e>post-header</span> .<span style=color:#a6e22e>entry-meta</span><span style=color:#f92672>,</span>
</span></span><span style=display:flex><span>.<span style=color:#a6e22e>post-entry</span> .<span style=color:#a6e22e>entry-footer</span><span style=color:#f92672>,</span>
</span></span><span style=display:flex><span>.<span style=color:#a6e22e>post-entry</span> .<span style=color:#a6e22e>post-meta</span> {
</span></span><span style=display:flex><span> <span style=color:#66d9ef>display</span>:<span style=color:#66d9ef>flex</span>; <span style=color:#66d9ef>align-items</span>:<span style=color:#66d9ef>center</span>; <span style=color:#66d9ef>flex-wrap</span>:<span style=color:#66d9ef>wrap</span>; <span style=color:#66d9ef>gap</span>:<span style=color:#ae81ff>.35</span><span style=color:#66d9ef>rem</span>;
</span></span><span style=display:flex><span>}
</span></span><span style=display:flex><span>.<span style=color:#a6e22e>post-single</span> .<span style=color:#a6e22e>post-header</span> .<span style=color:#a6e22e>post-meta</span> <span style=color:#f92672>&gt;</span> <span style=color:#f92672>*,</span>
</span></span><span style=display:flex><span>.<span style=color:#a6e22e>post-single</span> .<span style=color:#a6e22e>post-header</span> .<span style=color:#a6e22e>entry-meta</span> <span style=color:#f92672>&gt;</span> <span style=color:#f92672>*,</span>
</span></span><span style=display:flex><span>.<span style=color:#a6e22e>post-entry</span> .<span style=color:#a6e22e>entry-footer</span> <span style=color:#f92672>&gt;</span> <span style=color:#f92672>*,</span>
</span></span><span style=display:flex><span>.<span style=color:#a6e22e>post-entry</span> .<span style=color:#a6e22e>post-meta</span> <span style=color:#f92672>&gt;</span> <span style=color:#f92672>*</span> {
</span></span><span style=display:flex><span> <span style=color:#66d9ef>display</span>:<span style=color:#66d9ef>inline-flex</span>; <span style=color:#66d9ef>align-items</span>:<span style=color:#66d9ef>center</span>; <span style=color:#66d9ef>white-space</span>:<span style=color:#66d9ef>nowrap</span>;
</span></span><span style=display:flex><span>}
</span></span><span style=display:flex><span>.<span style=color:#a6e22e>post-single</span> .<span style=color:#a6e22e>post-header</span> .<span style=color:#a6e22e>post-meta</span> <span style=color:#f92672>&gt;</span> <span style=color:#f92672>*</span> <span style=color:#f92672>+</span> <span style=color:#f92672>*</span>::<span style=color:#a6e22e>before</span><span style=color:#f92672>,</span>
</span></span><span style=display:flex><span>.<span style=color:#a6e22e>post-single</span> .<span style=color:#a6e22e>post-header</span> .<span style=color:#a6e22e>entry-meta</span> <span style=color:#f92672>&gt;</span> <span style=color:#f92672>*</span> <span style=color:#f92672>+</span> <span style=color:#f92672>*</span>::<span style=color:#a6e22e>before</span><span style=color:#f92672>,</span>
</span></span><span style=display:flex><span>.<span style=color:#a6e22e>post-entry</span> .<span style=color:#a6e22e>entry-footer</span> <span style=color:#f92672>&gt;</span> <span style=color:#f92672>*</span> <span style=color:#f92672>+</span> <span style=color:#f92672>*</span>::<span style=color:#a6e22e>before</span><span style=color:#f92672>,</span>
</span></span><span style=display:flex><span>.<span style=color:#a6e22e>post-entry</span> .<span style=color:#a6e22e>post-meta</span> <span style=color:#f92672>&gt;</span> <span style=color:#f92672>*</span> <span style=color:#f92672>+</span> <span style=color:#f92672>*</span>::<span style=color:#a6e22e>before</span> {
</span></span><span style=display:flex><span> <span style=color:#66d9ef>content</span>:<span style=color:#e6db74>&#34;·&#34;</span>; <span style=color:#66d9ef>margin</span>:<span style=color:#ae81ff>0</span> <span style=color:#ae81ff>.5</span><span style=color:#66d9ef>rem</span>; <span style=color:#66d9ef>opacity</span>:<span style=color:#ae81ff>.6</span>;
</span></span><span style=display:flex><span>}
</span></span><span style=display:flex><span>.<span style=color:#a6e22e>post-views-bottom</span> { <span style=color:#66d9ef>margin</span>:<span style=color:#ae81ff>.6</span><span style=color:#66d9ef>rem</span> <span style=color:#ae81ff>0</span> <span style=color:#ae81ff>0</span>; <span style=color:#66d9ef>opacity</span>:<span style=color:#ae81ff>.75</span>; <span style=color:#66d9ef>font-size</span>:<span style=color:#ae81ff>.95</span><span style=color:#66d9ef>em</span>; }
</span></span></code></pre></div><hr><h3 id=6-count-tor-onion-visits-into-the-same-site>6) Count Tor (.onion) visits <strong>into the same site</strong><a hidden class=anchor aria-hidden=true href=#6-count-tor-onion-visits-into-the-same-site>#</a></h3><p>In the <strong>.onion</strong> server block, proxy GoatCounter endpoints to the same site by forcing the Host header:</p><div class=highlight><pre tabindex=0 style=color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-nginx data-lang=nginx><span style=display:flex><span><span style=color:#75715e># inside: server { ... } for fjthpp2h3mj2r....onion
</span></span></span><span style=display:flex><span><span style=color:#75715e></span>
</span></span><span style=display:flex><span><span style=color:#66d9ef>location</span> = <span style=color:#e6db74>/count</span> {
</span></span><span style=display:flex><span> <span style=color:#f92672>proxy_pass</span> <span style=color:#e6db74>http://127.0.0.1:8081/count</span>;
</span></span><span style=display:flex><span> <span style=color:#f92672>proxy_set_header</span> <span style=color:#e6db74>Host</span> <span style=color:#e6db74>statsblog.alipourimjourneys.ir</span>;
</span></span><span style=display:flex><span> <span style=color:#f92672>proxy_set_header</span> <span style=color:#e6db74>X-Forwarded-For</span> $proxy_add_x_forwarded_for;
</span></span><span style=display:flex><span> <span style=color:#f92672>proxy_set_header</span> <span style=color:#e6db74>X-Forwarded-Proto</span> <span style=color:#e6db74>http</span>; <span style=color:#75715e># onion is http
</span></span></span><span style=display:flex><span><span style=color:#75715e></span>}
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span><span style=color:#66d9ef>location</span> <span style=color:#e6db74>/counter/</span> {
</span></span><span style=display:flex><span> <span style=color:#f92672>proxy_pass</span> <span style=color:#e6db74>http://127.0.0.1:8081/counter/</span>;
</span></span><span style=display:flex><span> <span style=color:#f92672>proxy_set_header</span> <span style=color:#e6db74>Host</span> <span style=color:#e6db74>statsblog.alipourimjourneys.ir</span>;
</span></span><span style=display:flex><span> <span style=color:#f92672>proxy_set_header</span> <span style=color:#e6db74>X-Forwarded-For</span> $proxy_add_x_forwarded_for;
</span></span><span style=display:flex><span> <span style=color:#f92672>proxy_set_header</span> <span style=color:#e6db74>X-Forwarded-Proto</span> <span style=color:#e6db74>http</span>;
</span></span><span style=display:flex><span> <span style=color:#f92672>add_header</span> <span style=color:#e6db74>Cache-Control</span> <span style=color:#e6db74>&#34;no-store&#34;</span>;
</span></span><span style=display:flex><span> <span style=color:#f92672>proxy_no_cache</span> <span style=color:#ae81ff>1</span>; <span style=color:#f92672>proxy_cache_bypass</span> <span style=color:#ae81ff>1</span>;
</span></span><span style=display:flex><span>}
</span></span></code></pre></div><p>Because the loader above sets <code>window.goatcounter.endpoint</code> to <code>/count</code> on <code>.onion</code>, both <strong>hits</strong> and <strong>reads</strong> stay on the onion origin and are tallied into the same GoatCounter site.</p><blockquote><p><strong>Note on DNT/Tor:</strong> Tor Browser sends <code>DNT: 1</code>. GoatCounter respects DNT by default, so Tor users wont be counted unless you disable “Respect Do Not Track” in GoatCounter settings. Your choice; I left the code compatible either way.</p></blockquote><hr><h3 id=7-troubleshooting-quick-notes>7) Troubleshooting quick notes<a hidden class=anchor aria-hidden=true href=#7-troubleshooting-quick-notes>#</a></h3><ul><li><strong>400 “no site at this domain”</strong> → create the site with <code>-vhost=statsblog.alipourimjourneys.ir</code> or fix your proxy <code>Host</code> header.</li><li><strong>Counts dont change on onion</strong> → verify Tor path via <code>torsocks</code>, watch logs:<div class=highlight><pre tabindex=0 style=color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-bash data-lang=bash><span style=display:flex><span>docker compose logs -f goatcounter
</span></span><span style=display:flex><span><span style=color:#75715e># look for “ignored: bot” (curl UA), or DNT being respected</span>
</span></span></code></pre></div></li><li><strong>DevTools sanity</strong> → on clearnet you should see <code>https://statsblog…/count</code> and <code>…/counter/*.json</code>; on onion you should see <code>/count</code> and <code>/counter/*.json</code> (sameorigin).</li></ul><p>Thats it. One dashboard; clearnet + onion both increment; tiny inline counters everywhere.</p><p>Honestly, self-hosting GoatCounter wasn&rsquo;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&rsquo;s done and dusted, and I&rsquo;m a happy puppy! :D</p><hr><div class=signature-block style=margin-top:1rem><p><strong>Downloads:</strong>
<a href=/sources/posts/view_count.md>Markdown</a> ·
<a href=/sources/posts/view_count.md.asc>Signature (.asc)</a></p><details class=signature><summary>View OpenPGP signature</summary><pre style=font-size:.85em;overflow-x:auto;padding:.75rem>-----BEGIN PGP SIGNATURE-----
iQIzBAABCgAdFiEEVaKl3oR5K6zGyu4/tYgoUOBMjSoFAmpkbuoACgkQtYgoUOBM
jSolRg//SwN9nMf9/Wgkr5h&#43;dQowZMCHXXcKWgO8J08ITVDN1&#43;weNNGANFrz63SQ
DajHGeSogYW1&#43;AAxJaAeQ7hLdOX9foV5pT&#43;kWANIjRBXnFyW&#43;WR7kt6tmN2rcSH8
z4GKxqE3kdd3kCRhqT0pLiwnurqLgdCK&#43;YldftO6GB8WP4oNDqOwHvoIE6o0ekdH
sn7ZBIxMaWc5UqijjqgBHhdHz3uSmL&#43;rN4UwLFM3WXXlwl3HdGyjHcNTG7k648s2
X5&#43;7a78OZAuDElpyp9y6WqiAFJQdrwBbTv3vvpU&#43;f911voV7ZA&#43;KbUqHsQrISTKz
cd&#43;tPw2lcayfBZRtHMrL3fygvT3AWktcSavZrU5EOX/u/P7eMWEYu0FYh6aHqRak
&#43;Ft2FR7EPlB2faS6wWYfoua6BYxFvevXX1fk&#43;0HhZn9UJxJPaa/WTgVWDgpt&#43;&#43;Ce
fdaDmsR69LIj2QTjPMXdQiUKkWPG2ziadTwJEWUHzOuREifmuBgp9Mwedk6zYkdT
m5Roi70IPtX3dDDHG5Votw3AKng3gaU7YcNzZVyi3iZkQkUHPUK47Wj21FajikMP
gCcWsoYWBRqdhL5XDrodt28AuLpm0cslz79DZdbLnielcjOS&#43;GaUynjLzEWveOib
dxLcbIIap&#43;nt315cjvkfatGv14Dres/K7vhQAhqGy5o0LM1D0qc=
=o387
-----END PGP SIGNATURE-----
</pre></details><p><em>Verify locally:</em></p><pre style=font-size:.85em;overflow-x:auto;padding:.75rem>curl -fSLO https://blog.alipour.eu/sources/posts/view_count.md
curl -fSLO https://blog.alipour.eu/sources/posts/view_count.md.asc
gpg --verify view_count.md.asc view_count.md
</pre></div></div><footer class=post-footer><ul class=post-tags><li><a href=https://blog.alipour.eu/tags/hugo/>Hugo</a></li><li><a href=https://blog.alipour.eu/tags/papermod/>PaperMod</a></li><li><a href=https://blog.alipour.eu/tags/analytics/>Analytics</a></li><li><a href=https://blog.alipour.eu/tags/debugging/>Debugging</a></li><li><a href=https://blog.alipour.eu/tags/busuanzi/>Busuanzi</a></li><li><a href=https://blog.alipour.eu/tags/vercount/>Vercount</a></li><li><a href=https://blog.alipour.eu/tags/goatcounter/>GoatCounter</a></li></ul><nav class=paginav><a class=prev href=https://blog.alipour.eu/posts/skin_routine/><span class=title>« Prev</span><br><span>Skin routine</span>
</a><a class=next href=https://blog.alipour.eu/posts/inet_logo/><span class=title>Next »</span><br><span>INET Logo That Breathes — From CAD to LEDs to a Calm Little Server</span></a></nav></footer><div class=comments-switch style="display:flex;gap:.5rem;margin:.5rem 0 1rem"><button id=btn-isso type=button aria-pressed=true>Anonymous (Isso)</button>
<button id=btn-giscus type=button aria-pressed=false>GitHub (Giscus)</button>
<a href=https://github.com/AlipourIm/blog-comments/discussions/categories/comments target=_blank rel=noopener>Open on GitHub ↗</a></div><div id=panel-giscus style=display:none><section id=giscus-thread></section></div><div id=panel-isso><section id=isso-thread data-isso-id=/posts/papermod-views-debugging-story/ data-title="A Tiny &amp;#39;Views&amp;#39; Badge Sent Me Down a Rabbit Hole (PaperMod + Busuanzi + Debugging --&amp;gt; swapped with GoatCounter the GOAT)"><noscript>Javascript needs to be activated to view comments.</noscript></section><script src=/isso/js/embed.min.js data-isso=/isso/ data-isso-css=false data-isso-lang=en data-isso-max-comments-nested=5 data-isso-sorting=newest async></script><small class=isso-powered style="display:block;margin:.5rem 0;color:var(--secondary,#888)">Comments powered by <a href=https://isso-comments.de target=_blank rel=noopener>Isso</a></small></div><script>(function(){var i=document.getElementById("panel-isso"),a=document.getElementById("panel-giscus"),e=document.getElementById("btn-isso"),t=document.getElementById("btn-giscus"),s=!1;function r(){if(n=localStorage.getItem("pref-theme"),n==="dark")return!0;if(n==="light")return!1;var n,e=document.documentElement,t=document.body;return!!(e.classList&&e.classList.contains("dark")||t.classList&&t.classList.contains("dark")||e.getAttribute("data-theme")==="dark"||t.getAttribute("data-theme")==="dark"||e.classList&&e.classList.contains("theme-dark")||t.classList&&t.classList.contains("theme-dark"))||window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches}function o(){return r()?"dark_dimmed":"noborder_light"}function c(){if(s)return;var t,n,e=document.createElement("script");e.src="https://giscus.app/client.js",e.async=!0,e.crossOrigin="anonymous",e.setAttribute("data-repo","AlipourIm/blog-comments"),e.setAttribute("data-repo-id","R_kgDOQGARyA"),e.setAttribute("data-category","Comments"),e.setAttribute("data-category-id","DIC_kwDOQGARyM4Cw3x-"),e.setAttribute("data-mapping","pathname"),e.setAttribute("data-strict","0"),e.setAttribute("data-reactions-enabled","1"),e.setAttribute("data-emit-metadata","0"),e.setAttribute("data-input-position","bottom"),e.setAttribute("data-lang","en"),e.setAttribute("data-theme",o()),document.getElementById("giscus-thread").appendChild(e);function i(){var e=document.querySelector("iframe.giscus-frame");e&&e.contentWindow&&e.contentWindow.postMessage({giscus:{setConfig:{theme:o()}}},"https://giscus.app")}t=document.getElementById("theme-toggle"),t&&t.addEventListener("click",function(){setTimeout(i,0)}),n=new MutationObserver(function(){i()}),n.observe(document.documentElement,{attributes:!0,attributeFilter:["class","data-theme"]}),n.observe(document.body,{attributes:!0,attributeFilter:["class","data-theme"]}),s=!0}function n(n){var s=n==="isso";i.style.display=s?"block":"none",a.style.display=s?"none":"block",e&&e.setAttribute("aria-pressed",s?"true":"false"),t&&t.setAttribute("aria-pressed",s?"false":"true"),s||c()}e&&e.addEventListener("click",function(e){e.preventDefault(),n("isso")}),t&&t.addEventListener("click",function(e){e.preventDefault(),n("giscus")}),n("isso")})()</script></article></main><footer class=footer><span>&copy; 2026 <a href=https://blog.alipour.eu/>AlipourIm journeys</a></span> ·
<span>Powered by
<a href=https://gohugo.io/ rel="noopener noreferrer" target=_blank>Hugo</a> &
<a href=https://github.com/adityatelange/hugo-PaperMod/ rel=noopener target=_blank>PaperMod</a></span></footer><a href=#top aria-label="go to top" title="Go to Top (Alt + G)" class=top-link id=top-link accesskey=g><svg viewBox="0 0 12 6" fill="currentcolor"><path d="M12 6H0l6-6z"/></svg></a><div class=site-meta-row style=display:flex;align-items:center;gap:.75rem;flex-wrap:wrap><a href=/index.xml rel=alternate type=application/rss+xml title=RSS class=rss-link style=display:inline-flex;align-items:center;gap:.35rem><svg viewBox="0 0 24 24" fill="none" stroke="currentcolor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 11a9 9 0 019 9"/><path d="M4 4a16 16 0 0116 16"/><circle cx="5" cy="19" r="1"/></svg>
<span>RSS</span>
</a><span aria-hidden=true>·</span>
<span>Visitors this week: <span id=gc_site_visitors_week></span></span></div><script>let menu=document.getElementById("menu");menu&&(menu.scrollLeft=localStorage.getItem("menu-scroll-position"),menu.onscroll=function(){localStorage.setItem("menu-scroll-position",menu.scrollLeft)}),document.querySelectorAll('a[href^="#"]').forEach(e=>{e.addEventListener("click",function(e){e.preventDefault();var t=this.getAttribute("href").substr(1);window.matchMedia("(prefers-reduced-motion: reduce)").matches?document.querySelector(`[id='${decodeURIComponent(t)}']`).scrollIntoView():document.querySelector(`[id='${decodeURIComponent(t)}']`).scrollIntoView({behavior:"smooth"}),t==="top"?history.replaceState(null,null," "):history.pushState(null,null,`#${t}`)})})</script><script>var mybutton=document.getElementById("top-link");window.onscroll=function(){document.body.scrollTop>800||document.documentElement.scrollTop>800?(mybutton.style.visibility="visible",mybutton.style.opacity="1"):(mybutton.style.visibility="hidden",mybutton.style.opacity="0")}</script><script>document.getElementById("theme-toggle").addEventListener("click",()=>{document.body.className.includes("dark")?(document.body.classList.remove("dark"),localStorage.setItem("pref-theme","light")):(document.body.classList.add("dark"),localStorage.setItem("pref-theme","dark"))})</script><script>document.querySelectorAll("pre > code").forEach(e=>{const n=e.parentNode.parentNode,t=document.createElement("button");t.classList.add("copy-code"),t.innerHTML="copy";function s(){t.innerHTML="copied!",setTimeout(()=>{t.innerHTML="copy"},2e3)}t.addEventListener("click",t=>{if("clipboard"in navigator){navigator.clipboard.writeText(e.textContent),s();return}const n=document.createRange();n.selectNodeContents(e);const o=window.getSelection();o.removeAllRanges(),o.addRange(n);try{document.execCommand("copy"),s()}catch{}o.removeRange(n)}),n.classList.contains("highlight")?n.appendChild(t):n.parentNode.firstChild==n||(e.parentNode.parentNode.parentNode.parentNode.parentNode.nodeName=="TABLE"?e.parentNode.parentNode.parentNode.parentNode.parentNode.appendChild(t):e.parentNode.appendChild(t))})</script></body></html>