logo NodeSeekbeta

把你的 Hexo 博客接入 Fediverse —— VPS 部署 Hatsu 实战

本文首发于我的博客,原文链接《Hexo 博客接入 Fediverse —— Hatsu + Vercel 踩坑记》
版权声明请参阅原文。前往博客提高阅读体验。

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 插件会帮你处理好这个。如果你的博客没有,请检查配置。

<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>

Hatsu 部署

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

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

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

# 这是写作本文时的版本。请点击上面的「配置示例」查看最新版本,切勿照抄!!!
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。
echo "\nHATSU_ACCESS_TOKEN = \"$(cat /proc/sys/kernel/random/uuid)\"" >> .env

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

现在启动 docker:

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

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

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

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 域名!

{
  "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 软件上搜索账号 @[email protected] 应该就能请求成功了(这个示例是我的博客的 hatsu 用户,如果你搜索的话应该可以看到博客的简介)。因为上面创建了重定向,也可以直接查询 @[email protected],得到的账号是一样的~

AS2 Alternate

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

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

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

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

<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:

include ./head/fediverse.pug

然后,创建这个 fediverse.pug

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,然后配置文件稍微写一下:

# 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 应用。

示例如下:

{"@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/"}}

响应头是这样的:

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:

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

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

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

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

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

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

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

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

先增加第一个重定向:

"routes": [
{
    "src": "/(.*)",
    "has": [
    {
        "type": "header",
        "key": "Accept",
        "value": ".*(application/activity\\+json|application/ld\\+json).*"
    }
    ],
    "dest": "/api/apub?url=https://blog.samhou.moe/$1"
}
]

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

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 几天的改造,整个博客优化了几处小细节,增加了几个好用的新功能,还是挺不错的。

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

你好啊,陌生人!

我的朋友,看起来你是新来的,如果想参与到讨论中,点击下面的按钮!

📈用户数目📈

目前论坛共有59941位seeker

🎉欢迎新用户🎉