|
| 1 | +# Self-hosted visit counter — setup |
| 2 | + |
| 3 | +Cloudflare Worker + KV. ~10 minutes, no credit card. |
| 4 | + |
| 5 | +## 1. Cloudflare account |
| 6 | + |
| 7 | +1. Go to <https://dash.cloudflare.com/sign-up>, sign up with your email, verify it. |
| 8 | +2. You do **not** need to add a domain. Workers run on `*.workers.dev` for free. |
| 9 | + |
| 10 | +## 2. Get the code onto your machine |
| 11 | + |
| 12 | +The worker lives in this repo under `worker/`: |
| 13 | + |
| 14 | +```bash |
| 15 | +git clone https://github.com/RealMathModel/RealMathModel.github.io.git |
| 16 | +cd RealMathModel.github.io/worker |
| 17 | +``` |
| 18 | + |
| 19 | +``` |
| 20 | +worker/ |
| 21 | + src/index.js |
| 22 | + wrangler.toml |
| 23 | +``` |
| 24 | + |
| 25 | +(GitHub Pages will serve these two files as static text at |
| 26 | +`/worker/src/index.js` etc. That is harmless — they contain no secrets — but |
| 27 | +if you would rather they not be public, move the `worker/` folder to its own |
| 28 | +private repo.) |
| 29 | + |
| 30 | +You need Node 18+ (`node -v`). `npx` ships with it; there's nothing to install |
| 31 | +globally. |
| 32 | + |
| 33 | +## 3. Log in |
| 34 | + |
| 35 | +```bash |
| 36 | +cd worker |
| 37 | +npx wrangler login |
| 38 | +``` |
| 39 | + |
| 40 | +A browser window opens; approve the access request. |
| 41 | + |
| 42 | +## 4. Create the KV namespace |
| 43 | + |
| 44 | +```bash |
| 45 | +npx wrangler kv namespace create HITS |
| 46 | +``` |
| 47 | + |
| 48 | +It prints something like: |
| 49 | + |
| 50 | +``` |
| 51 | +[[kv_namespaces]] |
| 52 | +binding = "HITS" |
| 53 | +id = "a1b2c3d4e5f60718293a4b5c6d7e8f90" |
| 54 | +``` |
| 55 | + |
| 56 | +Copy that `id` into `wrangler.toml`, replacing `PASTE_YOUR_KV_NAMESPACE_ID_HERE`. |
| 57 | + |
| 58 | +Optional — start the count at a non-zero number (e.g. carry over your |
| 59 | +GoatCounter total): |
| 60 | + |
| 61 | +```bash |
| 62 | +npx wrangler kv key put --binding=HITS total "48213" --remote |
| 63 | +``` |
| 64 | + |
| 65 | +## 5. Deploy |
| 66 | + |
| 67 | +```bash |
| 68 | +npx wrangler deploy |
| 69 | +``` |
| 70 | + |
| 71 | +Output ends with the live URL, e.g. |
| 72 | +`https://rmm-counter.yourname.workers.dev`. |
| 73 | + |
| 74 | +Check it: |
| 75 | + |
| 76 | +```bash |
| 77 | +curl -s https://rmm-counter.yourname.workers.dev/ |
| 78 | +# {"count":0} |
| 79 | +``` |
| 80 | + |
| 81 | +(That `curl` reads without incrementing — only `POST` from the site's origin |
| 82 | +increments.) |
| 83 | + |
| 84 | +## 6. Wire up the page |
| 85 | + |
| 86 | +In `index.html`, replace the GoatCounter badge IIFE with the block in |
| 87 | +`counter-block.html`, and set: |
| 88 | + |
| 89 | +```js |
| 90 | +var API='https://rmm-counter.yourname.workers.dev/'; |
| 91 | +``` |
| 92 | + |
| 93 | +Commit and push. Load the site, open DevTools → Network, confirm a `POST` to |
| 94 | +the worker returning `{"count":N}` and the badge appearing. |
| 95 | + |
| 96 | +## 7. Later changes |
| 97 | + |
| 98 | +Edit `src/index.js`, run `npx wrangler deploy` again. To read or reset the |
| 99 | +count: |
| 100 | + |
| 101 | +```bash |
| 102 | +npx wrangler kv key get --binding=HITS total --remote |
| 103 | +npx wrangler kv key put --binding=HITS total "0" --remote |
| 104 | +``` |
| 105 | + |
| 106 | +--- |
| 107 | + |
| 108 | +## What the number means |
| 109 | + |
| 110 | +**Counts:** |
| 111 | + |
| 112 | +- Every full page load from a browser that runs JS and isn't blocking the |
| 113 | + worker domain — once per tab session (the `sessionStorage` guard). |
| 114 | +- Repeat visitors: each new tab, and each new session in the same tab after |
| 115 | + the browser clears session storage. This is closer to "visits" than to |
| 116 | + "pageviews" or "unique visitors". |
| 117 | + |
| 118 | +**Doesn't count:** |
| 119 | + |
| 120 | +- **Bots and crawlers** — Googlebot, GPTBot, most scrapers: no JS execution, |
| 121 | + no fetch, no count. Headless browsers *will* count. This is a feature: your |
| 122 | + number stays closer to human traffic than a server log would. |
| 123 | +- **Ad blockers / privacy extensions** — uBlock Origin et al. block by |
| 124 | + hostname lists. `workers.dev` isn't on those lists today, so you'll lose far |
| 125 | + fewer hits than GoatCounter does. But a strict blocker or a `connect-src` |
| 126 | + CSP could still drop it, and then the badge just stays hidden. |
| 127 | +- **Prefetches / speculative loads** — Chrome's prerender runs JS, so a |
| 128 | + prerendered page that the user never visits *can* count. Small inflation, |
| 129 | + rare in practice. |
| 130 | +- **Hash-route navigation** — deliberately excluded by the session guard. |
| 131 | + Clicking around inside the Study Hub adds nothing. |
| 132 | +- **Users with JS off**, and anyone whose fetch fails for any reason. |
| 133 | + |
| 134 | +Net: treat the number as "browser sessions that got far enough to run the |
| 135 | +script". It will read **lower** than GoatCounter's pageview total (which counts |
| 136 | +every hash route change) and **higher** than its unique-visitor count. Keep |
| 137 | +GoatCounter open for referrers and per-page breakdowns — the beacon is |
| 138 | +untouched. |
| 139 | + |
| 140 | +## Free-tier limits |
| 141 | + |
| 142 | +Workers free plan, per day, per account: |
| 143 | + |
| 144 | +| Resource | Free allowance | |
| 145 | +|---|---| |
| 146 | +| Worker requests | 100,000 / day | |
| 147 | +| CPU time | 10 ms per request (this worker uses well under 1 ms) | |
| 148 | +| KV reads | 100,000 / day | |
| 149 | +| KV writes | **1,000 / day** | |
| 150 | +| KV storage | 1 GB | |
| 151 | + |
| 152 | +**The binding constraint is KV writes: 1,000/day.** One write = one counted |
| 153 | +visit. So you're fine up to roughly **1,000 new visits per day**; reads |
| 154 | +(repeat loads in an already-counted tab) are effectively free at this scale. |
| 155 | + |
| 156 | +Past that, writes start failing — the worker still returns the last known |
| 157 | +count, so the badge keeps working, it just stops rising until midnight UTC. |
| 158 | +If you ever get there, the fix is the Workers Paid plan ($5/mo) or the |
| 159 | +alternative below. |
| 160 | + |
| 161 | +There is also a hard KV limit worth knowing: **~1 write per second to the same |
| 162 | +key**. Sustained bursts above that get rate-limited. |
| 163 | + |
| 164 | +## The honest caveat about KV (read this) |
| 165 | + |
| 166 | +You asked for Worker + KV, and for a personal site it's fine. But KV is the |
| 167 | +wrong *primitive* for a counter, and I'd rather you know why: |
| 168 | + |
| 169 | +1. **Lost updates.** Read-modify-write isn't atomic. Two visitors landing |
| 170 | + within the same few hundred milliseconds both read `N` and both write |
| 171 | + `N+1`. You lose one. At your traffic this is a rounding error; at 10 |
| 172 | + concurrent visitors it's a visible undercount. |
| 173 | +2. **Eventual consistency.** KV reads are served from edge caches and can lag |
| 174 | + writes by up to ~60 seconds globally. A visitor in Cairo may see a number |
| 175 | + a minute behind one in Frankfurt. You specifically asked for "live, not |
| 176 | + hourly-cached" — KV gives you "usually within a minute", not "exact". |
| 177 | +3. **1,000 writes/day** as above. |
| 178 | + |
| 179 | +**Durable Objects solve all three.** A Durable Object is a single-threaded |
| 180 | +addressable instance with its own transactional storage — increments are |
| 181 | +genuinely atomic, reads are strongly consistent, and it's on the Workers free |
| 182 | +tier now (with a much higher write budget). It's about the same amount of code |
| 183 | +and the same `wrangler deploy`. |
| 184 | + |
| 185 | +If you want it, say so and I'll ship the DO version — same API shape, same |
| 186 | +front-end block, so nothing in `index.html` changes. I built KV because you |
| 187 | +specified it, and it will work; I just wouldn't call it the better design. |
| 188 | + |
| 189 | +## Security notes |
| 190 | + |
| 191 | +- No token, key, or secret is in `index.html`. The page sends an unauthenticated |
| 192 | + `POST` and reads a number back. That's the whole contract. |
| 193 | +- `Access-Control-Allow-Origin` is the exact site origin, never `*`, and |
| 194 | + `Vary: Origin` is set so no cache mixes responses across origins. |
| 195 | +- CORS is a *browser* control, not a security boundary. Anyone can `curl -X |
| 196 | + POST` your worker with a forged `Origin` header and inflate the count. For a |
| 197 | + visit badge that's acceptable; there's no way to prevent it without either a |
| 198 | + secret in the page (which you correctly ruled out) or Cloudflare Turnstile. |
| 199 | + If it ever gets abused, add a Cloudflare Rate Limiting rule on the worker |
| 200 | + route — it's free and takes two minutes. |
| 201 | +- `Cache-Control: no-store` on every response keeps browsers and Cloudflare's |
| 202 | + edge from serving a stale number. |
0 commit comments