起因
博客上线一年,从来没有评论功能。读者反馈只能去长毛象上 @ 我或者私信。一直想加个评论框,但有两条硬要求:
我想要显示长毛象上的评论,但是因为不是永远转发,所以也希望在网页上有评论界面,希望大家评论的时候最简单方便不要登录自己的账号登录半天。
也就是:
- 网站自带评论框 —— 留言不要求登录,最好填个昵称就能发
- 长毛象评论也显示 —— 我转发的那条 toot 下面的回复,自动同步到对应文章页面
方案
Claude 给了 5 种主流方案的对比:
| 方案 | 用户门槛 | 部署复杂度 | dream 主题原生支持 |
|---|---|---|---|
| Utterances | 需 GitHub 账号 | ⭐ 5 分钟 | ✅ |
| Disqus | 任意社交账号 + 广告 | ⭐ 10 分钟 | ✅ |
| Waline | 邮箱即可,可匿名 | ⭐⭐⭐ 30-60 分钟 | ✅ |
| Twikoo | 邮箱即可,可匿名 | ⭐⭐ 20-30 分钟 | ✅ |
| 长毛象评论 | 需长毛象账号 | ⭐⭐⭐ 自己写 | ❌ |
Waline 是中文圈最成熟的"可匿名 + 自托管"方案,dream 主题内置支持。长毛象评论需要自己写。两者叠加用:
- 文章底部:Waline 评论框(永远显示)
- 如果 frontmatter 加了
mastodon: <toot-id>,再加一块:长毛象评论区(拉取 toot 的回复)
后端选型
Waline 需要一个后端服务(Node API + 数据库)。问 Claude 部署位置时坚持要统一在 Cloudflare 生态(和博客 + team_workout_data 一致)。
Claude 一开始给的 npm install @waline/cloudflare 命令直接报 404 —— 这个包根本不存在。我自己搜了一下找到 wuyilingwei/Waline_On_Worker
:
一个运行在 Cloudflare Workers 上的 Waline 评论系统后端实现,使用 D1 (SQLite) 作为数据存储。实现了 Waline 的绝大多数功能。
这是个第三方 port,作者明确声明是"AI 辅助开发,生产前自行评估风险"。对个人博客这种低流量场景接受。
部署 Waline 后端
整个流程(用了 ~20 分钟):
# 1. 装 pnpm(之前没用过)
brew install pnpm
# 2. 克隆 repo 到统一的 GitHub 文件夹
cd ~/Documents/GitHub/
git clone https://github.com/wuyilingwei/Waline_On_Worker.git
cd Waline_On_Worker
# 3. 装依赖
pnpm install
# 报错:[ERR_PNPM_IGNORED_BUILDS] esbuild、sharp、workerd 需要构建脚本批准
pnpm approve-builds # 交互式批准这三个 native 包
# 4. 创建 D1 数据库
npx wrangler d1 create waline-db
# 输出 database_id = "1c35622c-2a7e-48a0-9132-b7ec418b932a"
第 5 步发现 repo 里没有 wrangler.toml 模板。Claude 让我手动建一个:
name = "waline-comment"
main = "src/index.ts"
compatibility_date = "2024-12-01"
[[d1_databases]]
binding = "DB"
database_name = "waline-db"
database_id = "1c35622c-2a7e-48a0-9132-b7ec418b932a"
[vars]
SITE_NAME = "Leona的田园牧歌"
SITE_URL = "https://shaohua2kuaiqian.com"
SECURE_DOMAINS = "https://shaohua2kuaiqian.com"
注意 SECURE_DOMAINS 要完整 URL 含协议(后面会讲为什么)。
继续:
# 6. 初始化 D1 数据库 schema
pnpm run db:init
# ⚠️ 这条命令默认跑在本地开发数据库!schema 没到线上!
# 7. 生成 JWT 密钥
openssl rand -hex 32
# 复制输出
npx wrangler secret put JWT_SECRET
# 粘贴
# 8. 部署 Worker
pnpm run deploy
# 输出:https://waline-comment.leonashao.workers.dev
# 9. 浏览器打开 /ui/register 注册第一个用户(自动成为 admin)
第一个坑:500 Internal Server Error
注册时 500 报错。Claude 排查后让我跑:
npx wrangler d1 execute waline-db --remote --command "SELECT name FROM sqlite_master WHERE type='table';"
输出:
┌────────┐
│ name │
├────────┤
│ _cf_KV │
└────────┘
确诊:线上 D1 数据库完全是空的,只有 Cloudflare 自动加的 _cf_KV 表。第 6 步的 pnpm run db:init 在 package.json 里是 wrangler d1 execute waline-db --file=./schema.sql —— 没有 --remote,新版 wrangler 默认跑在本地开发库,线上库根本没初始化。
修复:
npx wrangler d1 execute waline-db --remote --file=./schema.sql
注意 --remote。再去 /ui/register 注册成功。
Hugo 这边的集成
后端跑通后,前端要把评论框接上:
# hugo.toml
[params]
waline = true
walineServer = "https://waline-comment.leonashao.workers.dev"
dream 主题内置支持 waline = true,但默认 init 配置不够个性化,所以覆盖了 layouts/partials/waline.html:
<div id="waline" class="waline-wrapper"></div>
<script type="module">
import { init } from 'https://unpkg.com/@waline/client@v3/dist/waline.js';
init({
el: '#waline',
serverURL: "https://waline-comment.leonashao.workers.dev",
dark: 'html.dark',
lang: 'zh-CN',
requiredMeta: [], // 都不强制;昵称留空即匿名
pageview: false, // 浏览量交给 GA
reaction: false, // 关掉评论赞踩
meta: ['nick', 'mail'], // 只显示昵称 + 邮箱,去掉网址
locale: {
nick: '昵称(必填)',
mail: '邮箱(可选)',
placeholder: '欢迎留言(◍•ᴗ•◍)',
sofa: '快来抢沙发!',
},
});
</script>
注意 meta: ['nick', 'mail'] —— Waline 用的字段名是 nick mail link(不是 nickname email link)。我一开始用错的 nickname email,整个 meta 区被 Waline 忽略,输入框完全不显示,只剩下主评论框。
第二个坑:CORS
第一次部署完去博客发评论,浏览器报:
Access to fetch at 'https://waline-comment.leonashao.workers.dev/api/comment'
from origin 'https://shaohua2kuaiqian.com' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
Claude 直接定位到 worker 的 CORS 中间件:
origin: (origin, c) => {
const allowed = c.env.SECURE_DOMAINS.split(',').map(d => d.trim());
if (allowed.some(d => origin === d || origin.endsWith(`.${d}`))) {
return origin;
}
return '';
}
origin === d 严格相等。我 SECURE_DOMAINS 设的是裸域 shaohua2kuaiqian.com,但浏览器发的 Origin 头是带协议的 https://shaohua2kuaiqian.com,匹配不上。
修:SECURE_DOMAINS = "https://shaohua2kuaiqian.com",重新部署。CORS 通过。
CSS 微调:字段布局 + 隐藏 UA
Waline 默认的元字段(昵称 / 邮箱)是横排,三个字段挤在一行,输入框很窄。我让 Claude 强制改成纵向布局:
#waline .wl-header {
display: block !important;
}
#waline .wl-header-item {
display: flex !important;
width: 100% !important;
border-bottom: 1px dashed var(--waline-border-color);
}
#waline .wl-header label {
min-width: 3em;
cursor: pointer;
}
#waline .wl-header input {
flex: 1;
padding: 0.5em 0.75em;
}
/* 每个字段右侧加灰色辅助说明 */
#waline .wl-header-item:nth-child(1)::after {
content: '但是可以不填就是匿名';
flex: 0 0 auto;
padding: 0 0.75em;
color: var(--waline-light-grey);
font-size: 0.75em;
}
#waline .wl-header-item:nth-child(2)::after {
content: '留了能收到回复通知';
/* 同上样式 */
}
每个字段单独一行,输入框撑满,右侧用 ::after 加灰色说明。
另一个想关掉的:每条评论下方默认显示访客的 Chrome 版本号、macOS 版本号、地理位置。env 里有 DISABLE_USERAGENT 配置项,但 Claude 看 worker 代码后说:
这个 port 在 env.ts 声明了 DISABLE_USERAGENT 字段,但整个 worker 代码里没一处真的用它。设了也没效果。
简单粗暴的修法:CSS 隐藏。
#waline .wl-card .wl-meta {
display: none;
}
数据后端还是会收集和存,前端不显示而已。
长毛象评论
主要是 Mastodon API 的客户端实现。layouts/partials/mastodon-comments.html 是一个独立 partial,frontmatter 里有 mastodon 字段才渲染:
{{ with .Params.mastodon }}
<section class="mastodon-comments" data-instance="m.cmx.im" data-status-id="{{ . }}">
<h3>长毛象上的讨论</h3>
<div class="mastodon-comments-list">加载中...</div>
</section>
<script>
fetch('https://m.cmx.im/api/v1/statuses/' + statusId + '/context')
.then(r => r.json())
.then(data => {
// 渲染 data.descendants 里的 replies
});
</script>
{{ end }}
完整代码 ~100 行 JS,处理:直接回复 toot 的过滤(不显示回复的回复,避免嵌套)、HTML 转义、头像渲染、时间格式化、错误处理。
用法:每次想要某篇文章带长毛象评论,就:
- 在长毛象上发一条 toot 提到这篇文章
- 复制 toot 的 ID(URL 最后一段)
- 加到 frontmatter:
mastodon: "111234567890123456" - push,~2 分钟后该文章页底部就会出现"长毛象上的讨论"板块
没加这个字段的文章页面就不显示,纯 Waline。
我自己需要做的事
- pnpm 之前没装,要先
brew install pnpm - 跑
pnpm approve-builds批准 native 依赖(pnpm 11+ 新的安全机制) - 手写
wrangler.toml(repo 不提供模板) - 注意 schema 初始化要带
--remote openssl rand -hex 32生成 JWT secret 并设到 Cloudflare- 浏览器
/ui/register创建管理员账号(记得记住邮箱密码) - 注意
SECURE_DOMAINS要完整 URL - 想要长毛象同步的文章:手动发 toot + 复制 ID 到 frontmatter
现在
文章底部,分隔线后面:
长毛象上的讨论 (如果 frontmatter 有 mastodon)
...自动同步的回复...
—————
[ 昵称(必填) [ ] 但是可以不填就是匿名 ]
[ 邮箱(可选) [ ] 留了能收到回复通知 ]
欢迎留言(◍•ᴗ•◍)
[ 提交 ]
访客填昵称(或留空当匿名)→ 写评论 → 提交,立刻显示在下面。我在 https://waline-comment.leonashao.workers.dev/ui 后台能审核、置顶、删除。
技术栈最终全在 Cloudflare:博客 (Pages) + 域名 (Registrar) + DNS + 评论后端 (Workers) + 数据库 (D1),统一一个 dashboard。