手把手教你搭建 Waline 评论系统:从零到上线,踩坑全记录#
这篇文章会带你一步一步搭建 Waline ↗ 评论系统。不需要懂后端开发,不需要会写 SQL,跟着操作就行。
零、先搞清楚我们要搭什么#
一个带评论功能的博客,实际上由三样东西组成:
你写的评论 ──→ 存到哪里? ──→ 怎么显示出来?plaintext传统网站(比如 WordPress)这三件事都在同一台服务器上。静态博客(比如我们的 Astro 站点)没有后端,所以需要额外拼装两个服务:
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 你的博客 │ │ Waline 后端 │ │ 数据库 │
│ (Netlify) │ ──→ │ (Vercel) │ ──→ │ (Supabase) │
│ │ ←── │ │ ←── │ │
│ 显示评论 │ │ 处理评论数据 │ │ 存储评论数据 │
│ 显示浏览量 │ │ 提供 API 接口 │ │ 存储浏览量 │
│ │ │ │ │ 存储点赞 │
└─────────────────┘ └─────────────────┘ └─────────────────┘
前端(已有) 后端(需要新建) 数据库(需要新建)plaintext三者的关系可以用一个生活类比来理解:
- 数据库(Supabase) = 仓库。评论、浏览量、点赞都存在这里。
- Waline 后端(Vercel) = 仓库管理员。你不直接进仓库拿东西,你跟管理员说”我要这篇文章的评论”或”我要存一条新评论”,管理员帮你去仓库操作。
- 前端(你的博客) = 店门口。顾客在店门口看到评论、写评论,背后是管理员在仓库里忙活。
一、准备工作#
1.1 确认前端组件已就位#
astro-pure 主题已经把 Waline 的”店门口”部分做好了,项目里已经有这些文件,不需要额外写代码:
| 文件 | 作用 |
|---|---|
src/components/waline/Comment.astro | 评论区组件(含点赞按钮) |
src/components/waline/PageInfo.astro | 显示”X 次浏览 · Y 条评论” |
src/components/waline/Pageview.astro | 浏览量计数器 |
public/icons/heart-item.svg | 点赞用的心形图标 |
这些组件现在处于”休眠”状态——因为 Waline 后端的地址还没填,它们不知道该找谁要数据。等我们部署好 Waline 后端,填上地址,它们就活了。
1.2 列一下你需要准备的东西#
- 一个 GitHub 账号(用来登录 Vercel 和 Supabase)
- 一个浏览器
- 大约 30 分钟时间
- 不需要懂代码
二、第一步:注册并创建 Supabase 数据库#
Supabase 是一个云数据库服务,简单理解就是”放在云上的 Excel 表格”。我们要用它来存评论数据。
2.1 注册账号#
- 打开 supabase.com ↗
- 点击右上角 Sign in,选择 Continue with GitHub,用你的 GitHub 账号登录
2.2 创建项目#
- 登录后会进入 Dashboard 页面
- 点击黄色按钮 New project
- 填写以下信息:
| 字段 | 填什么 | 说明 |
|---|---|---|
| Name | 随便填,比如 my-blog-comment | 只是给你自己看的项目名 |
| Database Password | 自己设定一个密码,记下来! | 后面部署 Waline 时要用。建议用 12 位以上,不要包含 @ : / % 这些特殊字符 |
| Region | 选离你近的 | 亚洲用户选 Northeast Asia(东京)或 Southeast Asia(新加坡) |
| Pricing Plan | 选 Free | 免费版:500MB 存储 + 每月 5GB 流量,个人博客完全够 |
- 页面上有一个勾选框 “Enable automatic RLS” → 不要勾选
RLS 是什么? 全称 Row Level Security(行级安全),是数据库的一种权限控制功能。比如”用户 A 只能看到自己的订单,不能看用户 B 的”。你的博客不需要这个——所有评论都是公开的,Waline 自己会管理读写权限。如果勾上了,Waline 访问数据库可能被拦截。
- 点击 Create project,等 1-2 分钟初始化
2.3 找到连接信息#
项目创建完成后,我们需要拿到”仓库的钥匙”——数据库连接信息。
- 左侧菜单点击 Settings(齿轮图标)
- 在 Settings 子菜单中点击 Database
- 往下滚动,找到 Connection string 这一栏
- 你会看到一串字符:
postgresql://postgres:[YOUR-PASSWORD]@db.xxxxx.supabase.co:5432/postgresplaintext把它拆开来看,每一部分都有含义:
postgresql:// 协议名,不用管
postgres 用户名(Supabase 默认就是 postgres)
[YOUR-PASSWORD] 你刚才设定的数据库密码(这里显示为占位符)
@ 分隔符
db.xxxxx.supabase.co 数据库的地址(xxxxx 是你的项目 ID)
:5432 端口号(5432 是 PostgreSQL 的默认端口)
/postgres 数据库名(Supabase 默认创建一个叫 postgres 的库)plaintext现在只要知道这些信息存在这里就行,后面填环境变量时会逐一用到。
三、第二步:在 Vercel 部署 Waline 后端#
Vercel 是一个托管平台,可以免费运行小型后端服务。我们现在要把 Waline 的”仓库管理员”部署上去。
3.1 什么是 Waline 后端?#
一句话解释:Waline 后端就是一段 JavaScript 代码,它做的事情很简单:
- 收到”获取这篇文章的评论”的请求 → 去数据库查 → 返回评论列表
- 收到”发表一条新评论”的请求 → 检查内容 → 写入数据库
- 收到”获取浏览量”的请求 → 查数据库 → 返回数字
- 提供管理后台
/ui,让你能删评论、审核留言
这段代码需要在一个能 24 小时运行的地方执行——Vercel 的 Serverless Function 就是干这个的。
3.2 开始部署#
重要提示:Waline 的代码仓库(repository)是一个包含多个子项目的大仓库。如果直接克隆整个仓库部署,会导致各种配置问题。Waline 官方已经把 Vercel 部署所需的代码单独放在 example 目录里了。
- 打开这个链接(这是关键,不能错):
https://vercel.com/new/clone?repository-url=https://github.com/walinejs/waline/tree/main/exampleplaintext如果你直接去 Vercel 新建项目然后输入
https://github.com/walinejs/waline(不带/tree/main/example),会导致部署失败。因为 Vercel 会尝试构建整个 Waline 仓库,而这个仓库是一个包含 admin、api、client 等多个子项目的 monorepo(一个代码仓库包含多个独立子项目),根目录没有可以直接部署的输出文件。
- Vercel 会让你用 GitHub 登录授权
- 登录后会进入项目配置页面:
| 设置项 | 操作 |
|---|---|
| Framework Preset | 不用管,默认的 Vite 就行 |
| Root Directory | 保持默认(Vercel 已自动识别为 example) |
| Environment Variables | 这是重点,见下一节 |
3.3 填写环境变量#
环境变量就是告诉 Waline “你的仓库在哪里、钥匙是什么”的配置信息。Waline 启动时会读取这些变量,用来连接数据库。
在 Environment Variables 区域,点 Add 逐个添加以下变量:
第一组:数据库连接信息(必填)
| 变量名 | 值 | 从哪里来 |
|---|---|---|
PG_DB | postgres | Supabase 默认的数据库名 |
PG_USER | postgres | Supabase 默认用户名 |
PG_PASSWORD | 你的数据库密码 | 创建 Supabase 项目时你设定的那个密码 |
PG_HOST | db.xxxxx.supabase.co | 从 Supabase 连接字符串 @ 后面 :5432 前面复制 |
PG_PORT | 5432 | PostgreSQL 默认端口 |
PG_SSL | true | 必须开,Supabase 强制加密连接 |
第二组:站点信息
| 变量名 | 值 | 说明 |
|---|---|---|
SITE_NAME | 你的博客名 | 在 Waline 管理后台顶部显示 |
SITE_URL | https://你的博客域名 | 用于安全域名校验 |
填好 8 个变量后,点击 Deploy,等待约 2 分钟。
3.4 第一次部署大概率会失败#
等你看到部署结果,很大概率是这样的:
500: getaddrinfo ENOTFOUND db.xxxxx.supabase.coplaintext别慌,这是预料之中的。我们来排查。
为什么会失败?#
打开终端(或 PowerShell),执行:
nslookup db.xxxxx.supabase.cobash(把 xxxxx 换成你的 Supabase 项目 ID)
你会看到结果里只有 AAAA 记录(IPv6 地址),没有 A 记录(IPv4 地址)。
这意味着:Supabase 免费版的数据库地址只支持 IPv6。但 Vercel 的服务器不支持 IPv6 出站连接。 两边协议对不上,就像你拿 Type-C 的线去插 USB-A 的口——物理上不匹配。
这不是你配置错了,是两个平台的设计限制叠加了。
解决方案:使用 Supabase 的连接池(Pooler)#
Supabase 提供了一个免费的”翻译官”——Shared Pooler。它可以把 IPv4 的请求转成 IPv6,帮你桥接 Vercel 和数据库。
操作步骤:
- 回到 Supabase 后台 → Settings → Database
- 找到 Connection string,旁边有一个下拉框
- 从 Direct connection 切换到 Session pooler
- 连接字符串会变化,新格式如下:
postgresql://postgres.xxxxx:[你的密码]@aws-0-ap-northeast-1.pooler.supabase.com:5432/postgresplaintext注意三个变化:
| 项目 | 直连(失败) | Pooler(有效) |
|---|---|---|
| 主机名 | db.xxxxx.supabase.co | aws-0-xxx.pooler.supabase.com |
| 用户名 | postgres | postgres.xxxxx |
| IPv4 支持 | ❌ 只有 IPv6 | ✅ 有 IPv4 |
- 回到 Vercel,更新 Environment Variables:
| 变量 | 新值 |
|---|---|
PG_HOST | aws-0-xxx.pooler.supabase.com(从新连接字符串复制) |
PG_USER | postgres.xxxxx(从新连接字符串复制) |
PG_PORT | 5432(不变) |
端口说明:Pooler 有两个端口可用。5432 是 Session 模式(一个用户会话期间保持同一连接),6543 是 Transaction 模式(每次数据库操作完就断开)。两者的区别后面会讲到。
- 去 Deployments 页面,点上次部署右侧的三个点
⋯→ Redeploy
这一次,部署应该成功了。但访问 Waline 地址可能会看到新的错误:
500: relation "wl_users" does not existplaintext3.5 第二次部署也大概率会失败#
这次的错误信息变了,这说明数据库已经连上了(否则报的是连接错误),但 Waline 需要的”表格”不存在。
什么是”表”?#
数据库里,数据是按”表”来组织的。你可以把表理解为 Excel 里的一个 Sheet:
| 表名 | 存什么 | 类比 |
|---|---|---|
wl_comment | 所有评论 | Excel 的”评论”Sheet |
wl_counter | 所有浏览量和点赞数 | Excel 的”统计”Sheet |
wl_users | 管理员账号 | Excel 的”用户”Sheet |
Waline 首次启动时会自动创建这些表(相当于新建 Excel Sheet 并设置好表头)。但现在表没有被创建——因为 Pooler 不支持这种”建表”操作。
为什么 Pooler 不能建表?#
Pooler 底层用的是 PgBouncer(PostgreSQL 的连接池中间件)。把它想象成一个前台:
- 普通操作(查评论、写评论、更新点赞数)→ 前台转交给后台,正常处理 ✅
- 建表操作(CREATE TABLE)→ 这是”重新装修”级别的工程,前台拦下来不让通过 ❌
这不分 Session 模式还是 Transaction 模式——只要走 Pooler,建表(SQL 术语叫 DDL)就可能被拦截。
解决方案:在 Supabase 后台手动建表#
Pooler 不让建,我们就绕过它——在 Supabase 自己的后台直接操作数据库(后台走的是直连,不受 Pooler 限制)。
- Supabase 后台 → 左侧菜单 SQL Editor
- 点击 New query(新建查询)
- 把下面这段 SQL 全部复制粘贴进去:
CREATE SEQUENCE wl_comment_seq;
CREATE TABLE wl_comment (
id int check (id > 0) NOT NULL DEFAULT NEXTVAL ('wl_comment_seq'),
user_id int DEFAULT NULL,
comment text,
insertedAt timestamp(0) without time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
ip varchar(100) DEFAULT '',
link varchar(255) DEFAULT NULL,
mail varchar(255) DEFAULT NULL,
nick varchar(255) DEFAULT NULL,
pid int DEFAULT NULL,
rid int DEFAULT NULL,
sticky numeric DEFAULT NULL,
status varchar(50) NOT NULL DEFAULT '',
"like" int DEFAULT NULL,
ua text,
url varchar(255) DEFAULT NULL,
createdAt timestamp(0) without time zone NULL DEFAULT CURRENT_TIMESTAMP,
updatedAt timestamp(0) without time zone NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
);
CREATE SEQUENCE wl_counter_seq;
CREATE TABLE wl_counter (
id int check (id > 0) NOT NULL DEFAULT NEXTVAL ('wl_counter_seq'),
time int DEFAULT NULL,
reaction0 int DEFAULT NULL,
reaction1 int DEFAULT NULL,
reaction2 int DEFAULT NULL,
reaction3 int DEFAULT NULL,
reaction4 int DEFAULT NULL,
reaction5 int DEFAULT NULL,
reaction6 int DEFAULT NULL,
reaction7 int DEFAULT NULL,
reaction8 int DEFAULT NULL,
url varchar(255) NOT NULL DEFAULT '',
createdAt timestamp(0) without time zone NULL DEFAULT CURRENT_TIMESTAMP,
updatedAt timestamp(0) without time zone NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
);
CREATE SEQUENCE wl_users_seq;
CREATE TABLE wl_users (
id int check (id > 0) NOT NULL DEFAULT NEXTVAL ('wl_users_seq'),
display_name varchar(255) NOT NULL DEFAULT '',
email varchar(255) NOT NULL DEFAULT '',
password varchar(255) NOT NULL DEFAULT '',
type varchar(50) NOT NULL DEFAULT '',
label varchar(255) DEFAULT NULL,
url varchar(255) DEFAULT NULL,
avatar varchar(255) DEFAULT NULL,
github varchar(255) DEFAULT NULL,
twitter varchar(255) DEFAULT NULL,
facebook varchar(255) DEFAULT NULL,
google varchar(255) DEFAULT NULL,
weibo varchar(255) DEFAULT NULL,
qq varchar(255) DEFAULT NULL,
oidc varchar(255) DEFAULT NULL,
huawei varchar(255) DEFAULT NULL,
"2fa" varchar(32) DEFAULT NULL,
createdAt timestamp(0) without time zone NULL DEFAULT CURRENT_TIMESTAMP,
updatedAt timestamp(0) without time zone NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
);sql这段 SQL 是什么意思?
CREATE TABLE就是”新建一张表”,后面的括号里是表的列定义(相当于 Excel 的表头)。int是整数、varchar(255)是文本(最多 255 个字符)、timestamp是时间。PRIMARY KEY是主键——每条数据的唯一编号。
-
点击右下角的 Run 执行
-
执行时会弹出一个警告:“This query creates tables without enabling Row Level Security…” → 点击 Run 继续,不用管(第二节已经解释过,Waline 不需要 RLS)
-
看到 “Success” 提示后,回到 Vercel → Deployments → 再次 Redeploy
这次部署应该成功了!
四、第三步:注册管理员账号#
部署成功后,你获得了 Waline 后端的地址,类似:
https://你的项目名-git-main-你的用户名.vercel.appplaintext4.1 设置管理员密码#
- 浏览器打开
https://你的waline地址.vercel.app/ui - 第一次访问会进入注册页面
- 设定一个管理员密码,记好
- 注册成功后,你就进入了 Waline 管理后台
4.2 管理后台能做什么#
- 📝 评论管理:查看所有评论,可以通过、删除、标记为垃圾
- 📊 数据查看:每篇文章的浏览量和评论数
- 👤 用户管理:封禁恶意用户
- 🔔 通知设置:配置新评论的邮件/微信通知(可选)
五、第四步:打开前端开关#
后端已经就绪。现在让前端(你的博客)知道该去哪里找 Waline。
5.1 启用 Waline#
编辑 src/site.config.ts,找到这一段:
waline: {
enable: false
}ts改成:
waline: {
enable: true,
server: 'https://你的waline地址.vercel.app',
additionalConfigs: {
pageview: true,
comment: true
}
}ts| 字段 | 含义 |
|---|---|
enable: true | 打开 Waline 功能的总开关 |
server | Waline 后端的地址(告诉前端去这里找 API) |
pageview: true | 启用浏览量统计 |
comment: true | 启用评论计数显示 |
5.2 修复站点 URL#
astro.config.ts 里有一个 site 字段,它影响 RSS、sitemap 等功能的 URL 生成。如果你的站点域名已经换了,需要同步更新:
site: 'https://你的实际域名',ts5.3 默认开启评论功能#
src/content.config.ts 中,comment 字段的默认值是 false——这意味着每篇新文章都需要手动在 frontmatter 里加 comment: true 才会显示评论区。
把默认值改成 true,所有文章自动生效:
// 改之前
comment: z.boolean().default(false),
// 改之后
comment: z.boolean().default(true),ts5.4 部署前端#
构建、推送、部署到 Netlify。访问你的博客,随便点开一篇文章,滑到底部——你应该能看到 Waline 评论区了。文章标题下方会显示”X views · Y comments”。
六、你踩过的坑我都替你踩过了#
| 序号 | 坑 | 为什么会遇到 | 怎么解决 |
|---|---|---|---|
| 1 | Vercel 部署失败,找不到 dist | 克隆了整个 monorepo 仓库 | 使用 tree/main/example 后缀的模板链接 |
| 2 | 环境变量填了但连不上数据库 | 旧教程用的 LEAN_* 前缀是 LeanCloud 的,Supabase 要用 PG_* | 改用 PG_DB、PG_USER 等变量 |
| 3 | ENOTFOUND db.xxx.supabase.co | Supabase 免费版数据库只有 IPv6,Vercel 不支持 IPv6 | 改用 Shared Pooler(有 IPv4 地址) |
| 4 | relation "wl_users" does not exist | Pooler(PgBouncer)拦截建表 DDL 语句 | 在 Supabase SQL Editor 手动执行建表 SQL |
| 5 | 数据库密码含 @ 等特殊字符 | 连接字符串中 @ 是分隔符,密码里有会截断 | 把密码改成不含特殊字符的 |
每个坑的原因都可以归结为一句话:不同平台之间的兼容性假设不一致。Supabase 假设你的后端能连 IPv6,Vercel 假设你的数据库有 IPv4。PgBouncer 假设你不会通过它来建表。这些假设写在各自的文档角落里,不踩一遍根本不知道。
七、费用的最后确认#
全程零费用:
| 服务 | 免费额度 | 够用吗 |
|---|---|---|
| Supabase | 500MB 数据库 + 5GB/月流量 | 个人博客几年用不完 |
| Vercel | 100GB/月带宽 + 1000 次/天函数调用 | 完全够用 |
| Netlify | 100GB/月带宽 | 静态站点够了 |
你唯一需要付出的,就是这篇文章里踩坑的时间——但现在你已经看过了,照着操作 30 分钟搞定。