尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證Cloudflare Worker dashboard aggregating Web Analytics traffic, Google Search Console keywords, and Discourse forum activity across a domain portfolio into daily D1 snapshots with anomaly signals and SEO opportunity ranking.
翻譯準備中
取自儲存庫 README
正在載入完整 README。
檢視完整 README ↗A serverless Cloudflare Worker that aggregates Cloudflare Web Analytics (RUM traffic + referrers), Google Search Console (keywords, CTR, ranking position), and Discourse forum activity for a portfolio of domains into one self-hosted daily dashboard — no external analytics SaaS, no client-side tracking script. A Cron Trigger pulls everything nightly, writes a snapshot per domain into D1 (SQLite at the edge), and renders instantly from stored history with 14-day sparklines, anomaly detection, and ranked SEO opportunities. Live example: https://stats.davidveksler.com.
david-stats-cf-serp).Cron 13:00 UTC ─┐
├─> Worker (src/index.js runDaily)
/run?key=… ───┘ ├─ pullTraffic() → Cloudflare GraphQL (all 4 accounts)
├─ querySearchSummary()/queryKeywords()/queryPages() → Google Search Console
├─ write snapshot → D1 (traffic, referrers, queries, pages)
└─ sendNtfy() → ntfy.sh/david-stats-cf-serp
GET / → loadDashboard() reads D1 → renderDashboard() HTML
GET /api/json → same data as JSON (`period`, `domain`, and `sort` query params)
GET /health → "ok"
GET /run?key=… → manual re-pull (needs REFRESH_KEY secret)
Files: src/config.js (domains + accounts), src/cloudflare.js (RUM pull),
src/gsc.js (Search Console + JWT auth), src/render.js (HTML), src/index.js (handlers).
Edit SITES in src/config.js. host = the Cloudflare Web Analytics requestHost;
gsc = the exact Search Console property string (sc-domain:… or a URL prefix).
Set gscPageFilter to an RE2 expression when a broad property needs to be limited to
specific page URLs. The davidveksler.com card uses this to exclude subdomains.
The nightly job runs headless, so it can't use an interactive Google login. It uses a
service account (GSC_SA_KEY, now set). If GSC_SA_KEY is ever missing, traffic/referrers
still work fully and keywords are simply skipped (the ntfy push notes it). To rotate the key,
repeat these steps and run ./deploy.sh --gsc-key path/to/key.json.
client_email (looks like [email protected]) → Restricted (read) is enough.
Do this for every configured property.export CLOUDFLARE_API_TOKEN=<your-cf-token>
wrangler secret put GSC_SA_KEY < path/to/key.json
curl "https://stats.davidveksler.com/run?key=$REFRESH_KEY"
# gscOk should be true; the ntfy warning disappears
Use the deploy script — idempotent, parses the CF token from ~/Projects/.cloudflare.env,
ensures secrets, deploys, and smoke-tests:
./deploy.sh # install, ensure secrets, deploy, verify /health
./deploy.sh --refresh # ...then trigger a live pull (/run) and print the result
./deploy.sh --gsc-key key.json # also (re)set the GSC service-account secret
./deploy.sh --schema # also (re)apply schema.sql — needs a D1:Edit-scoped token
Refresh the stored stats without deploying. The command reads the gitignored
.deploy/refresh_key.txt, sends a browser user-agent to avoid WAF rule 1010, prints the
JSON result, and exits nonzero when the request or GSC portion fails:
npm run refresh
./scripts/refresh-stats.sh
.\scripts\refresh-stats.ps1
Use npm run refresh -- --help for endpoint, key-file, timeout, and environment overrides.
From Windows PowerShell, use the Git Bash wrapper. It avoids accidentally selecting WSL's older Node runtime and forwards the same options:
.\deploy.ps1
.\deploy.ps1 -Refresh
.\deploy.ps1 -Schema
.\deploy.ps1 -GscKey C:\path\to\key.json
Manual bits:
export CLOUDFLARE_API_TOKEN=<first token in ~/Projects/.cloudflare.env>
npm run deploy # raw wrangler deploy
npm run tail # live logs
Secrets (set once via wrangler secret put, or auto-provisioned by deploy.sh):
CF_API_TOKEN — Cloudflare token with Account Analytics: Read (the Worker's GraphQL calls). ✅ setREFRESH_KEY — protects GET /run and POST /ingest-bing. ✅ set (saved locally in .deploy/refresh_key.txt, gitignored)GSC_SA_KEY — Google service-account JSON. ✅ setBING_API_KEY — Bing Webmaster Tools API key. Set for /bing-sites and the (expected-to-fail,
diagnostic-only) /run-bing — see below for why the real nightly Bing pull doesn't use it from here.Note: the analytics token deploys the Worker and binds D1 at runtime, but is not scoped for D1 management writes, so
d1 execute --remote/--schemacan fail with codes10000or7500. Additive dashboard migrations are also applied idempotently through the Worker's runtime D1 binding before a data pull.
Vars (in wrangler.jsonc): NTFY_TOPIC = david-stats-cf-serp.
The Bing pull runs in GitHub Actions, not on a Worker cron (.github/workflows/bing-pull.yml,
scripts/bing-pull.mjs) — Bing throttles every call routed through Cloudflare Workers' shared egress
IPs (ErrorCode 17 "ThrottleIP"), confirmed 2026-09-11 to be independent of the account, key, or
pacing. The Action fetches from Bing and POSTs the results to POST /ingest-bing?key=<REFRESH_KEY>,
which only writes them to D1. Requires two GitHub repo secrets (Settings → Secrets and variables →
Actions): BING_API_KEY and STATS_REFRESH_KEY (the same value as the Worker's REFRESH_KEY).
The Action posts to the Worker's *.workers.dev URL, not stats.davidveksler.com — that zone has
Bot Fight Mode on, which JS-challenges the GitHub runner's IP (a separate issue from the Bing-side
throttle above, found right after that fix shipped). workers_dev: true in wrangler.jsonc serves
the same Worker there too; REFRESH_KEY still gates /ingest-bing regardless of hostname. See the
AGENTS.md gotcha for the full diagnosis.
stats-dashboard on account David Veksler's Websites (556c237bf8cb62edb8f7b401499bb7a9)stats-dashboard (2f5ea431-472e-462f-94a4-b396576c1a5b)stats.davidveksler.com0 13 * * *davidveksler.com GSC property is a broad sc-domain: property, but its dashboard query
filters the page dimension to root-domain URLs. This keeps its keywords separate from
cheatsheets.davidveksler.com without requiring another GSC property.DELETE if desired.