Serverless analytics powered by Cloudflare Workers + KV + D1 with a beautiful, bilingual dashboard.
想看繁體中文介紹?請參考README.zh-TW.md。
- Edge-native analytics – one Worker serves the REST APIs and the dashboard from 300+ Cloudflare PoPs.
- Privacy first – no cookies, IPs are SHA-256 hashed and truncated after 24h.
- Realtime dashboards – brand-new v1.6.0 UI with dark/light mode, i18n toggle, health indicator, and Chart.js trends.
- KV + D1 hybrid storage – KV keeps blazing-fast counters while D1 stores historical daily and “top pages” aggregates.
- Self-healing storage –
/api/topautomatically backfills D1 from KV, plus documented playbook for zeroing counters safely. - Hugo friendly – drop-in helper script keeps Blowfish placeholders in sync; zero custom CSS required.
- Works offline – every API call adds a timestamp so the dashboard can show last-updated status even on stale networks.
stats.example.com/ → Dashboard UI (this file)
stats.example.com/api/count → Increment PV/UV counters (KV)
stats.example.com/api/stats → Page or global stats (KV)
stats.example.com/api/batch → Batch lookups for multiple paths
stats.example.com/api/top → Top pages (D1, optional)
stats.example.com/api/daily → Daily PV/UV trend (D1)
stats.example.com/health → Health check + Worker version
All endpoints return JSON and include permissive CORS headers (Access-Control-Allow-Origin: *).
Requirements: Node.js ≥ 18,
wranglerCLI ≥ 3.0.
- Clone and enter the repository.
git clone https://github.com/Zakkaus/cloudflare-stats-worker.git cd cloudflare-stats-worker - Install (or upgrade) Wrangler and log in once.
npm install -g wrangler wrangler login
- Create the KV namespace and paste the IDs into
wrangler.toml.wrangler kv namespace create PAGE_STATS wrangler kv namespace create PAGE_STATS --preview
- (Optional) Enable the D1-backed features.
Uncomment the
wrangler d1 create cloudflare-stats-top wrangler d1 execute cloudflare-stats-top --file=schema.sql --remote
d1_databasessection inwrangler.tomland set the generated IDs. - Deploy 🎉
wrangler deploy
The Worker URL (e.g. https://cloudflare-stats-worker.your-subdomain.workers.dev) will now serve the dashboard at / and every API route under /api/*.
- Responsive glassmorphism UI with live PV/UV cards, API health, and UTC timestamps.
- 7/14/30 day trend selector powered by Chart.js, with graceful zero-state fallbacks.
- Instant search for any page path – enter
/zh-cn/posts/gentoo-optimization/and get normalized stats. - Top 10 list with PV/UV badges, ranking, and automatic language switching.
- Dark/light mode toggle + language toggle (Traditional Chinese ⇄ English) with localStorage persistence.
Try it live: https://stats.zakk.au/
| Method | Path | Description | Writes |
|---|---|---|---|
GET |
/api/count?url=/path/ |
Increment counters for /path/ and return page + site totals |
✅ |
GET |
/api/stats?url=/path/ |
Fetch stats for a specific path | ❌ |
GET |
/api/stats |
Fetch site-wide totals | ❌ |
GET |
/api/batch?urls=/, /about/ |
Batch lookup (max 50 paths) | ❌ |
GET |
/api/top?limit=10 |
Top pages ordered by PV (requires D1) | ❌ |
GET |
/api/daily?days=7 |
Daily PV/UV trend (requires D1) | ❌ |
GET |
/health |
Worker status + semantic version | ❌ |
Every response contains a UTC timestamp so clients can display “last updated” information.
Because the Worker will repopulate D1 from KV when
/api/topsees an empty table, always clear KV first, then D1.
-
Remove every key from the production KV namespace:
# list first (optional) wrangler kv key list --binding=PAGE_STATS --preview false --remote # delete individual keys wrangler kv key delete "page:/posts/example/:pv" --binding=PAGE_STATS --preview false --remote # or iterate in a shell session # Cloudflare Stats Worker [](https://github.com/Zakkaus/cloudflare-stats-worker/releases) [](LICENSE) [](https://deploy.workers.cloudflare.com/?url=https://github.com/Zakkaus/cloudflare-stats-worker) > Serverless analytics powered by Cloudflare Workers + KV + D1 with a polished, bilingual dashboard. > Looking for the Traditional Chinese guide? See [`README.zh-TW.md`](README.zh-TW.md). --- ## Table of contents - [Why choose Cloudflare Stats Worker](#why-choose-cloudflare-stats-worker) - [Free tier and upgrade options](#free-tier-and-upgrade-options) - [Architecture & data flow](#architecture--data-flow) - [Dashboard highlights](#dashboard-highlights) - [Prerequisites](#prerequisites) - [Step 1: Clone the repository](#step-1-clone-the-repository) - [Step 2: Run the install script](#step-2-run-the-install-script) - [Step 3: Verify the APIs](#step-3-verify-the-apis) - [Step 4: Import the Hugo client script](#step-4-import-the-hugo-client-script) - [Step 5: Override Blowfish templates](#step-5-override-blowfish-templates) - [Step 6: Test locally](#step-6-test-locally) - [Step 7: Embed the dashboard page](#step-7-embed-the-dashboard-page) - [API quick reference](#api-quick-reference) - [Operations cheat sheet](#operations-cheat-sheet) - [FAQ](#faq) --- ## Why choose Cloudflare Stats Worker - **One Worker = APIs + dashboard + data sync** – deploy once and ship `/api/*` plus a hosted dashboard. - **Privacy first** – zero cookies, IP addresses are SHA-256 hashed and truncated; data retention is entirely under your control. - **Internationalization aware** – `normalizePath()` merges `/zh-tw/posts/foo/`, `/posts/foo/`, and `/posts/foo/index.html` into the same key. - **Starts free** – the Cloudflare free tier easily covers personal blogs and most small sites. - **Blowfish ready** – shipping client script + partial overrides tailored for the Hugo Blowfish theme. ## Free tier and upgrade options | Service | Free allowance | When to upgrade | |---------|----------------|-----------------| | **Workers** | 100k requests/day, 10ms CPU | Upgrade to Workers Paid ($5/mo) when daily traffic exceeds 100k or you need larger CPU headroom. | | **KV** | 1 GB storage, 100k reads / 1k writes per day | Move to the paid bundle when you store large JSON payloads or retain long-tail history. | | **D1** | 5 M queries/month, 1 GB storage | Switch to D1 Paid for heavier Top 10 usage or long-running trend queries. | > D1 is optional. If you only need real-time PV/UV counting, KV is sufficient and the dashboard still works (minus Top 10 and trends). ## Architecture & data flow ```mermaid graph LR Browser[Visitor] -->|/api/count| Worker Browser -->|/api/batch| Worker Dashboard[stats subdomain] -->|/api/stats /api/daily /api/top| Worker Worker -->|read/write| KV[(Cloudflare KV)] Worker -->|optional| D1[(Cloudflare D1)]
- Every endpoint returns JSON and ships with permissive CORS headers (
Access-Control-Allow-Origin: *). - When
/api/topfinds an empty D1 table it automatically backfills from KV to keep the dashboard stable. - Built-in caching keeps
/api/stats,/api/daily, and/api/topcost-efficient without sacrificing freshness.
- Every endpoint returns JSON and ships with permissive CORS headers (
See it live: stats.zakk.au
-
Glassmorphism cards, light/dark theme toggle, instant zh‑TW ⇄ EN locale switch.
-
Today/sitewide PV/UV, API health badge, last-updated timestamp (UTC).
-
7/14/30-day trend charts (Chart.js) with clean zero-state fallbacks.
-
Top 10 list with badge counts, quick path search, deep links to posts.
-
Deploys as a standalone website—optionally embed via iframe/shortcode in any site. ## Prerequisites
- Cloudflare account (free plan is fine).
- Node.js ≥ 18 and
wranglerCLI ≥ 3.0 installed. - Git and a POSIX shell (macOS, Linux, or WSL).
- To bind a custom subdomain like
stats.example.com, ensure the apex domain is proxied by Cloudflare.
git clone https://github.com/Zakkaus/cloudflare-stats-worker.git cd cloudflare-stats-workerProject layout essentials:
src/index.js– Worker entrypoint with routing, cache invalidation, and D1 sync logic.dashboard/– static assets for the built-in dashboard.scripts/– automation helpers for deploy, verification, and cleanup.schema.sql– D1 schema creatingpage_statsandsite_daily_stats.
chmod +x scripts/deploy.sh ./scripts/deploy.sh --domain stats.example.com
The script will:
- Check that Wrangler is authenticated (prompting for
wrangler loginif needed). - Create the KV namespace and persist IDs back into
wrangler.toml. - Apply
schema.sqlwhen a D1 binding is present (optional but recommended). - Deploy the Worker and print out the dashboard / API URLs.
Prefer manual setup? Execute the steps individually:
wrangler kv namespace create PAGE_STATS wrangler kv namespace create PAGE_STATS --preview wrangler d1 create cloudflare-stats-top # optional wrangler d1 execute cloudflare-stats-top --file=schema.sql --remote wrangler deploycurl https://stats.example.com/health curl "https://stats.example.com/api/count?url=/" | jq curl "https://stats.example.com/api/stats" | jq curl "https://stats.example.com/api/top?limit=5" | jq
Or run the bundled smoke test:
./scripts/verify.sh https://stats.example.com
The script checks every endpoint, validates cache headers, and surfaces common misconfigurations.
- Copy
client/cloudflare-stats.jsinto your Hugo project (e.g.assets/js/cloudflare-stats.js). - Add the script to
layouts/partials/extend-head.html:{{ $stats := resources.Get "js/cloudflare-stats.js" | resources.Minify | resources.Fingerprint }} <script defer src="{{ $stats.RelPermalink }}" data-api="https://stats.example.com" data-site="https://zakk.au"></script> - Rebuild Hugo and confirm the PV placeholders on article pages animate in.
The client scans
span[id^="views_"]/likes_, normalizes paths, and uses/api/countor/api/batchto hydrate values with graceful fallbacks.To keep multilingual pages consolidated under the same key, override these files:
layouts/_default/list.htmllayouts/_default/single.htmllayouts/partials/meta/views.htmllayouts/partials/meta/likes.html
Core snippet:
{{- $path := partial "stats/normalize-path" . -}} <span id="views_{{ $path }}" class="views-counter animate-pulse">—</span>The
stats/normalize-pathpartial strips language prefixes and/indexendings so/zh-tw/posts/foo/and/posts/foo/share the same KV entry.wrangler dev # in a separate terminal hugo server -D- Browse locally and confirm
/api/countand/api/batchrespond with HTTP 200. - Use
npx autocannon https://stats.example.com/api/count?url=/to probe rate limiting if needed. - Stream logs with
wrangler tailto watch KV/D1 interactions in real time.
Once deployed, visit your dashboard domain directly:
https://stats.example.com/
You'll see the same interface as stats.zakk.au:
- Real-time today/site PV・UV cards
- API health status indicator
- 7/14/30-day trend charts
- Top 10 pages ranking
- Dark/light theme and locale switcher
Optional: Embed in Hugo
Want to embed the dashboard in a blog page? Use the provided shortcode:
{{< statsDashboard url="https://stats.example.com" heightClass="md:h-[1200px]" >}}-
The shortcode lives in
layouts/shortcodes/statsDashboard.htmlwith responsive, dark-mode aware styling. -
Use it inside
content/stats/index.*.mdto create a/stats/page on your site. -
Want full control? Port the
dashboard/assets into Hugo partials or a standalone SPA. ---Method Path Description Writes Default cache GET/api/count?url=/path/Increment PV/UV and return page + site totals ✅ ❌ GET/api/stats?url=/path/Fetch stats for a specific path ❌ ✅ (30 s) GET/api/statsFetch site-wide totals ❌ ✅ (30 s) GET/api/batch?urls=/,/about/Batch lookup (≤ 50 paths) ❌ ✅ (30 s) GET/api/top?limit=10Top pages ordered by PV (needs D1) ❌ ✅ (60 s) GET/api/daily?days=7Daily PV/UV trend (needs D1) ❌ ✅ (30 s) GET/healthHealth check + semantic version ❌ ❌ All responses include a UTC
timestamp, making “last updated” displays trivial.Because
/api/topbackfills D1 from KV, always wipe KV first, then clear D1.# 1. Delete every key in the production KV namespace wrangler kv key list --binding=PAGE_STATS --preview false --remote \ | jq -r '.[].name' \ | xargs -I{} wrangler kv key delete "{}" --binding=PAGE_STATS --preview false --remote # 2. Truncate D1 tables wrangler d1 execute cloudflare-stats-top --command "DELETE FROM page_stats;" --remote wrangler d1 execute cloudflare-stats-top --command "DELETE FROM site_daily_stats;" --remote # 3. Double-check wrangler kv key list --binding=PAGE_STATS --preview false --remote wrangler d1 execute cloudflare-stats-top --command "SELECT COUNT(*) AS count FROM page_stats;" --remote curl -s https://stats.example.com/api/top?limit=5
- Inspect KV for leftover
page:*keys – clearing D1 alone will rehydrate from KV. - Cache TTL is 60 s; either wait it out or hit
/healthto trigger invalidation. - Ensure no other environments (e.g.
wrangler dev) are writing stats back in.
Why not Google Analytics?
Cloudflare Stats Worker is cookie-free, works in China, and keeps raw numbers under your control—all while running on the free tier.Will the dashboard slow my site?
The article script loads withdeferand batches requests, so above-the-fold performance is unaffected. The dashboard itself can be embedded via iframe to isolate resources.Can I extend the data model?
Absolutely. Add new D1 tables, store richer JSON blobs in KV, or hook into Cloudflare Queues/Scheduled Jobs for offline processing.How do I exclude internal traffic?
Add allow/deny logic insideenforceRateLimitorhandleCount—for example, skip counting specific IP ranges or User-Agents.
- Changelog: see
CHANGELOG.md. - License: MIT (see
LICENSE).
- Inspect KV for leftover