Hexo 生成的是静态页面,它可以展示评论框,却不能独自保存评论、管理用户或执行反垃圾校验。本教程会从零搭建一套完整方案:把 Waline Server 放在独立的 Vercel 项目中,用 Neon PostgreSQL 持久化评论数据,用 Cloudflare Turnstile 降低机器人滥用,再把 ShokaX 客户端接到普通文章和独立留言板。
完成后,你将得到这些能力:
- 普通文章默认显示评论,独立页面按需开启评论;
/comments/作为独立留言板,并与文章评论共用同一套服务;comment.example.com作为清晰、可迁移的评论服务域名;- 评论持久化到 Neon,而不是跟随 Vercel 无状态实例消失;
- Turnstile 在浏览器与服务端之间完成人机验证;
- PJAX 切换页面后重新初始化评论,深色模式自动跟随主题;
- 加载失败时显示友好提示,并允许用户重试。
全文只使用示例域名和占位符。博客仓库不保存 PostgreSQL 密码、数据库连接串、Turnstile Secret 或其它服务端密钥。
# 一、先理解整体架构
各部分职责如下:
- Hexo + ShokaX:生成博客页面,在浏览器中加载 Waline 客户端,并为当前页面指定评论路径。
- Waline Server:提供评论 API、用户和管理界面、频率限制、人机验证以及数据库访问逻辑。
- Vercel:运行 Waline Server。它负责部署代码和注入环境变量,但不应该被当作评论数据存储。
- Neon PostgreSQL:保存评论、用户等持久化数据。
- Cloudflare Turnstile:浏览器拿公开 Site Key 显示验证组件,Waline Server 用 Secret Key 向 Cloudflare 验证提交结果。
请求链路可以简化为:浏览器打开 Hexo 页面 → Waline 客户端请求 Vercel 上的 Waline Server → Waline Server 读写 Neon。发布评论时,还会多一步 Turnstile 服务端校验。
这里最重要的安全边界是:前端只接触公开的评论服务地址和 Site Key;数据库密码与 Secret Key 只存在于 Vercel 服务端环境变量中。
# 二、准备清单与部署路线
开始前准备好:
建议把 Waline Server 放进独立 Git 仓库和独立 Vercel 项目,不要直接塞进 Hexo 博客仓库。这样做有三个好处:
- 博客是静态站点,评论服务是后端服务,两者部署和回滚节奏不同;
- Vercel 的数据库密码、Turnstile Secret、SMTP 密码不会混进博客项目;
- 服务端可以单独记录 Waline 版本、依赖变化和部署历史。
不要为了“目录干净”删除服务端仓库里的 package.json、lockfile、Vercel 配置、Git remote 或提交历史。这些都是升级接口:将来需要知道服务端来自哪个模板、当前依赖版本是什么、如何拉取上游改动,以及发生故障时回滚到哪个提交。
# 三、创建独立的 Waline Server
# 3.1 从官方部署流程创建仓库
- 打开文末的 Waline 快速上手页面,使用官方 Vercel 部署入口。
- 登录 Vercel,并允许它通过模板创建一个新的 GitHub 仓库。
- 仓库名可以使用
waline-server,Vercel 项目也使用独立名称,避免与 Hexo 项目混淆。 - 等待第一次部署结束。此时服务页面可能还不能正常读写评论,因为数据库尚未初始化。
- 在服务端仓库记录当前模板版本、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 的项目,然后:
- 选择离主要访客和 Vercel 部署区域较近的区域;
- 创建生产数据库,例如
<database>; - 创建专用登录角色,例如
<user>,不要长期复用 Neon 的最高权限角色; - 确认该角色对目标数据库和用于 Waline 的 schema 有建表及读写权限;
- 分别保存 Host、Port、Database、User、Password,但不要写入 Git、Hexo 配置或文章。
# 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 项目的 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。 |
特别注意以下三点:
IPQPS=10表示同一 IP 两次评论提交至少间隔 10 秒,绝不是“允许 10 QPS”或“每秒 10 次请求”。SECURE_DOMAINS=example.com,www.example.com,comment.example.com中只能写域名,不写https://、端口、路径或末尾斜杠;未实际使用www.example.com时就删除它,只保留真实投入使用的域名。- Waline Server 同时使用
TURNSTILE_KEY和TURNSTILE_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 中创建一个 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 自动域名更容易记忆,也便于未来迁移。
- 在 Waline Server 的 Vercel 项目中添加
comment.example.com; - 打开 Vercel 当前显示的 DNS 配置要求;
- 到域名 DNS 服务商处,按该项目当时给出的记录类型、名称和值创建记录;
- 等待 Vercel 验证域名并签发 HTTPS 证书;
- 访问
https://comment.example.com,确认服务端页面和管理入口能够正常加载; - 再把它写入 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 中。
# 九、文章评论、独立页与留言板
本项目当前行为是:
- 普通文章页默认开启评论;需要关闭某篇文章时,可以在该文章 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 时,页面主体会在不整页刷新的情况下替换。正确流程是:
- PJAX 页面刷新完成后重新查找
#comments; - 为新的
#comments创建并重新挂载可见性观察; - 评论区域接近视口时开始加载;
- 初始化新评论前先调用旧 Waline 实例的
destroy(); - 使用当前地址经
normalizeCommentPath()处理后的路径初始化; - 切换到下一页时重复上述过程。
“销毁旧实例”可以避免事件监听和 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 线上检查
部署到正式域名后按顺序测试:
- 打开普通文章,确认评论框加载;
- 打开
/comments/,确认留言板和站点页脚都存在; - 提交一条可识别的测试评论,刷新页面后确认仍然存在;
- 分别访问
/article与/article/,确认显示同一评论线程; - 在两篇文章之间通过 PJAX 来回切换,确认没有重复评论框,返回后仍能加载;
- 切换浅色/深色主题,确认
html[data-theme="dark"]下样式可读; - 用移动端视口检查输入框、验证码和按钮没有溢出;
- 确认 Turnstile 正常出现并能完成提交;
- 打开浏览器控制台,确认没有初始化、跨域或资源加载错误;
- 查看对应 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 升级流程
- 记录当前可用的服务端 Git commit、Waline 版本和 Vercel Deployment;
- 创建 Neon 临时分支或使用测试数据库;
- 在独立服务端分支升级依赖,保留 lockfile 变化;
- 阅读目标版本的发布说明、环境变量文档和对应
waline.pgsql; - 把 Preview 指向测试数据库,验证注册、登录、读写评论、Turnstile 和管理后台;
- 备份生产数据库;
- 合并并部署 Production;
- 再次执行线上验证清单。
不要只更新前端客户端而完全忽略服务端兼容性,也不要把未经验证的新 schema 直接执行在唯一的生产数据库上。
# 12.4 回滚
如果新版本出现问题:
- 在 Vercel 中回滚到已知可用的 Deployment,或把服务端 Git 分支回退到记录的 commit 后重新部署;
- 恢复与该服务端版本匹配的环境变量;
- 如果升级包含不可向后兼容的数据库迁移,从经过验证的备份恢复到新数据库,再切换连接;
- 不要在未确认数据影响前,直接对生产库执行破坏性逆向 SQL;
- 回滚后重新测试发布、刷新持久化、管理后台和 Turnstile。
代码回滚不一定自动回滚数据库,因此版本记录、Preview 验证和可恢复备份缺一不可。
# 十三、常见问题与故障排查
# 13.1 评论框完全不显示
先检查生成页面中是否存在 #comments。普通文章默认开启,独立页默认关闭;留言板必须同时有 type: comments 和 comment: 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 实际注入值。修正后重新部署,并审核测试期间是否写入了生产评论或用户表。