惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

S
Schneier on Security
有赞技术团队
有赞技术团队
T
The Blog of Author Tim Ferriss
F
Fortinet All Blogs
D
DataBreaches.Net
F
Full Disclosure
腾讯CDC
博客园 - 【当耐特】
MyScale Blog
MyScale Blog
Stack Overflow Blog
Stack Overflow Blog
小众软件
小众软件
Hugging Face - Blog
Hugging Face - Blog
Last Week in AI
Last Week in AI
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
爱范儿
爱范儿
The GitHub Blog
The GitHub Blog
Engineering at Meta
Engineering at Meta
大猫的无限游戏
大猫的无限游戏
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
S
SegmentFault 最新的问题
The Register - Security
The Register - Security
WordPress大学
WordPress大学
博客园 - 聂微东
雷峰网
雷峰网
J
Java Code Geeks
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
P
Privacy International News Feed
酷 壳 – CoolShell
酷 壳 – CoolShell
A
Arctic Wolf
Scott Helme
Scott Helme
C
Cyber Attacks, Cyber Crime and Cyber Security
T
Tor Project blog
博客园 - 三生石上(FineUI控件)
Know Your Adversary
Know Your Adversary
AWS News Blog
AWS News Blog
G
Google Developers Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
C
CERT Recently Published Vulnerability Notes
O
OpenAI News
Project Zero
Project Zero
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
Application and Cybersecurity Blog
Application and Cybersecurity Blog
云风的 BLOG
云风的 BLOG
N
News and Events Feed by Topic
MongoDB | Blog
MongoDB | Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Microsoft Security Blog
Microsoft Security Blog
Cisco Talos Blog
Cisco Talos Blog
P
Palo Alto Networks Blog
Schneier on Security
Schneier on Security

SamHou's Blog

总感觉有什么不对——Anemoi 全线点评 水星冲浪日志 3 —— Fediverse、Arch Linux 和写作风格 DroidSpaces 在安卓上跑 Linux 发行版踩坑实录 从零开始配置 VPS —— 主机名、用户组和远程权限安全实践 奶奶都能看懂的 C++ —— 类、初始化、预处理和分离式编译 让你验证请求来自 CF —— Authenticated Origin Pulls 攻略 水星冲浪日志 2 —— PT、换域名、约稿和建站机 奶奶都能看懂的 C++ —— 函数指针、decltype、类型别名和尾置返回 奶奶都能看懂的 C# —— LINQ、 Lambda 和 IEnumerable
Hexo 博客接入 Fediverse —— Hatsu + Vercel 踩坑记
Sam Hou · 2026-06-05 · via SamHou's Blog

Fediverse 这个东西,我个人是真的非常喜欢。在之前的杂谈里面,曾经提到过我到底是怎么接触到这个去中心化社交媒体的。

其实给博客接 Fedi 的想法已经由来已久了。几个月前,找到了 Hatsu 这个非常厉害的工具,于是接入了 Fediverse,每次博客更新的时候,Hatsu 都会自动把 Feed 里面的内容转换为 Fediverse 贴文。不过,在之前因为对文档的忽视和「能用就行」的想法,导致我只是部署了 Hatsu 后端,实现了博客 -> Fedi 的单向转换,有两个重要的问题没有解决:

  • Fedi 收到评论后,怎么把评论显示回来博客?
  • 读者知道一篇文章的 URL,怎么找到这个 URL 对应的 Fedi 帖子?由于去中心化的特点,在本地查看远程实例信息会出现帖子的遗漏,除非直接输入远程实例 URL 。但是 Hatsu 贴文的 URL 和博客文章的不是一个,它的格式是 https://feed.clanna.dev/posts/${url} 后面的 URL 是文章的原始 URL。也就是说,直接查询博文的原始 URL 会直接失败

所以这篇文章就来聊聊到底怎么实现双向互通。从 Hatsu 部署开始,到彻底互通,来说说一路上的坑。

在开始之前,先来说说我这套架构是什么:

  • 博客是部署在 Vercel 上的静态博客,接入的是 github 仓库,当我们推送更新时,vercel 拉取并执行 npm 命令,生成静态网站。
  • Hatsu 部署在 VPS 上,作为 Fedi 后端。上面注册了一个机器人用户,对应的就是博客的 Feed。

此外,如果你也想复刻这套方案,我强烈建议你先读一遍 Hatsu 官方文档,再来看跟着操作,否则……你懂得,出问题了别找我

Feed 准备

在一切开始之前,你得确保你的博客有 RSS,并且 RSS 可被自动发现。

也就是,html head 里面要有 link 指向你的 feed,比如下面。

注:大部分情况下都有,hexo rss 插件会帮你处理好这个。如果你的博客没有,请检查配置。

1
2
3
4
5
6
<head>
<link rel="alternate" type="application/feed+json" href="https://example.com/feed.json" />
<link rel="alternate" type="application/atom+xml" href="https://example.com/atom.xml" />
<link rel="alternate" type="application/rss+xml" href="https://example.com/rss.xml" />
</head>

准备好 RSS 之后,首先,咱们来讲讲 hatsu 的部署。

和官方文档不同,准备工作后面再讲,先讲部署,因为部署不是今天的重点。

打开配置示例,在你的 VPS 上创建一个文件夹来存储内容,然后创建 yml 的 compose。这个不多说,如果你不知道 docker,我建议你读一下之前的看番教程系列,详细介绍了如何用 docker。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 这是写作本文时的版本。请点击上面的「配置示例」查看最新版本,切勿照抄!!!
version: "3"

services:
hatsu:
container_name: hatsu
image: ghcr.io/importantimport/hatsu:nightly
restart: unless-stopped
ports:
- 3939:3939
# env_file:
# - .env
environment:
- HATSU_DATABASE_URL=sqlite://hatsu.sqlite3
- HATSU_DOMAIN=hatsu.example.com
- HATSU_LISTEN_HOST=0.0.0.0
- HATSU_PRIMARY_ACCOUNT=blog.example.com
volumes:
# - ./.env:/app/.env
- ./hatsu.sqlite3:/app/hatsu.sqlite3

要改的不多,就两处:

  • HATSU_DOMAIN=hatsu.example.com 改成你的域名,这是 hatsu 所在的域名,是一个独立的域用于 fedi 交换
  • HATSU_PRIMARY_ACCOUNT=blog.example.com 改成你的博客域名
    此外,你还需要生成 access token。
1
echo "\nHATSU_ACCESS_TOKEN = \"$(cat /proc/sys/kernel/random/uuid)\"" >> .env

然后取消上面配置文件中的注释加载环境变量。

现在启动 docker:

1
docker compose up -d && docker compose logs -f

不出意外的话就出意外了容器就起来了。

然后创建用户(记得改变量):

1
NAME="example.com" curl -X POST "http://localhost:$(echo $HATSU_LISTEN_PORT)/api/v0/admin/create-account?name=$(echo $NAME)&token=$(echo $HATSU_ACCESS_TOKEN)"

然后,用你各种手段,无论是 nginx 还是 caddy 还是 cf 的 tunnel,把 3939 端口反代出去到你的域名。这个不多说了,我相信看这篇文章的不是来学这个的。

现在后端 Fedi 部分已经部署完毕,接下来我们来看看前端吧~

重定向跳转

根据文档,设置自定义跳转内容。

让用户名可搜索

这个最简单,文档里都有现成的内容可以用,我给翻译到中文。

在你的博客项目根目录下,创建 vercel.json,填入下面内容。记得把 hatsu.local 改成你的 hatsu 域名!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"redirects": [
{
"source": "/.well-known/host-meta",
"destination": "https://hatsu.local/.well-known/host-meta"
},
{
"source": "/.well-known/host-meta.json",
"destination": "https://hatsu.local/.well-known/host-meta.json"
},
{
"source": "/.well-known/nodeinfo",
"destination": "https://hatsu.local/.well-known/nodeinfo"
},
{
"source": "/.well-known/webfinger",
"destination": "https://hatsu.local/.well-known/webfinger"
}
]
}

提交,等待 vercel 部署。

现在在你的 fedi 软件上搜索账号 @blog.samhou.moe@feed.clanna.dev 应该就能请求成功了(这个示例是我的博客的 hatsu 用户,如果你搜索的话应该可以看到博客的简介)。因为上面创建了重定向,也可以直接查询 @catch-all@blog.samhou.moe,得到的账号是一样的~

AS2 Alternate

细心的读者肯定发现了,我给出来的 hatsu 原版文档里面,可不止有用户名重定向,还有个叫 AS2 的重定向。

是的我几个月前没看到,搭建了个残废的 hatsu……

Redirects file only applies to .well-known. for AS2 redirects, you need to use AS2 Alternate.

点开,你会发现,你需要在你的博客文章的 head 中注入更多内容:

1
<link rel="alternate" type="application/activity+json" href="https://hatsu.local/posts/https://example.com/foo/bar" />

hmmm,看起来这个就有点难了,要按照 url,给每个页面增加独立的 href。对于接入 fedi 这种小众的事情,hexo 不可能原版有,你的主题也不一定有。

所以,我们来魔改主题源码鞭打LLM黑奴吧!我的 hexo 主题用的是 hexo-theme-butterfly,于是 fork 一份到自己这里,然后用 subtree 集成到我的博客里面进行开发。

首先我们找一找就不难发现,处理 head 的内容在 themes/butterfly/layout/includes/head.pug。简单搜索一下就知道,这个语法是 jade,一种用于生成 html 内容的模板。

那就好办了,直接加上 include:

1
include ./head/fediverse.pug

然后,创建这个 fediverse.pug

1
2
3
4
5
6
7
8
9
if theme.fediverse
link(rel="alternate"
type="application/activity+json"
href=new URL(`/posts/${urlNoIndex(null,config.pretty_urls.trailing_index,config.pretty_urls.trailing_html)}`,
theme.hatsu.instance).href)
link(rel="alternate"
type="application/ld+json"
href=new URL(`/posts/${urlNoIndex(null,config.pretty_urls.trailing_index,config.pretty_urls.trailing_html)}`,
theme.hatsu.instance).href)

OK,然后配置文件稍微写一下:

1
2
3
4
5
# Hatsu
# https://hatsu.local/
hatsu:
instance: https://feed.clanna.dev
fediverse: true

完成!再次 commit 之后推送。

现在在 fediverse 上搜索框直接键入文章 URL……

不出意外的话,就出意外了。

如果你用的是 mastodon misskey 这类软件,确实可以。但是很可惜,我用的是 sharkey 这个 misskey 分支,并没有成功识别这个 link 标签,而是直接报错。

细心的读者肯定也发现了,文档里面这么说:

Only Mastodon and Misskey (and their forks) is known to support auto-discovery, other software requires redirection to search correctly. w3c/activitypub#310

行,看来这条路是走通一半了,但没有完全走通。

根据请求头进行重定向

@skyone提醒下,我决定不止靠上面的 link 元素,而是从根源入手:

当收到来自 Fedi 软件的 activity pub 请求时,把请求交给 hatsu 处理
请求头 Accept 中会包含:application/ld+jsonapplication/activity+json

简单翻翻 vercel 的控制台,这个 Routing Rules 引起了我的注意。于是我们可以直接简单写个规则,当收到来自 activity pub 客户端的请求时,自动将请求交给 hatsu 处理。

路由规则

一切都很顺利……才怪勒!

我的 Sharkey 还是报错。仔细研究才发现:

由于使用的是 rewrite,所以 fedi 应用请求我的博客文章 https://blog.samhou.moe/aqua-surf-3/ 时,vercel 会代理请求,重写请求发送到 hatsu,此时 hatsu 返回一串 json。这个 json 是从 hatsu https://feed.clanna.dev/posts/https://blog.samhou.moe/aqua-surf-3/ 这个 URL 返回的 JSON 内容,发送回 vercel ,再发回给 fedi 应用。

示例如下:

1
{"@context":"https://www.w3.org/ns/activitystreams","id":"https://feed.clanna.dev/posts/https://blog.samhou.moe/aqua-surf-3/","type":"Note","published":"2026-05-24T07:35:00Z","attributedTo":"https://feed.clanna.dev/users/blog.samhou.moe","to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://feed.clanna.dev/users/blog.samhou.moe/followers"],"content":"<p>水星冲浪日志 3 —— Fediverse、Arch Linux 和写作风格</p>\n<p>《水星冲浪日志》的第三期,记录了博主加入 Fediverse 社交媒体的经历,Arch Linux 安装、磁盘加密和桌面环境的折腾,以及回首写博客旅途,自己文风的变化。</p>\n<p><a href=\"https://blog.samhou.moe/aqua-surf-3/\">https://blog.samhou.moe/aqua-surf-3/</a></p>\n\n<a href=\"https://feed.clanna.dev/t/%E6%9D%82%E8%B0%88\" rel=\"tag\">#<span>杂谈</span></a> <a href=\"https://feed.clanna.dev/t/fediverse\" rel=\"tag\">#<span>fediverse</span></a> <a href=\"https://feed.clanna.dev/t/arch\" rel=\"tag\">#<span>arch</span></a> <a href=\"https://feed.clanna.dev/t/linux\" rel=\"tag\">#<span>linux</span></a> <a href=\"https://feed.clanna.dev/t/writing\" rel=\"tag\">#<span>writing</span></a> <a href=\"https://feed.clanna.dev/t/misskey\" rel=\"tag\">#<span>misskey</span></a> <a href=\"https://feed.clanna.dev/t/sharkey\" rel=\"tag\">#<span>sharkey</span></a>","contentMap":null,"source":{"content":"水星冲浪日志 3 —— Fediverse、Arch Linux 和写作风格\n\n《水星冲浪日志》的第三期,记录了博主加入 Fediverse 社交媒体的经历,Arch Linux 安装、磁盘加密和桌面环境的折腾,以及回首写博客旅途,自己文风的变化。\n\nhttps://blog.samhou.moe/aqua-surf-3/\n\n#杂谈 #fediverse #arch #linux #writing #misskey #sharkey","mediaType":"text/markdown"},"tag":[{"type":"Hashtag","href":"https://feed.clanna.dev/t/%E6%9D%82%E8%B0%88","name":"#杂谈"},{"type":"Hashtag","href":"https://feed.clanna.dev/t/fediverse","name":"#fediverse"},{"type":"Hashtag","href":"https://feed.clanna.dev/t/arch","name":"#arch"},{"type":"Hashtag","href":"https://feed.clanna.dev/t/linux","name":"#linux"},{"type":"Hashtag","href":"https://feed.clanna.dev/t/writing","name":"#writing"},{"type":"Hashtag","href":"https://feed.clanna.dev/t/misskey","name":"#misskey"},{"type":"Hashtag","href":"https://feed.clanna.dev/t/sharkey","name":"#sharkey"}],"url":{"type":"Link","rel":"canonical","href":"https://blog.samhou.moe/aqua-surf-3/"}}

响应头是这样的:

1
2
3
4
5
HTTP/2 200
age: 0
cache-control: public, max-age=0, must-revalidate
content-type: application/activity+json; charset=utf-8
date: Fri, 05 Jun 2026 09:09:49 GMT

你有没有注意到一个根本性的异常?

请求的 URL 是 https://blog.samhou.moe/aqua-surf-3/,返回了 200,但是返回的 JSON 帖子却显示这个帖子的 URL 是 https://feed.clanna.dev/posts/https://blog.samhou.moe/aqua-surf-3/

虽然 sharkey 报错信息没见着,但是直接把后面帖子 URL 贴进去是可以识别的。

这说明什么?请求 URL 必须和贴文本身 URL 匹配!不能用 rewrite 这种类似「反向代理」的方式来返回内容给 fedi 软件。

不过这个也好解决,我们可以直接不用 rewrite 了,直接用 redirect 嘛。

改成 Redirect

此时你看了一眼这篇博客右边的滚动条和左边的目录,发现事情并没有这么简单。

完成这样的部署之后,Sharkey 依旧报错。

是的,这次 hatsu 也在报错了。

仔细一看一堆 404:

1
uri: /posts/https:/blog.samhou.moe/aqua-surf-3/

不是我斜杠怎么就剩下一个了?赶紧 curl 看看:

1
{ "redirect": "https://feed.clanna.dev/posts/https:/blog.samhou.moe/aqua-surf-3/", "status": "302" }

不是,哥们?由于 vercel 的妙妙处理,跳转的 url 直接干没了一个斜杠。

求助了狗屁通ChatGPT老师之后,才知道这次是遇到面板极限了。

进行了一番深入的探讨,G 老师建议我写一个边缘函数,结合 vercel.json 完成两次跳转。

也就是说,当检测到 fedi 软件请求时的请求路径:

1
request /blog-post 重定向 -> /api/apub?url=xxx 302 重定向 -> https://feed.clanna.dev/posts/xxxx

先增加第一个重定向:

1
2
3
4
5
6
7
8
9
10
11
12
13
"routes": [
{
"src": "/(.*)",
"has": [
{
"type": "header",
"key": "Accept",
"value": ".*(application/activity\\+json|application/ld\\+json).*"
}
],
"dest": "/api/apub?url=https://blog.samhou.moe/$1"
}
]

很好!稍微写点边缘函数:

1
2
3
4
5
6
7
8
export default async function handler(req, res) {
const url = req.query.url

res.redirect(
302,
`https://feed.clanna.dev/posts/${url}`
)
}

完美。现在提交并推送等待 vercel 构建。

试一试……完美!输入文章 URL,就可以直接跳转到目标帖子了!

自动跳转

自定义评论系统

在 Hatsu 的文档里面,还提到了你可以把来自 fedi 的评论集成回你的网站中。

文档里面,提到可以用 kkna(作者写的轻量加载器)或者 Mastodon Comments 来实现。

前者搞半天都失败,所以我就选择了后者。

直接把文档和 butterfly 主题的源码塞给 AI,让它来写。

在无数次 Vibe 出 Bug 之后,终于……

完成了下面的大作:

评论区示例

是的,按一下右上角的切换按钮就可以找到原来的 Artalk 了,而默认展示来自 Fediverse 的评论!

如果你也想用这个的话,我已经把修改版主题开源了,你可以也集成到你的 hexo 博客里面,也可以自己魔改。

经过我和 AI 几天的改造,整个博客优化了几处小细节,增加了几个好用的新功能,还是挺不错的。

那么这篇文章就到这里结束了,希望各位能有所收获~