228 lines
No EOL
69 KiB
HTML
228 lines
No EOL
69 KiB
HTML
<!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 ‘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."><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> » <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> · 9 min · 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’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.</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><<span style=color:#f92672>div</span> <span style=color:#a6e22e>class</span><span style=color:#f92672>=</span><span style=color:#e6db74>"site-meta-row"</span> <span style=color:#a6e22e>style</span><span style=color:#f92672>=</span><span style=color:#e6db74>"display:flex;align-items:center;gap:.75rem;flex-wrap:wrap;"</span>>
|
||
</span></span><span style=display:flex><span> <<span style=color:#f92672>a</span> <span style=color:#a6e22e>href</span><span style=color:#f92672>=</span><span style=color:#e6db74>"/index.xml"</span> <span style=color:#a6e22e>rel</span><span style=color:#f92672>=</span><span style=color:#e6db74>"alternate"</span> <span style=color:#a6e22e>type</span><span style=color:#f92672>=</span><span style=color:#e6db74>"application/rss+xml"</span> <span style=color:#a6e22e>title</span><span style=color:#f92672>=</span><span style=color:#e6db74>"RSS"</span>
|
||
</span></span><span style=display:flex><span> <span style=color:#a6e22e>class</span><span style=color:#f92672>=</span><span style=color:#e6db74>"rss-link"</span> <span style=color:#a6e22e>style</span><span style=color:#f92672>=</span><span style=color:#e6db74>"display:inline-flex;align-items:center;gap:.35rem;"</span>>
|
||
</span></span><span style=display:flex><span> { partial "svg.html" (dict "name" "rss") }
|
||
</span></span><span style=display:flex><span> <<span style=color:#f92672>span</span>>RSS</<span style=color:#f92672>span</span>>
|
||
</span></span><span style=display:flex><span> </<span style=color:#f92672>a</span>>
|
||
</span></span><span style=display:flex><span>
|
||
</span></span><span style=display:flex><span> <<span style=color:#f92672>span</span> <span style=color:#a6e22e>aria-hidden</span><span style=color:#f92672>=</span><span style=color:#e6db74>"true"</span>>·</<span style=color:#f92672>span</span>>
|
||
</span></span><span style=display:flex><span>
|
||
</span></span><span style=display:flex><span> <<span style=color:#f92672>span</span> <span style=color:#a6e22e>id</span><span style=color:#f92672>=</span><span style=color:#e6db74>"busuanzi_container_site_pv"</span>>
|
||
</span></span><span style=display:flex><span> Site views: <<span style=color:#f92672>span</span> <span style=color:#a6e22e>id</span><span style=color:#f92672>=</span><span style=color:#e6db74>"busuanzi_value_site_pv"</span>>—</<span style=color:#f92672>span</span>>
|
||
</span></span><span style=display:flex><span> </<span style=color:#f92672>span</span>>
|
||
</span></span><span style=display:flex><span> <<span style=color:#f92672>span</span> <span style=color:#a6e22e>aria-hidden</span><span style=color:#f92672>=</span><span style=color:#e6db74>"true"</span>>·</<span style=color:#f92672>span</span>>
|
||
</span></span><span style=display:flex><span> <<span style=color:#f92672>span</span> <span style=color:#a6e22e>id</span><span style=color:#f92672>=</span><span style=color:#e6db74>"busuanzi_container_site_uv"</span>>
|
||
</span></span><span style=display:flex><span> Visitors: <<span style=color:#f92672>span</span> <span style=color:#a6e22e>id</span><span style=color:#f92672>=</span><span style=color:#e6db74>"busuanzi_value_site_uv"</span>>—</<span style=color:#f92672>span</span>>
|
||
</span></span><span style=display:flex><span> </<span style=color:#f92672>span</span>>
|
||
</span></span><span style=display:flex><span></<span style=color:#f92672>div</span>>
|
||
</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 site‑wide 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><<span style=color:#f92672>script</span> <span style=color:#a6e22e>src</span><span style=color:#f92672>=</span><span style=color:#e6db74>"//cdn.busuanzi.cc/busuanzi/3.6.9/busuanzi.min.js"</span> <span style=color:#a6e22e>defer</span>></<span style=color:#f92672>script</span>>
|
||
</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>That’s “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>. Busuanzi’s 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 Busuanzi‑style drop‑in without the 22‑char 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><!-- extend_head.html --></span>
|
||
</span></span><span style=display:flex><span><<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>"https://events.vercount.one/js"</span>></<span style=color:#f92672>script</span>>
|
||
</span></span></code></pre></div><p>I kept the same tidy footer row, but switched the IDs to Vercount’s:</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:#f92672>div</span> <span style=color:#a6e22e>class</span><span style=color:#f92672>=</span><span style=color:#e6db74>"site-meta-row"</span> <span style=color:#a6e22e>style</span><span style=color:#f92672>=</span><span style=color:#e6db74>"display:flex;align-items:center;gap:.75rem;flex-wrap:wrap;"</span>>
|
||
</span></span><span style=display:flex><span> <<span style=color:#f92672>a</span> <span style=color:#a6e22e>href</span><span style=color:#f92672>=</span><span style=color:#e6db74>"/index.xml"</span> <span style=color:#a6e22e>rel</span><span style=color:#f92672>=</span><span style=color:#e6db74>"alternate"</span> <span style=color:#a6e22e>type</span><span style=color:#f92672>=</span><span style=color:#e6db74>"application/rss+xml"</span> <span style=color:#a6e22e>title</span><span style=color:#f92672>=</span><span style=color:#e6db74>"RSS"</span>
|
||
</span></span><span style=display:flex><span> <span style=color:#a6e22e>class</span><span style=color:#f92672>=</span><span style=color:#e6db74>"rss-link"</span> <span style=color:#a6e22e>style</span><span style=color:#f92672>=</span><span style=color:#e6db74>"display:inline-flex;align-items:center;gap:.35rem;"</span>>
|
||
</span></span><span style=display:flex><span> { partial "svg.html" (dict "name" "rss") }
|
||
</span></span><span style=display:flex><span> <<span style=color:#f92672>span</span>>RSS</<span style=color:#f92672>span</span>>
|
||
</span></span><span style=display:flex><span> </<span style=color:#f92672>a</span>>
|
||
</span></span><span style=display:flex><span>
|
||
</span></span><span style=display:flex><span> <<span style=color:#f92672>span</span> <span style=color:#a6e22e>aria-hidden</span><span style=color:#f92672>=</span><span style=color:#e6db74>"true"</span>>·</<span style=color:#f92672>span</span>>
|
||
</span></span><span style=display:flex><span>
|
||
</span></span><span style=display:flex><span> <<span style=color:#f92672>span</span>>Site views: <<span style=color:#f92672>span</span> <span style=color:#a6e22e>id</span><span style=color:#f92672>=</span><span style=color:#e6db74>"vercount_value_site_pv"</span>>—</<span style=color:#f92672>span</span>></<span style=color:#f92672>span</span>>
|
||
</span></span><span style=display:flex><span> <<span style=color:#f92672>span</span> <span style=color:#a6e22e>aria-hidden</span><span style=color:#f92672>=</span><span style=color:#e6db74>"true"</span>>·</<span style=color:#f92672>span</span>>
|
||
</span></span><span style=display:flex><span> <<span style=color:#f92672>span</span>>Visitors: <<span style=color:#f92672>span</span> <span style=color:#a6e22e>id</span><span style=color:#f92672>=</span><span style=color:#e6db74>"vercount_value_site_uv"</span>>—</<span style=color:#f92672>span</span>></<span style=color:#f92672>span</span>>
|
||
</span></span><span style=display:flex><span></<span style=color:#f92672>div</span>>
|
||
</span></span></code></pre></div><p>For per‑post 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><<span style=color:#f92672>span</span> <span style=color:#a6e22e>id</span><span style=color:#f92672>=</span><span style=color:#e6db74>"vercount_value_page_pv"</span>>—</<span style=color:#f92672>span</span>> 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>"https://alipourimjourneys.ir/"</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, they’ll populate.</p></li></ul><h3 id=postmortem-checklist-aka-things-i-learned>Post‑mortem 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>, it’s not always IDs or caching. Sometimes it’s 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>Don’t 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 Content‑Security‑Policy 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 you’ll 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… 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.</p><hr><h2 id=change-of-plans-selfhosting-goatcounter-for-papermod-docker--cloudflare--onion>Change of plans: Self‑hosting 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>self‑host</strong> part I ended up with: Docker for GoatCounter, Cloudflare (orange‑cloud) 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>"busuanzi|vercount|vercount_value_|busuanzi_value_"</span> .
|
||
</span></span></code></pre></div><p>Remove any matching <code><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>"127.0.0.1:8081:8080"</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>&&</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>&&</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>Self‑host <code>count.js</code> so the onion mirror never fetches a third‑party 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><!-- Loader: choose endpoint based on hostname --></span>
|
||
</span></span><span style=display:flex><span><<span style=color:#f92672>script</span>>
|
||
</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>'/count'</span> <span style=color:#f92672>:</span> <span style=color:#e6db74>'https://statsblog.alipourimjourneys.ir/count'</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>'script'</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>'/js/count.js'</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></<span style=color:#f92672>script</span>>
|
||
</span></span><span style=display:flex><span>
|
||
</span></span><span style=display:flex><span><span style=color:#75715e><!-- Helper: inline meta counts (post pages + lists) and totals --></span>
|
||
</span></span><span style=display:flex><span><<span style=color:#f92672>script</span>>
|
||
</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>''</span> <span style=color:#f92672>:</span> <span style=color:#e6db74>'https://statsblog.alipourimjourneys.ir'</span>; <span style=color:#75715e>// '' = 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>'view'</span>, <span style=color:#a6e22e>PLUR</span><span style=color:#f92672>=</span><span style=color:#e6db74>'views'</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>'loading'</span>) <span style=color:#a6e22e>fn</span>(); <span style=color:#66d9ef>else</span> document.<span style=color:#a6e22e>addEventListener</span>(<span style=color:#e6db74>'DOMContentLoaded'</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>&&</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>'omit'</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>''</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>) => (<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>'—'</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>'.post-single'</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>'.post-header .post-meta, .post-header .entry-meta, .post-meta'</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>'.post-views'</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>'span'</span>); <span style=color:#a6e22e>span</span>.<span style=color:#a6e22e>className</span><span style=color:#f92672>=</span><span style=color:#e6db74>'post-views'</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>'.post-content'</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>'.post-views-bottom'</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>'div'</span>); <span style=color:#a6e22e>bottom</span>.<span style=color:#a6e22e>className</span><span style=color:#f92672>=</span><span style=color:#e6db74>'post-views-bottom'</span>; <span style=color:#a6e22e>after</span>.<span style=color:#a6e22e>insertAdjacentElement</span>(<span style=color:#e6db74>'afterend'</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>'article.post-entry'</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>) => {
|
||
</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>'.entry-footer, .post-meta'</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>'a.entry-link, h2 a, .entry-title a'</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>'href'</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>'.post-views'</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>'span'</span>); <span style=color:#a6e22e>span</span>.<span style=color:#a6e22e>className</span><span style=color:#f92672>=</span><span style=color:#e6db74>'post-views'</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>'vercount_value_site_pv'</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>'TOTAL'</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>'—'</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></<span style=color:#f92672>script</span>>
|
||
</span></span></code></pre></div><p>Style so meta items stay inline with middle dots (PaperMod‑like). 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>></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 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 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>></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>></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>></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>></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>></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>"·"</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>"no-store"</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 won’t 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 don’t 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> (same‑origin).</li></ul><p>That’s it. One dashboard; clearnet + onion both increment; tiny inline counters everywhere.</p><p>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</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+dQowZMCHXXcKWgO8J08ITVDN1+weNNGANFrz63SQ
|
||
DajHGeSogYW1+AAxJaAeQ7hLdOX9foV5pT+kWANIjRBXnFyW+WR7kt6tmN2rcSH8
|
||
z4GKxqE3kdd3kCRhqT0pLiwnurqLgdCK+YldftO6GB8WP4oNDqOwHvoIE6o0ekdH
|
||
sn7ZBIxMaWc5UqijjqgBHhdHz3uSmL+rN4UwLFM3WXXlwl3HdGyjHcNTG7k648s2
|
||
X5+7a78OZAuDElpyp9y6WqiAFJQdrwBbTv3vvpU+f911voV7ZA+KbUqHsQrISTKz
|
||
cd+tPw2lcayfBZRtHMrL3fygvT3AWktcSavZrU5EOX/u/P7eMWEYu0FYh6aHqRak
|
||
+Ft2FR7EPlB2faS6wWYfoua6BYxFvevXX1fk+0HhZn9UJxJPaa/WTgVWDgpt++Ce
|
||
fdaDmsR69LIj2QTjPMXdQiUKkWPG2ziadTwJEWUHzOuREifmuBgp9Mwedk6zYkdT
|
||
m5Roi70IPtX3dDDHG5Votw3AKng3gaU7YcNzZVyi3iZkQkUHPUK47Wj21FajikMP
|
||
gCcWsoYWBRqdhL5XDrodt28AuLpm0cslz79DZdbLnielcjOS+GaUynjLzEWveOib
|
||
dxLcbIIap+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 &#39;Views&#39; Badge Sent Me Down a Rabbit Hole (PaperMod + Busuanzi + Debugging --&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>© 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> |