(又一次)更换博客评论系统
又一次在博客的评论系统上左右摇摆了。
距离上一次迁移博客评论系统到依靠 GitHub Discussions 的 Giscus 应用(为博客支持评论系统)已经过去三年半 了,也是时候鼓捣点新玩意了。在前年也有变化是依赖 Mastodon 进行接入评论(使用 Mastodon 作为博客的评论系统),但不出意 外我非常懒,不想在 Fedi 应用先发文拿到 URI 后再回来更新博文元数据连接博文和 activityPub 资源。究其原因是 Mastodon 这应用 的 URI 是用雪崩算法之类生成的,没法提前拿到,在构建时检查然后通过 API token 发帖也是一种办法,不过这也不能算「优雅」。所以到最后,要 完全掌握相关的设计就只能自己动手了。其实在三年多前我就有在设计自己的 Fedi 应用了 1 ,不过更多是作为概念验证没写多少就晾在 一旁,最近回来倒是依据「想将博客评论系统依托在这上面构建」的想法将功能给完善。
开发
接入动感酒吧(ActivityPub)本身不算什么难事,往底层看其实无非就是两个站点互相丢 JSON 而已,但真要做起来还是要花挺大功夫。首先为 了稳定必须要有个异步队列系统;其次是以 2018 年的 ActivityPub 规范来看,现在的 ActivityPub 环境已经有很多不同了,更多可以查看:
- ap-next/ap-next: ActivityPub Next - Codeberg.org: 推荐,更实用的 activityPub 开发指导
- fediverse/fep: Fediverse Enhancement Proposals - Codeberg.org
- Guide for new ActivityPub implementers - SocialHub
资源标识
因为是自己写的,所以可以设定资源的建立方式,这里为了不在 CJK 文字上栽跟头就选择将域名和文章 slug 做次哈希成资源标识了, 哈希算法选择简单的 fnv-1a ,这个算法简单但是数据输入产生的扰动也足够大,用 scheme 写起来不用十行就能实现:
(define (fnv-1a str)
(let ((bytes (string->utf8 str)))
(let loop ((i 0) (h 14695981039346656037))
(if (= i (bytevector-length bytes))
(number->string h 16)
(loop (+ i 1)
(logand (* (logxor h (bytevector-u8-ref bytes i))
1099511628211)
#xffffffffffffffff))))))例如本文章就可以用 blog.southfox.me+/2026/08/update-comment-system-again/ 作为输入最后得到 3c42353677587d26 ,因为空间足够
大也不做什么碰撞检测,要是真出现资源碰撞情况我可要大声嚷嚷自己的「幸运」了。
确立资源标识算法后就是确定资源绑定策略,如果想比较透明的话可以选择像 Giscus 一样,在收到评论请求后再去创建相关资源,不过考虑下还是觉得这种 方式可控性不高所以还是选择通过在构建时取最新文章然后计算资源标识符通过 API 创建资源了,相关 API 是自己写的也是能够自己提前指定了(搞了大半个月总 算能蘸醋了)。
绑定
虽然这样看来和手动流程没什么区别,但关键在于——因为是自己实现的 ActivityPub 可以做到更多事,例如比起常见的 Note 类型,在发 布时可以指定成 Article 类型:
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1"
],
"type": "Article",
"id": "https://southfox.gay/tail/5e4c0ea72702337b",
"attributedTo": "https://southfox.gay",
"content": "",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://southfox.gay/followers"
],
"published": "2026-08-01T00:40:45Z",
"url": "https://blog.southfox.me/2026/07/fox-thinking-36/",
"tag": [],
"summary": null,
"inReplyTo": null,
"sensitive": false,
"attachment": [],
"name": "我想知道的,前人已经说过了"
}区别是在不少 ActivityPub 应用上,点击「跳转到源站」,会是文章的 URL( https://blog.southfox.me/2026/07/fox-thinking-36/ ) 而不是资源 的 URL( https://southfox.gay/tail/5e4c0ea72702337b ),这一点对于文章发布来说体验上是更好的。
除了 Article 类型,也可在 HTML 的 head 中插入 alternate link 帮助 ActivityPub 进行资源发现:
<link href="https://southfox.gay/tail/5e4c0ea72702337b" rel="alternate" title="ActivityPub (JSON)" type="application/activity+json">这样 ActivityPub 在搜索栏搜索文章的 URL 也能自动识别并跳转到 ActivityPub 资源进行互动,不过看起来这个功能并不是所有的 ActivityPub 应用 都支持(我进行的测试 mastodon 支持但 gotosocial 不支持),所以只能作为一个兜底。
原始回归
除了关联 ActivityPub ,我还额外实现了 web 直接提交的评论,主要还是出于「惯性」,觉得总归需要这种形式的评论的,反正我这个博客互动量也少,所 以可预计的负担并不算很大。并且评论甚至连昵称都可以选择不填(灵感参考 Isso ),毕竟这就是互联网最开始的原始风 味,没有验证没有负担,在互联网上没人知道你是一条狐狸。
而且更「奇怪」的决定是,这里的评论甚至可以用 Orgmode 来写,原因很简单,就是应用本身设计就可以用 Orgmode 向联邦宇宙发帖,给评论系统复用倒 是「顺带」的事。不过支持 Markdown 的评论系统到处都是,出现个只支持 Orgmode 的评论系统也挺新奇的不是吗?
Webmention
最后还抽空做了 Webmention 支持,也是两三年前就计划做的事但现在才完成了,也是因为实现了异步任务队列后发现能「顺带」支持了 Webmention ,感想是 比起 ActivityPub ,Webmention 还真是简单啊……
前端
前端上还是使用 LIPS 写点 scheme 代码操作些 dom ,lips 在不加载标准库的情况下用起来确实还是很受限制的,得自己不停尝试这两 种语言的边界线。为什么不用标准库?因为 lips 本身就三百多 kb 了,再额外加上一百多 kb 的标准库就感觉有点过大了,所以就只能自己从底层开始往上 构建了,虽然很破车但至少还是实现了……
总结
勉强算是完成了自建的评论系统,不过总体感觉还是有点虚,除了对 XSS 的担心外 2 重点还是 web 评论方面没做限流和验证防护,不过想 到 ActivityPub 面也有这个问题就想着推迟到之后做决定看看两端能不能共用一层中间件……嗯,总之多想想总是没错的。
脚注
1 southfox/foxhole - Forgejo: Beyond coding. We Forge.
2 网页端倒是直接使用了 Element: setHTML() method - Web APIs | MDN ,如果是没有这个特性的浏览器就直接插入字面形式的 HTML 。
