n8n 之同步博客到 Mastodon
- 4 分钟阅读
- CC BY-SA 4.0

n8n 之同步博客到 Mastodon
@eallion
前言
有很多方式来同步博客内容到 Mastodon,比如我曾经就是使用的 GitHub Actions 在构建时就同步了。但我们不是已经部署了一个 n8n 吗?就把同步流程搬到 n8n 上了。
n8n 是一个强大的自动化工具,可以用来备份 Mastodon 嘟文——《 n8n 之备份 Mastodon 嘟文》。今天我们来看看如何使用 n8n 将博客内容同步到 Mastodon。
流程简介
中间会引入
Sink 短链接服务,Sink 也是判断博客文章是否是新发布的依据。用 Sink 来生成 Mastodon 嘟文的短链接,创建博客文章到 Mastodon 嘟文的联系,把一篇博客文章和一条嘟文对应起来,建立双向链接。见文末评论区的 Discuss on Mastodon。Hugo 的 Comments.html 模板中的代码:
<a href="https://s.e5n.cc/{{ .Slug }}">Mastodon</a>
比如:
文章的 URL 是:https://www.eallion.com/n8n-sync-blog-to-mastodon
文章的 Slug 是:n8n-sync-blog-to-mastodon
Sink 短链接就是:Sink 域名 + Slug:
https://sink.to/n8n-sync-blog-to-mastodon
PS:n8n 中 Slug 提取代码:
const path = originalLink.split('/').slice(3).join('/'); // 移除协议和域名部分
const slug = path.replace(/\/$/, ''); // 去除末尾的斜杠
具体流程

- 触发器:监测 GitHub 博客仓库的 Push
- 触发后睡眠 5 分钟,等待 CI/CD 构建和 CDN 刷新,根据自己的时间调整
- 获取博客 RSS 的内容
- 处理 RSS 最新的一条内容
- 检测最新内容的 Sink 短链接是否存在,
404表示这篇文章还没有在 Sink 创建过,即这篇文章是新发布的 - 如果
404就发布到 Mastodon - 发布后创建 Sink 短链接
Mastodon 创建应用
在 Mastodon 实例中创建一个应用:
应用名称:随便填应用网站:https://n8n.example.com/rest/oauth2-credential/callback重定向 URI:https://n8n.example.com/rest/oauth2-credential/callback权限范围:write:statuses、可选write:media

n8n 凭证
在 n8n 面板中,点右上角的三角形,选择 Create Credentials,创建一个 Mastodon 凭证。
注意:Mastodon 不能用官方的,需要选择 OAuth2 API

n8n 流程
复制这个流程代码,保存为 .json 文件,然后在 n8n 中导入。
{
"name": "SinkBlogtoMastodon",
"nodes": [
{
"parameters": {
"owner": {
"__rl": true,
"value": "gh_username",
"mode": "name"
},
"repository": {
"__rl": true,
"value": "gh_blog_repo",
"mode": "name"
},
"events": [
"push"
],
"options": {}
},
"type": "n8n-nodes-base.githubTrigger",
"typeVersion": 1,
"position": [
-240,
-420
],
"id": "6ea99614-9c0f-49a7-88db-f552dbac7952",
"name": "Github Trigger",
"webhookId": "b33dca07-0939-4107-a315-231bfab13df3",
"credentials": {
"githubApi": {
"id": "8qzxqmikk9WEzTtg",
"name": "GitHub account"
}
}
},
{
"parameters": {
"url": "https://my.blog/rss",
"options": {}
},
"type": "n8n-nodes-base.rssFeedRead",
"typeVersion": 1.1,
"position": [
380,
-120
],
"id": "b48c23ba-ef9a-4c2a-8d0d-e73c0de35ec1",
"name": "BlogRSS",
"alwaysOutputData": true,
"executeOnce": true,
"retryOnFail": true,
"onError": "continueErrorOutput"
},
{
"parameters": {
"url": "={{ $json.newLink }}",
"options": {
"response": {
"response": {
"fullResponse": true,
"neverError": true
}
}
}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
900,
-120
],
"id": "9fe05f54-5107-40e4-b979-2e22e029d4cc",
"name": "FetchNewURL",
"alwaysOutputData": true,
"executeOnce": true,
"retryOnFail": true,
"notesInFlow": true,
"onError": "continueErrorOutput",
"notes": "Fetch New URL"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose",
"version": 2
},
"conditions": [
{
"id": "84ba6f92-1023-4902-997c-c6e3efd6ea38",
"leftValue": "={{ $json.statusCode }}",
"rightValue": "404",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"looseTypeValidation": true,
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
1160,
-120
],
"id": "fdf2464c-12c2-4ea8-85f5-f38ad9d0ff2a",
"name": "CheckSlugExists",
"executeOnce": true,
"retryOnFail": true,
"notesInFlow": true,
"onError": "continueErrorOutput",
"notes": "Check if 404"
},
{
"parameters": {
"method": "POST",
"url": "https://sink.to/api/link/create",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "authorization",
"value": "Bearer ehJy..."
},
{
"name": "content-type",
"value": "application/json"
}
]
},
"sendBody": true,
"bodyParameters": {
"parameters": [
{
"name": "url",
"value": "={{ $json.body.uri }}"
},
{
"name": "slug",
"value": "={{ $('ConvertBlogPost').item.json.slug }}"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1680,
-120
],
"id": "0b8ed4aa-ddba-4d1e-af96-24c39acb0b30",
"name": "SinkAPICreate",
"executeOnce": true,
"retryOnFail": true,
"notesInFlow": true,
"notes": "Shorter the link with Sink"
},
{
"parameters": {
"jsCode": "// 获取 RSS 节点的第一条数据的 link\nconst originalTitle = $input.all()[0].json.title;\nconst originalLink = $input.all()[0].json.link;\nconst originalContent = $input.all()[0].json.content;\n\n// 替换域名为 s.e5n.cc\nconst newLink = originalLink.replace('www.eallion.com', 's.e5n.cc');\n\n// 提取 slug(例如 nas-building)\n// 使用字符串方法提取路径部分\nconst path = originalLink.split('/').slice(3).join('/'); // 移除协议和域名部分\nconst slug = path.replace(/\\/$/, ''); // 去除末尾的斜杠\n\n// 返回 newLink、originalLink 和 slug\nreturn [\n {\n json: {\n originalTitle,\n newLink,\n originalLink,\n slug,\n originalContent,\n },\n },\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
640,
-120
],
"id": "2750e5ce-1a04-4cfd-bc53-27cd0aff166a",
"name": "ConvertBlogPost",
"executeOnce": true,
"retryOnFail": true,
"onError": "continueErrorOutput"
},
{
"parameters": {
"method": "POST",
"url": "https://mastodon.instance/api/v1/statuses",
"authentication": "genericCredentialType",
"genericAuthType": "oAuth2Api",
"sendBody": true,
"bodyParameters": {
"parameters": [
{
"name": "status",
"value": "=博客发布新文章:《{{ $('ConvertBlogPost').item.json.originalTitle }}》\n{{ $('ConvertBlogPost').item.json.originalLink }}\n{{ $('ConvertBlogPost').item.json.originalContent }}"
},
{
"name": "visibility",
"value": "public"
}
]
},
"options": {
"response": {
"response": {
"fullResponse": true,
"neverError": true
}
}
}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1420,
-120
],
"id": "6f734551-c959-41e2-b05e-8691e76a265d",
"name": "POSTtoMastodon",
"notesInFlow": true,
"executeOnce": true,
"retryOnFail": true,
"alwaysOutputData": true,
"credentials": {
"oAuth2Api": {
"id": "GqNI5NcHBoCsIywV",
"name": "Mastodon e5n"
}
},
"onError": "continueErrorOutput",
"notes": "POST to Mastodon statuses"
},
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
-200,
160
],
"id": "f38321ef-5d2b-46b6-ad80-bb9ddda155eb",
"name": "手动(测试用)",
"disabled": true
},
{
"parameters": {
"unit": "minutes"
},
"type": "n8n-nodes-base.wait",
"typeVersion": 1.1,
"position": [
40,
-420
],
"id": "4326ba6f-794e-420f-a961-998973c16989",
"name": "Wait",
"webhookId": "ad0849a8-63c0-4b84-97d8-86bfe59e3dab",
"alwaysOutputData": true,
"executeOnce": true,
"retryOnFail": true,
"notesInFlow": false
}
],
"pinData": {},
"connections": {
"Github Trigger": {
"main": [
[
{
"node": "Wait",
"type": "main",
"index": 0
}
]
]
},
"BlogRSS": {
"main": [
[
{
"node": "ConvertBlogPost",
"type": "main",
"index": 0
}
]
]
},
"FetchNewURL": {
"main": [
[
{
"node": "CheckSlugExists",
"type": "main",
"index": 0
}
]
]
},
"CheckSlugExists": {
"main": [
[
{
"node": "POSTtoMastodon",
"type": "main",
"index": 0
}
],
[],
[]
]
},
"ConvertBlogPost": {
"main": [
[
{
"node": "FetchNewURL",
"type": "main",
"index": 0
}
]
]
},
"POSTtoMastodon": {
"main": [
[
{
"node": "SinkAPICreate",
"type": "main",
"index": 0
}
]
]
},
"手动(测试用)": {
"main": [
[
{
"node": "BlogRSS",
"type": "main",
"index": 0
}
]
]
},
"Wait": {
"main": [
[
{
"node": "BlogRSS",
"type": "main",
"index": 0
}
]
]
}
},
"active": true,
"settings": {
"executionOrder": "v1"
},
"versionId": "f1aa189b-645b-44e1-906a-fdee06face62",
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "f9ad6c4f03ea7c2c4f16a8dda8e47b847dcd27971aa32eadc1b11bbddddddddd"
},
"id": "KzVqqCxAGWaVZAmG",
"tags": []
}