数据格式
评论数据存放在 COMMENTS_PATH(默认 comments/)目录下。
comments/
index.json
hello-world.json
posts/another-post.json每篇文章一个聚合文件,而不是一条评论一个文件。原因:代理读下 N+1 请求成本太高,聚合成一个文件后读取成本与评论数无关。
文章文件
comments/<slug>.json:
json
{
"version": 1,
"post": "hello-world",
"updatedAt": "2026-10-30T05:00:00.000Z",
"comments": [
{
"id": "8c8f3690-7bb7-4add-901d-c829b337417e",
"parentId": null,
"name": "Alice",
"content": "写得好。",
"createdAt": "2026-10-30T04:12:00.000Z",
"emailEncrypted": "<base64>",
"emailKeyFingerprint": "a1b2c3d4"
}
]
}| 字段 | API 返回 | 说明 |
|---|---|---|
id | 是 | UUID v4,提交时生成,批准不变 |
parentId | 是 | 只指向同文件内已存在的评论;V1 最多两级 |
name | 是 | 纯文本 |
content | 是 | 纯文本,展示时转义,不解析 HTML |
createdAt | 是 | 提交时刻(UTC),不是批准时刻 |
emailEncrypted | 否 | AES-256-GCM,钥匙由 COMMENTS_SECRET 经 HKDF 派生 |
emailKeyFingerprint | 否 | 便于密钥轮换后识别旧密文 |
未填写邮箱时,三个邮箱相关字段全部省略。
索引文件
comments/index.json:
json
{
"version": 1,
"updatedAt": "2026-10-30T05:00:00.000Z",
"posts": {
"hello-world": { "count": 12, "updatedAt": "2026-10-30T05:00:00.000Z" }
}
}索引只提供站点级计数,不参与评论读取。极端情况下它可能短暂落后于文章文件,以文章文件为准。
slug 规则
- 允许字符:小写字母、数字、
-、_、/ - 以字母或数字开头
- 最长 179 字符
- 禁止
..、禁止绝对路径 - 结尾的
.json会被剥离
widget 默认取 location.pathname 并按同一套规则规范化。
并发
写入使用文件 sha 做乐观锁。同一篇文章两条评论同时批准时,后者会遇到 sha 不匹配,服务端重读后重试(有限次)。
索引文件是独立写入,失败只记日志,不会让批准本身失败。
Git 历史永久保留
删除评论只删当前版本,Git 历史仍然存在。表单必须写明这一点,任何抓取仓库历史的人都能看到已删除内容。