我原本只是想把评论区的 GitHub 登录门槛去掉,真正动手后才发现,这次迁移还牵出了邮件通知、历史浏览量和旧评论归属等一串问题。这篇文章记录本站从 Giscus 换到 Waline 的过程,也保留几处实际踩过的坑。
现在打开任意文章,评论区已经换成了新的 Waline 界面。读者不需要 GitHub 账户,可以直接以访客身份参与讨论;如果希望在评论收到回复时得到通知,还可以选择填写邮箱。

为什么从 Giscus 迁移
Giscus 是一个很适合技术博客的评论方案:它把评论保存在 GitHub Discussions,维护成本低,也很符合开源社区的使用习惯。问题是,发表评论需要登录 GitHub。
本站的内容已经不只面向开发者。工具介绍、马耳他生活信息、旅行内容和网站更新会吸引不同背景的读者。要求每位读者先拥有并登录 GitHub,实际上为一次普通留言设置了不必要的门槛。
最后选择 Waline,主要是因为它把我需要的几件事放在了一起:
- 支持访客评论,不强制注册或登录;
- 邮箱选填,并可在评论收到回复时发送通知;
- 支持评论点赞、文章反应和评论数量统计;
- 将文章浏览量与评论系统放在同一套计数接口中;
- 提供独立的评论管理后台,不必进入 GitHub Discussions 处理内容;
- 保留中英文界面,并让通知邮件跟随文章语言。
访客可以不登录,但评论服务仍会为了限流、安全防护和管理处理必要的技术信息。昵称和评论内容会公开显示,邮箱则不会显示在评论区。
现在的托管结构
博客本身仍然是 Hugo 生成的静态网站,由 GitHub Actions 构建并发布到 GitHub Pages。内容编辑使用 Sveltia CMS,文章和页面最终仍保存到 GitHub 仓库。
新的评论系统分成两部分:
- Waline Server 部署在 Vercel,负责评论 API、管理界面、邮件通知和计数请求;
- 评论、用户、文章互动和浏览量数据存储在 Neon PostgreSQL。
这种拆分没有把整个博客变成动态网站。GitHub Pages 继续提供静态页面,只有评论区和计数在浏览器中按需请求 Waline API。哪怕评论服务偶尔不可用,文章正文仍然可以正常访问——这是我不想放弃的一点。
仪表盘的管理选单也已经更新:
- 内容管理进入本站的 Sveltia CMS;
- 评论管理进入 Waline 管理界面。
仪表盘的系统信息现在会同时标明内容系统由 GitHub 托管,评论系统使用 Vercel 与 Neon。
评论区怎样融入现有主题
Waline 自带完整的输入框、评论列表、排序、Markdown、图片上传和表情选择功能,但默认视觉需要与 Stack 主题进一步协调。
我为评论区增加了一层主题适配,包括:
- 使用网站现有的卡片背景、边框、圆角和阴影变量;
- 同时适配浅色与深色模式;
- 把浏览量和评论数放进评论须知卡片;
- 保留移动端布局,避免输入框和用户信息被挤压;
- 根据文章语言切换界面文案;
- 使用 Twitter Emoji 风格的表情资源,保持不同系统上的显示一致。
文章反应目前提供“喜欢、好笑、思考、惊讶、难过”五种选择。它们使用 Waline 的 Twitter Emoji 图片资源,而不是读取访客电脑中的本地表情文件,因此不同浏览器会看到一致的图案。
从 Cloudflare 迁移浏览量
本站原来的浏览量系统运行在 Cloudflare Workers 与 D1 上,其中还合并了此前的 GA4 历史数据。迁移评论系统时,我没有从零重新计数,而是把已有统计带到 Waline。
迁移快照中,Cloudflare D1 有 82 条原始计数键。部分中文路径同时存在百分号编码和 Unicode 两种形式,例如同一页面可能分别以编码后的中文 URL 和直接中文 URL 出现。
迁移过程先对路径做规范化,再把相同页面合并,最终得到 76 个唯一 URL。全站累计浏览量在迁移时为 1,607。
目标数据库使用 Waline 的 wl_counter 表。迁移时还处理了一个容易忽略的问题:该表默认只以数字 ID 为主键,没有强制要求 URL 唯一。早期通过公开 API 并发补数时,同一路径可能产生重复记录。
最终采用数据库事务完成整理:
- 每个 URL 保留一条计数记录;
- 合并并清理重复项;
- 为 URL 增加唯一索引;
- 使用“只增不减”的写入规则补齐历史数据;
- 核对缺失路径、重复路径和低于源数据的计数。
验证结果为 76 条目标记录、0 个重复 URL、0 个缺失 URL,并且没有任何页面低于 Cloudflare 的迁移快照。使用“只增不减”规则,也避免了迁移期间的新访问被历史数据覆盖。
全站页面怎样计数
新的计数并不只出现在文章页。网站页脚会在非 404 页面加载 Waline 的轻量浏览量模块,为当前页面增加一次访问,同时更新一个专门的全站累计计数。
文章评论区显示浏览量时只读取现有数字,不会再次递增,这样可以避免同一次页面加载被页脚和评论组件重复计算。
主页、归档、分类、标签、图片墙、链接页、仪表盘和中英文页面都使用同一套新计数系统。旧的 Cloudflare 浏览量接口不再负责前端统计。
邮件通知与双语模板
邮件通知是这次迁移里最容易让人产生错觉的一部分:配置项都填好了,不代表邮件真的会发出去;邮件成功送达,也不代表收件箱里看起来正常。
第一次测试时,评论已经写进数据库,邮箱却没有任何动静。我最后在 Vercel 的函数日志里找到 Gmail 返回的 535-5.7.8 Username and Password not accepted。问题不在 Waline 模板,而在 SMTP 验证。换成 Gmail 单独生成的应用专用密码,并重新部署生产环境后,邮件才真正发出。
这里有两个容易漏掉的细节:
- Gmail SMTP 登录仍使用主邮箱地址;
+blogservice-noreply这样的加号地址适合做邮件标识,但不能代替真实登录账户; - 修改 Vercel 环境变量后需要重新部署,已经运行的生产函数不会自动读到新值。
目前发件人显示为 Jingyuan’s Blog Comment Service,标题和正文则跟随评论所在页面的语言。中文新评论与回复邮件使用“小景的博客”,英文邮件使用 “Jingyuan’s Blog”。这样收件箱里一眼就能看出邮件来自博客评论服务,又不会把中英文混在同一封通知里。
Waline 的邮件模板可以读取评论所属页面路径。本站中文文章都位于 /zh/ 下,因此模板会检查评论 URL:
|
|
中文文章下的评论会收到中文邮件,其他页面使用英文邮件。模板只引用评论者昵称、评论正文和文章链接,不会在邮件正文中暴露数据库凭据或其他系统密钥。
第一版邮件还有一个很直观的问题:评论正文周围出现了字面量 <p>...</p>。原因是模板把 Waline 已经处理过的评论 HTML 又转义了一次。把对应字段按已清理的 HTML 输出后,段落标签才恢复为正常排版。
随后我重新做了邮件样式。它没有照搬网页 CSS,而是采用更保守的单列卡片、表格布局和行内样式:浅灰背景、白色正文卡片、两块不同强调色的评论引用区,以及一个清晰的“查看并回复”按钮。邮件不加载外部字体、脚本或追踪图片,在 Gmail、Outlook 和窄屏设备里都更稳妥;Outlook 即使忽略部分圆角,也不会破坏阅读顺序。
邮件凭据和模板均通过 Vercel 环境变量配置。SMTP 账户、应用专用密码和数据库连接信息只保存在托管平台的敏感变量中,不会写入 Hugo 仓库、文章或前端代码。为了保护账户安全,SMTP 使用的也是应用专用密码,而不是邮箱账户的日常登录密码。
评论数据是怎样迁移的
旧评论先导出为 Waline JSON 格式,再导入新数据库。迁移时保留了评论正文、文章路径、回复关系、发布时间和审核状态。
部分由站长发布的旧评论在导出时缺少用户关联信息。处理时只补齐管理员用户 ID、昵称、邮箱标识、主页和客户端信息,不修改原评论内容、页面、回复关系或时间。
这类修复很重要。如果只搬运文字而没有恢复用户关联,旧评论可能会被显示成普通访客留言,头像、站长标记和后续管理权限也会不一致。
隐私与评论须知
评论区现在使用以下提示:
本站支持访客评论。如果您希望获得评论回复通知,可以填写您的邮箱;收到回复时,系统将自动发送通知给您。
邮箱不是发表评论的强制条件。填写邮箱的主要用途是回复通知和头像匹配,不会在评论列表中公开显示。
由于评论系统会处理评论内容、可选邮箱以及必要的技术信息,本站的隐私政策也已经同步更新,说明 GitHub Pages、Vercel、Neon 和 Waline 在当前架构中的作用。
接下来
直到测试回复干净地出现在 Outlook 收件箱里,我才觉得这次迁移算真正完成。数据库写入成功只是第一步,读者最后看到的评论区和通知邮件,才是这套系统实际的样子。
接下来我会继续留意邮件送达、垃圾邮件判定、移动端输入、反垃圾策略和计数一致性。如果你遇到加载失败、通知未送达或路径计数不对,欢迎直接在本文下方留言——现在不需要 GitHub 账户了。