为我的博客自建评论系统,摆脱 Giscus 局限
背景:为什么放弃 Giscus?
我遇到的问题
我的博客之前一直使用 Giscus 作为评论系统,它依托 GitHub Discussions 实现,开箱即用。但在长期使用中逐渐暴露了几个痛点:
-
登录状态不统一
博客本身有账号密码登录体系,但 Giscus 只支持 GitHub 登录。用户每次评论都需要跳转到 GitHub 授权,评论完又回到博客,来回切换身份,体验非常割裂。 -
样式难以完全自定义
Giscus 虽然提供了一定程度的主题配置,但底层基于 Web Component(<giscus-widget/>),Shadow DOM 天然隔离样式,外层 CSS 无法穿透。即使通过 CDN 加载自定义 CSS,仍有部分样式无论如何都无法覆盖。 -
功能无法与后端深度集成
评论数据托管在 GitHub Discussions,但读写必须走 Giscus 前端的 SDK,无法与我的 Cloudflare Workers 后端做任何联动——比如评论审核、敏感词过滤、用户权限判断等统统无法在后端实现。 -
评论登录方式单一,无法兼容多种账号体系
Giscus 只认 GitHub 登录。但我的博客除了 GitHub OAuth 登录外,还有独立的账号密码登录体系。这意味着账号密码用户无法在文章下方评论,只能干看着,或者逼迫他们也去注册 GitHub——这显然不合理。未来还计划接入微信扫码等更多登录方式,Giscus 的单一入口模式根本无法满足。
解决思路
我已有的 Cloudflare Workers 博客后端已经具备完整的用户系统(账号密码登录 + GitHub OAuth 登录)。思路很直接:
在 Worker 后端集成 GitHub API,直接操作 Discussions,由后端统一代理所有评论的读写。
这样:
- 前端只需一个简单的评论组件,不再依赖 Giscus 的 Web Component
- 样式完全由自己控制,不受 Shadow DOM 限制
- 登录体系统一:无论是 GitHub 登录用户还是密码登录用户,都能通过同一个后端发评论
- 评论数据仍保留在 GitHub Discussions,不丢失历史
- 后端可以统一做敏感词过滤、反垃圾、评论审核等逻辑
整体架构
系统分三层:
- 前端:轻量评论组件,负责展示评论树和输入框
- 后端 Worker:代理所有评论操作,处理身份认证,决定使用哪种 Token 调用 GitHub API
- 数据层:GitHub Discussions(评论存储)+ 自有数据库(点赞、自定义表情等,因为下文会提到 GitHub API 在这两方面的局限)
数据流示意:
前端评论组件 → HTTP POST → Worker API
├─ 检查用户登录态
├─ 有 GitHub Token → 以用户身份调 GitHub GraphQL API
└─ 无 Token → 用 App Installation Token 代写
Worker → 返回前端统一格式的评论数据
评论与页面的对应关系(理解原理的关键)
很多读者第一次接触会好奇:博客的文章页面和 GitHub Discussions 是怎么对应上的?
答案很简单——每个页面的路径名就是 Discussion 的标题。
一条评论的完整生命周期
假设有一篇博客文章路径为 /posts/hello-world:
-
文章发布时:
我手动创建了一个 Discussion,标题设为 "/posts/hello-world",
放在 "Announcements" 分类下。 -
用户打开文章时:
- 浏览器加载前端评论组件
- 组件向后端 Worker 发请求:GET /api/comments?path=/posts/hello-world
- Worker 收到后,去 GitHub 搜 Discussion:
搜索条件 → repo:pc-Blog/next "/posts/hello-world" in:title - 找到对应的 Discussion 后,拉取下面所有评论
- Worker 将评论按父子关系(评论 + 回复)重组
- 返回 JSON 给前端渲染
发表评论的具体数据流——GitHub 用户如何走、密码用户如何走、旧 Giscus 评论如何兼容——在下方「核心实现」章节的「发评论:两种策略按用户身份路由」之后有详细举例说明,这里先不展开。
之所以用路径名作为 Discussion 标题,是为了和 Giscus 的
data-mapping="pathname"保持一致——如果你原来就用 Giscus,迁移后历史评论一条都不会丢。
多篇文章共享一个 Discussion?还是各自独立?
绝大多数博客都是一篇文章对应一个 Discussion。因为:
- 每篇文章都有自己的 URL 路径
- 搜索时按路径精准匹配,互不干扰
- 如果想对特定文章的评论做批量操作,直接操作对应的 Discussion 即可
但也有例外——如果你用的是 GitHub Pages 这类单页博客,或者用 title 映射而非 pathname,那就按标题匹配。本方案默认按路径匹配,和 Giscus 一致。
准备工作
1. 打开仓库的 Discussions 功能
进入仓库 Settings → General 页面,下滑到 Features 模块,找到 Discussions 复选框并打开。开启成功后,页面上方标签栏会出现 Discussions 标签。

2. 获取 Discussions 分类 ID 和名称
Discussions 分类的 ID 无法在 GitHub 页面直接查看,最简单的方法是通过 giscus.app 获取(前提是仓库已安装 Giscus App)。
- 安装 Giscus App → GitHub Apps - giscus,选择目标仓库
- 打开 giscus.app,填入仓库地址和分类名称,页面会自动生成完整的配置代码段

giscus 页面生成的 script 标签中包含 data-category 和 data-category-id,记下来,后面要用。
<script src="https://giscus.app/client.js"
data-repo="pc-Blog/next"
data-repo-id="R_kgDOSk99gw"
data-category="Announcements"
data-category-id="DIC_kwDOSk99g84C9uoJ"
data-mapping="pathname"
data-strict="0"
data-reactions-enabled="1"
data-emit-metadata="0"
data-input-position="bottom"
data-theme="preferred_color_scheme"
data-lang="zh-CN"
crossorigin="anonymous"
async>
</script>
3. 注册 GitHub App
注意:必须注册 GitHub App,而不是 OAuth App。OAuth App 只能做用户登录认证,无法以 App 身份代写评论。

生成 Private Key
在 GitHub App 设置页的 Private keys 区域,点击 Generate a private key,浏览器会自动下载一个 .pem 文件,例如 ppc-blog.2026-06-26.private-key.pem,里面就是 RSA 私钥内容,后续用于签发 JWT。

编辑权限
在 Permissions & Events 页面,将 Discussions 权限设为 Read and write。

安装 App 到仓库
将 GitHub App 安装到个人账号,并选择目标仓库。

注意:如果在安装后更新了权限,需要在个人账号设置页面的 Installed GitHub Apps 中重新审核(Review)才能生效。
4. 获取必要数据清单
下面列出搭建评论系统所需的所有配置项及获取方式:
Installation ID
安装 GitHub App 后,进入个人 Settings → Applications → Installed GitHub Apps,点击你的 App 的 Configure。此时浏览器地址栏类似:
https://github.com/settings/installations/61234567
末尾的数字 61234567 就是 Installation ID。
完整配置清单
| 配置 | 获取位置 |
|---|---|
| 仓库(owner/name) | 仓库主页 URL,例如 pc-Blog/next |
| App ID | GitHub App 设置页顶部(数字) |
| Installation ID | 见上方两种方式 |
| Private Key | App 设置页生成的 .pem 文件内容 |
| Client ID | GitHub App 设置页 Basic information |
| Client Secret | GitHub App 设置页,生成后保存 |
| Discussions 分类名称 | giscus.app 或仓库 Discussions 页面查看 |
| Discussions 分类 ID | giscus.app 生成的配置段中获取 |
核心实现:一步一步拆解
以下是我在 Worker 中实现评论功能的核心流程,每个步骤只展示关键 API 调用,完整代码在项目仓库中。
认证:签发 JWT → 换取 Installation Token
GitHub App 认证分两步走:
Private Key → RS256 签名 → JWT(10分钟有效)
JWT → POST /app/installations/{id}/access_tokens → Installation Token(1小时有效)
拿到 Installation Token 后,后续所有 API 调用都通过 Authorization: Bearer {token} 进行。
如果你有用户的 GitHub OAuth Token,也可以直接用用户 Token 调 API,好处是评论会显示为用户本人身份。
查评论:按页面路径搜索 Discussion
核心思路是通过 GraphQL Search 找到当前页面对应的 Discussion(按 pathname 匹配,与 Giscus 的 data-mapping="pathname" 一致):
query($q: String!) {
search(query: $q, type: DISCUSSION, first: 1) {
nodes {
... on Discussion {
id
comments(first: 100) {
nodes { id body createdAt author { login avatarUrl } replyTo { id }
replies(first: 50) { nodes { id body createdAt author { login avatarUrl } } } }
}
}
}
}
}
拿到数据后,在代码中按 replyTo 字段将评论和回复重组成树形结构返回给前端。
发评论:两种策略按用户身份路由
这是整个系统最巧妙的部分——根据用户身份选择不同的调用方式:
用户提交评论 → Worker 验证 session
├─ GitHub 登录用户 → 用 userToken 调 addDiscussionComment → 显示为本人
└─ 账号密码用户 → 用 appToken 调 addDiscussionComment → 显示为 bot,body 嵌入用户信息
使用的 mutation 是一样的:
mutation($did: ID!, $b: String!) {
addDiscussionComment(input: { discussionId: $did, body: $b }) {
comment { id body createdAt author { login avatarUrl } }
}
}
差异在于:
- GitHub 用户:
$b直接放评论内容,评论显示为用户本人 - 账号密码用户:
$b前面加一段 HTML 注释嵌入用户身份,例如<!--u:12345|小明|https://...-->\n评论内容。GitHub 上看到的是 App Bot,但前端渲染时会解析注释头,展示真实昵称和头像。
下面用三个具体场景展示完整的数据流动过程,以便理解不同身份的用户评论时,后端到底做了什么。
场景一:GitHub 登录用户评论
[前端] 小明(GitHub 用户)已登录博客,打开文章 /posts/hello-world
[前端] 输入"好文章!",点击提交
[前端] POST /api/comments
请求体:{ path: "/posts/hello-world", content: "好文章!" }
[后端] Worker 检查 session → 发现小明有 GitHub Token(OAuth 登录时保存的)
[后端] 直接用小明的 userToken 调 GitHub GraphQL:
mutation($did: "DIC_kwDOSk99g84C9uoJ", $b: "好文章!")
[GitHub] Discussion 中新增一条评论
作者:xiaoming(小明的 GitHub 账号)
body: "好文章!"
[后端] 返回给前端:
{ id: "DC_xxx", author: "xiaoming", avatar: "https://...", content: "好文章!" }
[前端] 评论区出现小明的评论,显示他的 GitHub 头像和用户名
✅ 优点:身份真实,body 干净
❌ 局限:只有 GitHub 登录的用户才能走这个流程
场景二:账号密码登录用户评论(App 代发)
[前端] 小红(密码用户)已登录博客,打开同一篇文章 /posts/hello-world
[前端] 输入"写得太棒了!",点击提交
[前端] POST /api/comments
请求体:{ path: "/posts/hello-world", content: "写得太棒了!" }
[后端] Worker 检查 session → 小红没有 GitHub Token(她是密码登录的)
[后端] Worker 取出 App 的 Installation Token(缓存中,若过期先签发)
[后端] 合成带身份信息的 body:
"<!--u:42|小红|https://blog.cdn/avatar/xiaohong.png-->\n写得太棒了!"
[后端] 用 appToken 调 GitHub GraphQL:
mutation($did: "DIC_kwDOSk99g84C9uoJ", $b: "<!--u:42|小红|...png-->\n写得太棒了!")
[GitHub] Discussion 中新增一条评论
作者:pc-blog[bot](因为用的是 App Token)
body: "<!--u:42|小红|...png-->\n写得太棒了!"
[后端] 读取刚写入的评论,解析 body 中的注释头:
提取 → { id: "42", nickname: "小红", avatar: "https://blog.cdn/avatar/xiaohong.png" }
[后端] 替换 author 信息后返回给前端:
{ id: "DC_yyy", author: "小红", avatar: "https://blog.cdn/avatar/xiaohong.png", content: "写得太棒了!" }
[前端] 评论区出现小红的评论,显示她的昵称和头像
用户完全看不出这其实是 App 机器人代发的
✅ 优点:没有 GitHub 账号的用户也能参与评论
⚠️ 代价:GitHub 上看到的是 bot,但前端展示是真实用户
场景三:旧的 Giscus 评论原样保留(迁移无感)
[背景] 博客之前一直用 Giscus,GitHub Discussions 里已有大量旧评论
旧 Discussion 标题也是 "/posts/hello-world",分类也是 "Announcements"
[用户] 打开文章 → 前端 GET /api/comments?path=/posts/hello-world
[后端] 按 pathname 搜索 → 找到同一个 Discussion
[后端] 拉取 Discussion 下的所有 comments 和 replies
旧评论的 body 没有 "<!--u:...-->" 注释头(因为之前是 Giscus 发的)
[后端] 无需特殊处理,直接返回原始数据:
[
{ author: "laowang", content: "沙发!", replies: [...] },
{ author: "zhang3", content: "好文!", replies: [...] },
]
[前端] 按普通评论渲染,显示 GitHub 用户本人的身份
[混排效果] 同一篇文章下的评论区:
— laowang:沙发! ← 旧 Giscus 评论(无注释头)
— 小明:好文章! ← 场景一(GitHub 用户新评论)
— 小红:写得太棒了! ← 场景二(密码用户,前端解析后显示)
[用户视角] 三条评论排在一起,看不出有任何迁移痕迹
✅ 向后兼容:无需迁移数据,无需修改旧 Discussion,一切开箱即用
删评论:手工级联
GitHub API 不会自动删回复,需要三步走:
查回复 → 逐条删回复 → 删父评论
核心也是两个 GraphQL 调用——先 query 查 replies,再循环 deleteDiscussionComment。
怎么把上面的流程变成代码?
上面给出了所有关键的 GraphQL 查询和变更。如果你不想手写代码,直接把这几段 GraphQL 丢给 AI(如 Codex、ChatGPT、Claude),告诉它你要在 Cloudflare Workers 里用 TypeScript 实现,它就能帮你生成完整的函数实现——包括 JWT 签名、API 封装、错误处理等。
我就是这么写的。
GitHub API 的局限性
在实际开发中,我遇到了两个 GitHub API 无法直接解决的问题:
1. Emoji 表情
Discussions 的 addDiscussionComment Mutation 不支持添加 Reaction 表情。虽然有 addReaction Mutation,但它是针对 Issue/PR 的,Discussions 走的是另一套机制,且无法自定义表情种类。
解决:放弃 GitHub API,在自己数据库中维护一套表情系统,前后端自己实现。
2. 点赞功能
GitHub 明确禁止 App Token 调用 addUpvote Mutation,即使赋了 Read and write 权限也不行。用户 Token 虽然可以点赞,但无法给 App 代发的评论点赞。
解决:点赞系统完全由自己的数据库维护,与 GitHub Discussions 解耦。
展示效果
最终效果如下:

途中遇到的问题汇总
| 问题 | 原因 | 解决 |
|---|---|---|
| Installation Token 过期 | Token 有效期仅 1 小时 | 在 Worker 中缓存 Token,过期前刷新 |
| JWT 签名失败 | Private Key 格式不对 / 算法不匹配 | 确保使用 RS256,PEM 格式含完整头尾标记 |
| App 权限更新后不生效 | GitHub 需要用户重新审核 | 到个人 Settings → Applications → Installed GitHub Apps → Configure → 重新同意权限 |
| addUpvote 返回权限不足 | GitHub 限制 App Token 调此接口 | 改用自有数据库维护点赞 |
| 删除评论后回复成孤儿 | GitHub API 不级联删除 | 先查出所有回复再逐条删除 |
| Discussion 搜索不到 | 搜索索引有延迟 | 等待几秒后重试,或确认 pathname 与页面路径完全一致 |
参考资料
🌐 欢迎关注我的其他平台:
📧 联系我:mail@lxpavilion.top

浙公网安备 33010602011771号