告别不蒜子,自己给博客记个数
不蒜子好像又挂掉了。
之前已经遇到过几次。网页能打开,文章能看,偏偏浏览量那里空着。有时候浏览器还转很久,检查请求,才发现统计接口没有正常返回。以前看到过 504 Gateway Timeout,这次又得等。
本来想,免费的东西,等等也没什么。但一会儿有数字,一会儿没有,总归有点难受。于是又问 Codex:能不能用 GitHub、Cloudflare,或者办公室的Linux,自己记录一下?ai agent 我是离不开了。
不过有个前提:不能一换服务,之前的数字就全变成零。虽然也不是什么惊人的访问量,但好歹积累了这么久啊。
最后折腾成了下面这个样子AI帮忙折腾的,我自己估计得折腾很久。记一下,省得下次再问一遍。
网页还在 GitHub,计数交给 Cloudflare
博客仍然是Markdown写文章,Python生成网页,再把静态网站部署到GitHub Pages。
另外加一个 Cloudflare Worker 接收访问请求,用D1数据库保存计数。大概是这样:
flowchart TD A[读者打开博客] --> B[GitHub Pages 提供网页] B --> C[浏览器加载计数脚本] C --> D[Cloudflare Worker 校验请求] D --> E[D1 保存访问记录并累计] E --> F[返回数字并显示在页面上]
这里的 Worker 不是再转头去问不蒜子,而是操作自己的数据库。否则只是多绕了一层,原来的服务不返回,最后还是没有数字。
没有把主服务放在办公室的 Fedora 上,是因为它也有自己的脾气。办公室可能停电,网络也可能断,电脑还得重启。之前图片服务就用了这台机器,不想再让访问统计也跟着它一起上下班。
这也不代表换到 Cloudflare 就永远不会出问题。只是现在计数保存在哪里、如何备份,我至少能自己查了。
先把授权弄明白
管理 Worker 和 D1 用的是 Cloudflare 的官方工具 Wrangler。这次使用的是 4.154.0。下面的命令都在博客项目根目录运行,默认已经有 Node.js 和 npm。
一开始,授权页面显示成功了,本机却没有拿到凭据。还有一次浏览器跳到 localhost:8976,直接提示:
This site can't be reached
localhost refused to connect
ERR_CONNECTION_REFUSED
这个地址是登录过程临时使用的回调地址,不是博客挂了。但只看浏览器上的“授权成功”也不够,还得确认命令行真的接到了。
后来换成官方的设备授权方式,不再依赖浏览器访问本机回调端口:
npx wrangler@4.154.0 login --device --use-keyring \
--scopes account:read user:read workers_scripts:write \
workers_routes:write d1:write zone:read
终端会给出设备码和授权页面。在浏览器里核对设备码,确认需要的权限,再点击 Authorize。等待中的命令不要提前关掉;这次也确实遇到过授权页报错,不能保证换一种方式就一次成功。
最后终端返回 Successfully logged in.,再检查:
npx wrangler@4.154.0 whoami
确认账号和权限都对,这才算连上了。--use-keyring 用于把凭据以加密文件配合系统钥匙串保存,不把登录密钥写进博客仓库。设备码、密钥和完整授权回调地址,也不需要贴进文章里。
数据库、脚本和部署配置
这次新建了独立的 Worker 和 D1 数据库,都叫 seisamuse-counter-d1。没有覆盖原来的旧 Worker。
相关文件各有分工:
| 文件 | 用途 |
|---|---|
scripts/article_counter_20261028.js | Worker 的请求处理和计数逻辑 |
scripts/article_counter_schema_20261028.sql | 数据表和触发器 |
scripts/article_counter_manage_20261028.py | 本地初始化、历史基数导入和备份工具 |
scripts/article_counter_worker_20261028.toml | 云端绑定与部署配置 |
content/scripts/article_counter_20261028.js | 浏览器端发送请求、显示数字 |
data/counter_20261028/history.json | 手动确认的历史估算参数 |
这些是本次配套写好的脚本,下面的命令是在已有它们的前提下记录配置,不是只复制一个 TOML 文件就能得到计数器。
下面创建数据库和导入初始数据的操作,只用于第一次配置的空数据库。现在的数据库已经在计数,以后维护不要整段重新执行。
先创建数据库:
npx wrangler@4.154.0 d1 create seisamuse-counter-d1 \
--config scripts/article_counter_worker_20261028.toml
把返回的数据库 ID 填进 TOML。公开记录里用占位符代替实际 ID,主要配置如下:
name = "seisamuse-counter-d1"
account_id = "替换为自己的账号ID"
main = "article_counter_20261028.js"
compatibility_date = "2026-10-28"
workers_dev = false
[vars]
COLLECTION_ENABLED = "false"
[[d1_databases]]
binding = "DB"
database_name = "seisamuse-counter-d1"
database_id = "替换为新建数据库的ID"
[[ratelimits]]
name = "LIMITER"
namespace_id = "20261028"
simple = { limit = 300, period = 60 }
[observability]
enabled = false
[[routes]]
pattern = "stats.seis-jun.xyz"
custom_domain = true
main 相对于这个配置文件所在的目录;DB 和 LIMITER 则对应脚本使用的绑定。域名也要换成自己账号中可用的域名,不要照抄我的地址。
这里先把采集关着。数据库还没准备好,就别着急往里面写。
初始化时,先从生成的 site/ 整理页面路径,在本地创建数据库,再导入手动确认的历史基数。管理工具的处理顺序是:
init → import-history(先做完整备份)→ export-seed
初始化时间需要明确指定,历史参数文件中的时间也要与它一致。这次保存的是 2026-10-28T09:18:11.000Z,即北京时间 17:18:11。它是新库的初始化边界,不是从原统计服务导出的时间。
export-seed 生成 SQL 后,再导入刚创建的空 D1:
npx wrangler@4.154.0 d1 execute seisamuse-counter-d1 --remote \
--config scripts/article_counter_worker_20261028.toml \
--file data/counter_20261028/initial-seed.sql
本地 .sqlite 文件不是直接上传就能用,D1 导入需要 SQL;这点也可以查官方的导入导出说明。初始 SQL 不是日常发布文件,更不能拿它反复初始化正在使用的库。
然后部署 Worker:
npx wrangler@4.154.0 deploy \
--config scripts/article_counter_worker_20261028.toml
先在采集关闭的状态下检查绑定和历史基数,确认没问题,再把 COLLECTION_ENABLED 改为 "true",重新部署。这样不至于一边测试,一边把测试访问也算进去。
网页怎么接上,怎么检查
浏览器端的入口放在公共模板 templates/base.html 中,所以文章页和其他页面不需要各自抄一遍:
?v=2 是这次修改显示样式后加的版本标记,避免浏览器还沿用旧脚本。这个页面不再加载旧计数入口,也不在新接口失败后偷偷回退去请求原服务。
接口分成三种,不要拿错了:
POST /count:正式网页提交一次浏览,写入计数。GET /views?path=/:只查询某个路径的统计,不增加计数。GET /health:检查服务绑定和采集开关,不查询数据库里的数值。
平时排查可以先用这两个只读请求:
curl --max-time 15 'https://stats.seis-jun.xyz/health'
curl --max-time 15 'https://stats.seis-jun.xyz/views?path=/'
health 返回正常,并不等于数据库查询一定正常,还要看 views。第一次导入后,实际核对到历史浏览量是 80500、历史访客数是 40500,新增计数都是零,再开启采集。
本地 localhost 或 127.0.0.1 预览只读取统计,不提交浏览。否则我自己改个排版、刷新几十次,最后还以为突然来了不少读者。
正式发布一篇新文章时,Worker 会在首次计数前检查它是否确实是正式站点上能访问的 HTML 页面,成功后再登记。以后正常写文章,不需要每次手动把地址填进数据库。
旧数据没有想象中那么好搬
这次最纠结的其实不是部署,是旧数字怎么办。
我记得前几天总浏览量有八万零几百,访客数有四万零几百。最后确认,以 80,500 次浏览、40,500 位访客 作为历史起点。
但这两个数是根据回忆确认的估算,不是从不蒜子后台完整导出的精确记录。新系统把历史基数和新增记录分开保存,再加起来显示,不假装这些数字全是自己从头数出来的。
单篇文章更麻烦。我并没有每篇文章的可靠旧计数,因此没法把八万多次平均分给所有文章,也不能把未知的旧浏览量解释成零。现在单篇页面显示的,是新系统启用后记录到的浏览量,不是这篇文章从发表至今的累计数。
页面上最后只保留数字,估算来源和单篇统计的起点放在悬停提示及隐私说明里。去掉前面的提示词,只是改了显示方式,不是把旧数据变出来了。
以后如果原服务恢复,并且确实能读到可核实的单篇记录,再单独判断能不能补回、怎样避免重复。现在没有安排自动抓取,也不为了等它恢复而停掉新的计数。这个遗憾先留着。
还有,访客数也不等于精确的人数。目前是在浏览器本地保存一个有效期为 365 天的随机编号,服务端保存它的 SHA-256 摘要。同一个编号再次访问,不再新增一个访客。
清除网站数据、更换浏览器或设备,都可能产生新的编号;旧访客与新编号之间也无法去重。浏览器禁止本地存储时,则只记浏览量,不把每次访问都硬算成一个新访客。这个数字适合大概看看,不适合拿来做人口普查。
计数数据库不保存 IP、User-Agent 或随机浏览器编号原文,也不发送网址里的查询参数和页内锚点。不过请求经过 Cloudflare,它在网络传输和安全防护时仍可能处理网络信息,不能因此说成完全没有隐私问题。换了统计方式,隐私说明也得跟着更新。
以后要记得备份
现在数字存进了自己的数据库,至少可以把它导出来了。完整备份用:
npx wrangler@4.154.0 d1 export seisamuse-counter-d1 --remote \
--config scripts/article_counter_worker_20261028.toml \
--output backups/counter-full-20261028-203000.sql
这个输出名只是示例,每次要换成新的日期时间。备份放在非公开目录,不跟着网页发布,也不要覆盖上一次的文件。云端导出会暂时阻塞数据库的其他请求,尽量挑访问少的时候做。
还要分清楚:管理脚本的 export 是 JSON 汇总,不是完整备份;backup/restore 面向完整的本地 SQLite;云端完整备份则是上面的 D1 SQL 导出。要恢复时,先导入一个新数据库,核对好了再切换,不直接覆盖正在计数的库。
这次已经导出过云端初始备份,并实际导回本地检查过完整性,两个历史基数都还在。不过定时备份目前还没设置,不能写了“以后要备份”,就当它已经每天自动备份了。
以后发博客,仍然运行原来的 do_deploy.bash。它负责构建和发布静态网页;只有计数服务的代码或配置变了,才另外部署Worker。网页源码、数据库记录和数据库备份,是三件不同的东西。
当然,这个小计数器也不是绝对准确。统计请求可能被拦截,网络会出错,机器人也可能混进来;限流不是完全防刷的保证。接口暂时不可用时,脚本最多等待八秒,有最近的成功缓存就显示缓存,并在悬停提示里注明时间,不一直等,也不自动反复重试。
先这样吧。博客照常写,数字继续往下记。比起盯着数字不动,我更希望下一次打开这些配置,是为了备份,而不是又来查哪里挂了。