Hexo 生成的是静态页面,它可以展示评论框,却不能独自保存评论、管理用户或执行反垃圾校验。本教程会从零搭建一套完整方案:把 Waline Server 放在独立的 Vercel 项目中,用 Neon PostgreSQL 持久化评论数据,用 Cloudflare Turnstile 降低机器人滥用,再把 ShokaX 客户端接到普通文章和独立留言板。

完成后,你将得到这些能力:

  • 普通文章默认显示评论,独立页面按需开启评论;
  • /comments/ 作为独立留言板,并与文章评论共用同一套服务;
  • comment.example.com 作为清晰、可迁移的评论服务域名;
  • 评论持久化到 Neon,而不是跟随 Vercel 无状态实例消失;
  • Turnstile 在浏览器与服务端之间完成人机验证;
  • PJAX 切换页面后重新初始化评论,深色模式自动跟随主题;
  • 加载失败时显示友好提示,并允许用户重试。

全文只使用示例域名和占位符。博客仓库不保存 PostgreSQL 密码、数据库连接串、Turnstile Secret 或其它服务端密钥。

# 一、先理解整体架构

Waline 评论系统整体架构:Hexo 前端通过 Vercel 服务连接 Neon,并由 Turnstile 完成人机验证

各部分职责如下:

  1. Hexo + ShokaX:生成博客页面,在浏览器中加载 Waline 客户端,并为当前页面指定评论路径。
  2. Waline Server:提供评论 API、用户和管理界面、频率限制、人机验证以及数据库访问逻辑。
  3. Vercel:运行 Waline Server。它负责部署代码和注入环境变量,但不应该被当作评论数据存储。
  4. Neon PostgreSQL:保存评论、用户等持久化数据。
  5. Cloudflare Turnstile:浏览器拿公开 Site Key 显示验证组件,Waline Server 用 Secret Key 向 Cloudflare 验证提交结果。

请求链路可以简化为:浏览器打开 Hexo 页面 → Waline 客户端请求 Vercel 上的 Waline Server → Waline Server 读写 Neon。发布评论时,还会多一步 Turnstile 服务端校验。

这里最重要的安全边界是:前端只接触公开的评论服务地址和 Site Key;数据库密码与 Secret Key 只存在于 Vercel 服务端环境变量中。

# 二、准备清单与部署路线

开始前准备好:

Waline、Vercel、Neon、Turnstile 与 Hexo 的完整部署步骤路线图

建议把 Waline Server 放进独立 Git 仓库和独立 Vercel 项目,不要直接塞进 Hexo 博客仓库。这样做有三个好处:

  • 博客是静态站点,评论服务是后端服务,两者部署和回滚节奏不同;
  • Vercel 的数据库密码、Turnstile Secret、SMTP 密码不会混进博客项目;
  • 服务端可以单独记录 Waline 版本、依赖变化和部署历史。

不要为了“目录干净”删除服务端仓库里的 package.json、lockfile、Vercel 配置、Git remote 或提交历史。这些都是升级接口:将来需要知道服务端来自哪个模板、当前依赖版本是什么、如何拉取上游改动,以及发生故障时回滚到哪个提交。

# 三、创建独立的 Waline Server

# 3.1 从官方部署流程创建仓库

  1. 打开文末的 Waline 快速上手页面,使用官方 Vercel 部署入口。
  2. 登录 Vercel,并允许它通过模板创建一个新的 GitHub 仓库。
  3. 仓库名可以使用 waline-server,Vercel 项目也使用独立名称,避免与 Hexo 项目混淆。
  4. 等待第一次部署结束。此时服务页面可能还不能正常读写评论,因为数据库尚未初始化。
  5. 在服务端仓库记录当前模板版本、Waline 依赖版本或上游 commit,并保留 Git 历史。

首次部署的目标只是确认“服务端项目可以构建和运行”。数据库、域名和安全变量完成前,不要急着把这个地址写入公开博客。

# 3.2 记录可升级信息

建议在服务端仓库的维护文档中记录这些非敏感信息:

Upstream: Waline official Vercel template
Waline version/commit: <deployed version or commit>
Production Vercel project: <project name>
Database schema source: waline.pgsql from the same version/commit
Rollback point: <known-good git commit>

不要在这份记录里写密码、数据库连接串或 Secret。

# 四、创建并初始化 Neon PostgreSQL

# 4.1 创建项目、数据库和专用角色

在 Neon 中新建一个只服务于 Waline 的项目,然后:

  1. 选择离主要访客和 Vercel 部署区域较近的区域;
  2. 创建生产数据库,例如 <database>
  3. 创建专用登录角色,例如 <user>,不要长期复用 Neon 的最高权限角色;
  4. 确认该角色对目标数据库和用于 Waline 的 schema 有建表及读写权限;
  5. 分别保存 Host、Port、Database、User、Password,但不要写入 Git、Hexo 配置或文章。

Neon PostgreSQL 连接信息与 Waline 数据库初始化示意图

# 4.2 获取与服务端版本一致的 waline.pgsql

从文末“官方 PostgreSQL Schema”链接取得 waline.pgsql。不要无条件拿 main 分支最新脚本去初始化一个旧版 Waline Server:SQL 脚本、数据库表前缀和环境变量名称必须与实际部署的 Waline 版本一致。

最稳妥的做法是:先确认服务端仓库当前 Waline 版本或 commit,再下载同一版本或 commit 下的 assets/waline.pgsql

# 4.3 执行官方初始化脚本

方法一适合新手:在 Neon SQL Editor 中选择目标数据库,粘贴完整的官方 waline.pgsql,确认当前连接角色后执行。

方法二是在已安装 PostgreSQL 客户端的 PowerShell 中执行。先把脚本保存为当前目录下的 waline.pgsql,再运行:

$env:PGPASSWORD = '<password>'
$env:PGSSLMODE = 'require'
psql `
  --host '<host>' `
  --port '<port>' `
  --dbname '<database>' `
  --username '<user>' `
  --set ON_ERROR_STOP=on `
  --file '.\waline.pgsql'
Remove-Item Env:PGPASSWORD
Remove-Item Env:PGSSLMODE

如果本机环境无法通过 PGSSLMODE=require 建立 TLS 连接,优先使用 Neon 控制台给出的当前 psql 连接方式,但不要把完整连接串提交到仓库或粘贴进公开文章。

执行后,在 Neon 的 Tables/SQL Editor 中确认 Waline 所需表已经创建,并检查:

  • 表名前缀与准备配置的 PG_PREFIX 一致;
  • 专用角色能够查询和写入这些表;
  • 没有把脚本误执行到其它项目或 Preview 数据库;
  • 重复执行前先阅读脚本,避免在已有生产数据上盲目重建。

# 五、配置 Vercel 环境变量

Waline Server 在 Vercel 中需要配置的环境变量分组

进入 Waline Server 项目的 Settings → Environment Variables。下面是一组经过脱敏的完整示例:

PG_HOST=<host>
PG_PORT=<port>
PG_DB=<database>
PG_USER=<user>
PG_PASSWORD=<password>
PG_SSL=true
PG_PREFIX=wl_
SECURE_DOMAINS=example.com,www.example.com,comment.example.com
IPQPS=10
TURNSTILE_KEY=<public site key>
TURNSTILE_SECRET=<server-only secret>

逐项说明:

变量 作用
PG_HOST Neon 提供的 PostgreSQL Host,只填主机名。
PG_PORT Neon 提供的 PostgreSQL Port,常见值为 5432,以控制台实际值为准。
PG_DB Waline 使用的数据库名。
PG_USER 前面创建的专用数据库角色。
PG_PASSWORD 专用角色密码,只放 Vercel 服务端。
PG_SSL Neon 连接需要 TLS 时设为 true
PG_PREFIX Waline 表前缀,必须与 waline.pgsql 创建出的表一致。
SECURE_DOMAINS 允许调用评论服务的实际域名列表。
IPQPS 同一 IP 两次评论提交之间的最小间隔,单位为秒。
TURNSTILE_KEY Turnstile 的公开 Site Key,需与客户端使用的 key 对应。
TURNSTILE_SECRET 仅供 Waline Server 调用 Turnstile 验证接口的 Secret。

特别注意以下三点:

  1. IPQPS=10 表示同一 IP 两次评论提交至少间隔 10 秒,绝不是“允许 10 QPS”或“每秒 10 次请求”。
  2. SECURE_DOMAINS=example.com,www.example.com,comment.example.com 中只能写域名,不写 https://、端口、路径或末尾斜杠;未实际使用 www.example.com 时就删除它,只保留真实投入使用的域名。
  3. Waline Server 同时使用 TURNSTILE_KEYTURNSTILE_SECRET;后者绝不能进入 Hexo、浏览器代码或公开截图。

修改 Vercel 环境变量后必须重新部署,新变量才会进入新的运行实例。不要只保存变量后继续测试旧 Deployment。

# Production 与 Preview 分离

建议至少做到:

  • Production 使用生产 Neon 数据库和生产 Turnstile 配置;
  • Preview 使用单独的 Neon 分支、临时数据库或明确的测试数据集;
  • 不要让每个不受信任的 Preview 永久持有生产数据库写权限;
  • 如果暂时让 Preview 复用生产库,必须把它视为显式风险并限制使用时间。

Vercel 支持为 Production、Preview、Development 分别设置变量。检查每个变量的作用范围,避免“预览部署误写生产评论”。

# 可选 SMTP

SMTP 用于注册验证、评论回复通知或管理员新评论通知。只有确实需要邮件功能时才配置,并根据所用邮件服务提供商填写当前参数。SMTP 用户名、密码或授权码仍然只放 Vercel 环境变量;不要在教程、Git 历史或浏览器配置中保存它们。

# 六、配置 Cloudflare Turnstile

Cloudflare Turnstile Site Key 与 Secret Key 的前后端验证流程

在 Cloudflare Turnstile 中创建一个 Widget,并把实际会出现评论框或调用评论服务的 Hostname 加入允许列表,例如:

example.com
www.example.com
comment.example.com

如果没有使用 www,就不要为了照抄示例而保留它。域名迁移或新增 Preview 域名时,也要同步检查 Turnstile Hostname 与 Waline SECURE_DOMAINS

创建后会得到两类值:

  • Site Key:公开值,浏览器需要它来渲染或请求验证;填入 Waline Server 的 TURNSTILE_KEY,也填入 ShokaX 的 turnstileKey
  • Secret Key:私密值,只有 Waline Server 用它验证浏览器返回的 token;填入 Vercel 的 TURNSTILE_SECRET

浏览器不能直接持有 Secret。把 Secret 写进 Hexo YAML、JavaScript、Markdown 或 SVG,并不会“让验证更可靠”,只会让任何访客都能看到它。

# 七、绑定自定义评论域名

为 Waline Server 使用 https://comment.example.com,比长期依赖 Vercel 自动域名更容易记忆,也便于未来迁移。

  1. 在 Waline Server 的 Vercel 项目中添加 comment.example.com
  2. 打开 Vercel 当前显示的 DNS 配置要求;
  3. 到域名 DNS 服务商处,按该项目当时给出的记录类型、名称和值创建记录;
  4. 等待 Vercel 验证域名并签发 HTTPS 证书;
  5. 访问 https://comment.example.com,确认服务端页面和管理入口能够正常加载;
  6. 再把它写入 ShokaX 的 serverURL

这里不要硬编码某个“永久不变”的平台 DNS 目标。Vercel 的提示、域名接入方式或项目状态可能变化,应以当前项目 Domains 页面为准。

自定义域名启用后,别忘了同时更新:

  • Waline Server 的 SECURE_DOMAINS
  • Turnstile Widget 的允许 Hostname;
  • ShokaX 的 serverURL
  • 必要时的 Waline SERVER_URL 等版本相关服务端配置。

每次修改 Vercel 变量后重新部署。

# 八、配置 Hexo / ShokaX 客户端

在 ShokaX 配置文件中加入或调整 Waline 配置。下面的结构与本项目当前配置形状一致,所有值均为示例:

waline:
  enable: true
  serverURL: "https://comment.example.com"
  lang: zh-CN
  locale: {}
  emoji:
    - https://unpkg.com/@waline/[email protected]/weibo
  meta:
    - nick
    - mail
    - link
  requiredMeta:
    - nick
    - mail
  wordLimit: 0
  pageSize: 10
  pageview: false
  recaptchaV3Key:
  turnstileKey: "<public site key>"

字段含义:

  • enable:启用 Waline 客户端集成;
  • serverURL:可访问且 HTTPS 正常的 Waline Server 地址,不是 Neon 地址;
  • lang:界面语言;
  • locale:需要覆盖默认翻译时填写,自定义内容为空可保留 {}
  • emoji:评论表情包来源;
  • meta:评论者可以填写的字段;
  • requiredMeta:提交评论前必须填写的字段;
  • wordLimit:评论字数限制,0 表示不额外限制;
  • pageSize:每页评论条数;
  • pageview:是否启用 Waline 浏览量统计;
  • recaptchaV3Key:未使用 reCAPTCHA v3 时留空;
  • turnstileKey:浏览器侧公开 Site Key,必须与服务端的 TURNSTILE_KEY 配对。

turnstileKey 是公开 Site Key,可以出现在客户端配置;TURNSTILE_SECRET 不可以。PostgreSQL 的五项连接信息也不应该出现在这段 YAML 中。

# 九、文章评论、独立页与留言板

Hexo 主题配置、文章评论、留言板和 Waline 评论路径之间的关系

本项目当前行为是:

  • 普通文章页默认开启评论;需要关闭某篇文章时,可以在该文章 front matter 中显式设置 comment: false
  • 普通独立页面默认不开启评论,需要显式设置 comment: true
  • 留言板是独立页面,路径为 /comments/,通过 type: comments 选择留言板内容布局,再通过 comment: true 开启评论容器;
  • 留言板布局隐藏文章专属的版权、标签、上一篇/下一篇等区域,但仍保留整个站点的公共页脚。

留言板页面可以使用:

---
title: 留言板
type: comments
comment: true
---

type: comments 只决定页面正文采用留言板样式,comment: true 才明确允许独立页渲染评论。因此不要省略其中任意一项。

# 统一评论路径

Waline 使用页面路径区分评论线程。若同一文章有时访问 /article,有时访问 /article/,未经处理可能产生两个线程。本项目的 normalizeCommentPath() 采用以下原则:

const normalized = pathname.replace(/\/+$/, '')
return normalized || '/'

它移除一个或多个末尾斜杠,但根路径去掉斜杠后会回退为 /。因此:

/article  -> /article
/article/ -> /article
/         -> /

这样 /article/article/ 会共享同一个讨论线程。上线后不要随意改评论 path 规则,否则旧评论仍在数据库中,却可能因为新路径不同而看起来“消失”。

# 十、PJAX、深色模式与加载失败重试

这些属于主题客户端集成,不是 Vercel 环境变量。

ShokaX 使用 PJAX 时,页面主体会在不整页刷新的情况下替换。正确流程是:

  1. PJAX 页面刷新完成后重新查找 #comments
  2. 为新的 #comments 创建并重新挂载可见性观察;
  3. 评论区域接近视口时开始加载;
  4. 初始化新评论前先调用旧 Waline 实例的 destroy()
  5. 使用当前地址经 normalizeCommentPath() 处理后的路径初始化;
  6. 切换到下一页时重复上述过程。

“销毁旧实例”可以避免事件监听和 DOM 状态重复,“重新观察”则确保 PJAX 新插入的评论容器会再次触发懒加载。

深色模式选择器必须与主题真实属性一致:

html[data-theme="dark"]

当该选择器匹配时,Waline 进入深色样式。不要凭习惯改成 .dark 或只看系统颜色偏好,否则可能与主题按钮状态不同步。

初始化失败时,本项目会在评论容器中展示友好提示和重试按钮。点击重试会再次尝试初始化;同时浏览器控制台保留警告,便于定位 serverURL、网络、Turnstile 或服务端错误。重试是用户体验兜底,不应替代查看 Vercel 日志和修复根因。

# 十一、本地与线上验证

# 11.1 本地检查

在 Hexo 仓库根目录运行:

pnpm verify:comments
node --test tools/*.test.mjs
pnpm typecheck
pnpm build:site

这些命令应分别验证评论/留言板约束、工具测试、主题类型检查和完整站点生成。若你的项目没有同名脚本,请使用项目实际的等价命令,不要跳过最终静态生成。

生成完成后至少确认:

  • 教程文章页面可以生成;
  • 普通文章 HTML 中存在 #comments
  • 未显式开启评论的独立页没有 #comments
  • /comments/index.html 存在留言板内容和评论容器;
  • 七张 /images/waline-comment-system/*.svg 均能访问;
  • 生成结果没有把数据库密码或 Secret 打包进浏览器资源。

# 11.2 线上检查

部署到正式域名后按顺序测试:

  1. 打开普通文章,确认评论框加载;
  2. 打开 /comments/,确认留言板和站点页脚都存在;
  3. 提交一条可识别的测试评论,刷新页面后确认仍然存在;
  4. 分别访问 /article/article/,确认显示同一评论线程;
  5. 在两篇文章之间通过 PJAX 来回切换,确认没有重复评论框,返回后仍能加载;
  6. 切换浅色/深色主题,确认 html[data-theme="dark"] 下样式可读;
  7. 用移动端视口检查输入框、验证码和按钮没有溢出;
  8. 确认 Turnstile 正常出现并能完成提交;
  9. 打开浏览器控制台,确认没有初始化、跨域或资源加载错误;
  10. 查看对应 Vercel Deployment 日志,确认没有 Neon 连接、权限、表缺失或 Turnstile 校验错误。

测试完成后删除无意义的测试评论,保留一条用于持续验证也可以,但不要把真实访客数据复制到公开调试记录。

# 十二、管理员、备份、升级与回滚

# 12.1 注册首个管理员

服务端部署完成后,访问:

https://comment.example.com/ui/register

Waline 的首个注册用户会成为管理员。请在正式公开评论入口前完成注册,并立即:

  • 使用专用邮箱和强密码;
  • 登录 /ui,确认拥有评论管理权限;
  • 检查是否存在意外注册用户;
  • 配置邮件验证或当前 Waline 版本支持的注册限制;
  • 首个管理员创建并验证完成后,若不需要访客账号功能,应关闭或收紧公开注册策略。

不同 Waline 版本的注册限制方式可能变化,不要照抄未经当前版本文档确认的变量名。至少应保留域名限制、频率限制、Turnstile,并定期审计用户列表。

# 12.2 Neon 备份与分支

Neon 分支很适合在升级前复制数据库状态用于测试,但“创建过分支”不等于已经拥有长期备份。根据账号套餐和数据重要性,明确配置并验证以下策略之一:

  • Neon 提供的恢复/保留能力;
  • 定期导出的 PostgreSQL 备份;
  • 升级前创建的临时数据库分支。

使用 pg_dump 时可采用不在命令历史中写密码的方式:

$env:PGPASSWORD = '<password>'
$env:PGSSLMODE = 'require'
pg_dump `
  --host '<host>' `
  --port '<port>' `
  --dbname '<database>' `
  --username '<user>' `
  --format custom `
  --file '.\waline-backup.dump'
Remove-Item Env:PGPASSWORD
Remove-Item Env:PGSSLMODE

备份完成后还要在隔离数据库中做恢复演练,确认备份文件确实可用。备份文件包含用户和评论数据,不应提交到公开 Git 仓库。

# 12.3 Waline 升级流程

  1. 记录当前可用的服务端 Git commit、Waline 版本和 Vercel Deployment;
  2. 创建 Neon 临时分支或使用测试数据库;
  3. 在独立服务端分支升级依赖,保留 lockfile 变化;
  4. 阅读目标版本的发布说明、环境变量文档和对应 waline.pgsql
  5. 把 Preview 指向测试数据库,验证注册、登录、读写评论、Turnstile 和管理后台;
  6. 备份生产数据库;
  7. 合并并部署 Production;
  8. 再次执行线上验证清单。

不要只更新前端客户端而完全忽略服务端兼容性,也不要把未经验证的新 schema 直接执行在唯一的生产数据库上。

# 12.4 回滚

如果新版本出现问题:

  1. 在 Vercel 中回滚到已知可用的 Deployment,或把服务端 Git 分支回退到记录的 commit 后重新部署;
  2. 恢复与该服务端版本匹配的环境变量;
  3. 如果升级包含不可向后兼容的数据库迁移,从经过验证的备份恢复到新数据库,再切换连接;
  4. 不要在未确认数据影响前,直接对生产库执行破坏性逆向 SQL;
  5. 回滚后重新测试发布、刷新持久化、管理后台和 Turnstile。

代码回滚不一定自动回滚数据库,因此版本记录、Preview 验证和可恢复备份缺一不可。

# 十三、常见问题与故障排查

Waline 评论框无法加载时的故障排查流程图

# 13.1 评论框完全不显示

先检查生成页面中是否存在 #comments。普通文章默认开启,独立页默认关闭;留言板必须同时有 type: commentscomment: true。如果容器不存在,继续检查 front matter 和主题配置,而不是先改数据库。

# 13.2 serverURL 无法访问

直接访问 https://comment.example.com,检查 DNS、HTTPS 证书、Vercel Deployment 状态和浏览器控制台。只有服务端可访问后,客户端配置才有意义。

# 13.3 Neon 连接失败

在 Vercel 日志中区分 Host、Port、Database、User、Password、SSL 和权限错误。确认变量属于当前环境,修改后已经重新部署,并确认专用角色能访问目标表。

# 13.4 表不存在或 PG_PREFIX 不匹配

检查 waline.pgsql 实际创建的表名。若脚本创建的是 wl_ 前缀,服务端却配置为其它值,Waline 会查询另一组表。修正配置或按对应版本重新初始化,不能通过反复部署解决 schema 错误。

# 13.5 请求被 SECURE_DOMAINS 拒绝

确认值是:

SECURE_DOMAINS=example.com,www.example.com,comment.example.com

这里只允许域名,不包含协议和路径,并且只保留实际域名。若使用新的正式域名或 Preview 域名,需要有意识地添加;不要为了省事允许不受控域名。

# 13.6 Turnstile 校验失败

检查四项:

  • ShokaX turnstileKey<public site key>
  • Waline Server TURNSTILE_KEY 是同一个 <public site key>
  • Vercel TURNSTILE_SECRET<server-only secret>
  • Cloudflare Widget Hostname 包含当前网站域名。

Site Key 和 Secret Key 放反、前后端 Site Key 不一致、域名未允许,都会导致验证失败。

# 13.7 同一页面出现两个评论线程

比较客户端发送给 Waline 的 path。确认统一执行 normalizeCommentPath(),并验证 /article/article/ 归一化到同一路径。若修改过 permalink,还需评估旧路径评论迁移,而不是只改显示 URL。

# 13.8 PJAX 后评论没有加载或重复

确认 PJAX 完成后重新观察新的 #comments,并在初始化前销毁旧 Waline 实例。若只在首次 DOMContentLoaded 初始化,PJAX 新页面不会自动执行;若不销毁旧实例,则可能重复绑定。

# 13.9 数据写入成功但页面看不到

先到管理后台或数据库确认数据存在,再检查当前页面 path、审核状态、分页和客户端请求。最常见的原因之一是路径规则变化,评论仍在旧线程下。

# 13.10 Preview 误连生产数据库

检查 Vercel 环境变量作用范围和 Preview Deployment 实际注入值。修正后重新部署,并审核测试期间是否写入了生产评论或用户表。

# 十四、上线前安全检查表

# 官方参考