前言

写博客有个绕不开的问题: 读者怎么知道你有更新?

静态博客不像公众号那样有推送, 靠的是 RSS 订阅。但很多 Hexo 博客装好之后就没管过 RSS, 结果是:

  • 搜索引擎抓不到新文章(虽然有 sitemap 兜底, 但覆盖率不如 RSS)
  • 读者只能手动收藏首页
  • 想订阅 RSS 的朋友发现没有 atom.xml, 直接放弃

这篇记录 Hexo 博客接入 RSS 的完整流程。

原理: RSS 是什么

先说清楚原理, 后面的操作就好理解了。

RSS 本质上就是一个 XML 文件, 里面按时间顺序列着你最新的文章:

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>竹山一叶</title>
<link>https://zsyyblog.com</link>
<updated>2026-10-21T10:42:18.000Z</updated>
<entry>
<title>Hexo 博客添加 RSS 订阅</title>
<link href="https://zsyyblog.com/196ff3fc.html"/>
<updated>2026-10-21T10:42:18.000Z</updated>
<content type="html">...</content>
</entry>
</feed>

订阅者用阅读器(Feedly、Inoreader、RSSHub 等)定时拉取这个文件, 有新文章就推送。

关键点: RSS 文件是一个静态文件, 每次 hexo g 构建时自动重新生成。 不需要额外服务, 也不需要数据库。

第一步: 安装 feed 插件

Hexo 官方没有内置 RSS 支持, 需要装插件:

1
npm install hexo-generator-feed --save

装完会自动生成 atom.xml(和可选的 rss.xml)。

验证是否生效

1
hexo g

然后检查 public/ 目录:

1
ls public/*.xml

应该能看到 atom.xml 和 rss.xml。构建完不生成 XML, 就是插件没装上或者没启用。

本文写作时踩过一个坑: 习惯性写 hexo clean && hexo g, 结果 hexo clean 在 Windows 上卡了十几分钟没删完。原因是 hexo-fs 删目录时对所有文件发起无限并发的删除调用, 文件一多句柄就排队假死。文件多的话直接手动删更快:

1
rm -rf public db.json && hexo g

89 秒 vs 卡到没下文, 差别很大。

第二步: 配置输出

在 _config.yml 里加配置:

1
2
3
4
5
6
7
8
9
10
11
12
feed:
enable: true
type:
- atom
- rss2
path:
- atom.xml
- rss.xml
icon: https://img.zsyyblog.com/favicon.jpg
limit: 20
order_by: -date
autodiscovery: true

各字段含义:

配置项 说明 建议值
type 输出格式, atom 或 rss2 见下
path 输出文件名 与 type 一一对应
icon 订阅图标 图标要能被订阅源客户端直接拉到
limit 保留最近多少篇文章 20
order_by 排序字段, -date 是最新在前 -date
autodiscovery 自动在 <head> 注入 <link rel="alternate"> true

limit 别设太大。RSS 里塞几百篇全文, 拉取一次就好几 MB, 阅读器体验很差。20 篇左右最舒服。

⚠️ type 只认 atom 和 rss2 两个值。 写 rss 会被插件静默过滤掉 —— 不报错, 但 rss.xml 就是不生成。想同时输出两种格式, type 和 path 都得写成数组:

1
2
3
4
5
6
7
8
9
feed:
enable: true
type:
- atom
- rss2
path:
- atom.xml
- rss.xml
limit: 20

如果数组长度对不上(比如 type 写了两个但 path 只写一个), 插件会自动补齐, 但还是老实写全比较好。

关于 atom 还是 rss

简单说:

  • Atom —— 格式更规范, 支持全文和摘要两种模式, 主流阅读器都支持
  • RSS 2.0 —— 更老但更通用的格式, 兼容性好一些

建议两个都生成。Atom 给现代阅读器, RSS 2.0 兜住那些只认老格式的工具。两种格式指向同一批内容, 不会造成重复收录。

第三步: 在 Butterfly 中加订阅按钮

光生成 XML 还不够, 得让读者知道入口在哪。

Butterfly 自带的方案: 没有

先说个坑。Butterfly 5.x 主题里根本没有 RSS 按钮这个配置项 —— 不是默认关闭, 是主题源码里压根没有。你在 _config.butterfly.yml 里写:

1
2
3
rss:
enable: true
path: atom.xml

在 5.2.2 上是纯无效配置, 页面不会多出任何东西, 而且不会有任何报错提示你。网上很多教程(包括本文最初写的版本)都在讲这个 rss: 块, 但那是更早的版本或者别的分支。

验证方法很简单, 直接搜主题源码:

1
grep -rn "rss" themes/butterfly/layout/ themes/butterfly/scripts/

搜不到就是真没有。别浪费时间在配置上找。

零成本方案: 侧边栏社交图标

改主题模板之前, 先试试这个 —— Butterfly 的侧边栏头像下面有一排社交图标(GitHub、Email 那些), 那个位置固定在侧栏顶部, 每一页都在, 曝光比文章底部高得多。

它由 _config.butterfly.yml 的 social 配置驱动, 加一行就行, 不用碰任何主题文件:

1
2
3
4
social:
fab fa-github: https://github.com/zsyyblog || Github
fas fa-envelope: mailto:zsyyblog@qq.com || Email
fas fa-rss: /atom.xml || RSS订阅

格式是 图标类名: 链接 || 悬停提示 || 颜色, 三个部分都可以省。

主题的渲染源码在 themes/butterfly/layout/includes/header/social.pug, 一共就 8 行:

1
2
3
4
5
6
7
8
each url, icon in theme.social
-
const [link, title, color] = url.split('||').map(i => trim(i))
const href = url_for(link)
const iconStyle = color ? `color: ${color.replace(/[\'\"]/g, '')};` : ''
const iconTitle = title || ''
a.social-icon(href=href target="_blank" title=iconTitle)
i(class=icon style=iconStyle)

几个细节:

写法 说明
fas fa-rss 图标类名。fas 是 Font Awesome 的实心风格前缀, 别写成 fa-rss(不生效)
/atom.xml 相对站点根目录写。走 url_for() 处理, 部署到子目录也不会错
RSS订阅 鼠标悬停时的 title 提示
'#ee802f' 图标颜色(可选)。不写就继承主题默认色, 跟其他社交图标保持一致

颜色那一段是可选的, 建议直接省略。侧边栏就那么几个图标, 一个是橙的另外两个是灰的反而显得跳。想跟整个站的主题色统一, 让它继承 --font-color 就行。

真要单独指定颜色, 引号必须保留 —— 少了会被 YAML 解析成别的类型, 颜色就静默丢了。

想同时给 RSS 2.0 留个入口? 再加一行指向 /rss.xml 就行, 两个图标并排。不过一般一个就够, 阅读器都认 Atom。

这个方案我认为是性价比最高的: 零代码改动、主题升级不受影响、全站每页都显示。改主题模板那种方案留给需要自定义样式的情况。

更醒目的订阅入口

文章底部再放一个按钮, 转化率会更高。先说清楚 inject 的位置限制。 Butterfly 5.x 的 inject 实际只有两个键:

位置 插入点
head 所有页面的 <head> 里
bottom 所有页面底部(</body> 前)

没有 post-bottom 这样的文章级注入点。 网上教程里写的 inject.post-bottom 在这个版本不会被渲染。

所以想放在文章底部, 有两个办法:

办法一: 改主题模板(推荐)

在 themes/butterfly/layout/post.pug 里, 找到 include includes/post/post-copyright.pug 那一行(它在 .tag_share 之前), 在这两行之间插入:

1
2
3
4
5
//- RSS 订阅入口
.rss-subscribe
a(href=url_for('/atom.xml') target="_blank" rel="noopener")
i(class="fa-solid fa-rss")
span= '订阅本站更新'

注意缩进是 4 个空格 —— 这段代码在 article#article-container 块内部, 顶格写会被 Pug 当成另一个块, 直接编译报错。

对应样式加到 source/css/ 下(比如 rss.css), 再在 _config.butterfly.yml 的 inject.head 里引一下:

1
2
3
inject:
head:
- <link rel="stylesheet" href="/css/rss.css">

source/css/ 下的文件会被原样拷贝到 public/css/, 所以这里用绝对路径 /css/rss.css 引用就行。

用 url_for('/atom.xml') 而不是写死 /atom.xml, 这样部署在子目录时也能正确指向。图标类名 fa-solid fa-rss 依赖主题加载的 Font Awesome(通过 asset.fontawesome 配置项), Butterfly 默认带 FA6, 直接能用。

办法二: 放首页

如果不想改主题, 可以用 inject.bottom 加一段 JS, 在文章页动态插入按钮:

1
2
3
4
5
6
7
8
9
10
11
12
inject:
bottom:
- |
<script>
(function () {
var box = document.createElement('div');
box.className = 'rss-subscribe';
box.innerHTML = '<a href="/atom.xml" target="_blank" rel="noopener">📢 订阅本站更新</a>';
var c = document.getElementById('post');
if (c) c.appendChild(box);
})();
</script>

注意 pjax: 上面这段只在首次加载时执行, 切换页面后按钮会消失。需要额外监听:

1
document.addEventListener('pjax:complete', function () { /* 重新插入 */ });

所以还是办法一更省心。

这里的路径是站点根目录下的相对路径。如果你的博客部署在子目录, 需要改成 /子目录/atom.xml, 或者用 url_for() 自动处理。

第四步: 提交订阅源

生成好了就该让人订阅。以下平台的收录比较有效。

提交给阅读器服务

平台 地址 说明
Feedly feedly.com 老牌阅读器, 提交后聚合全网源
Inoreader inoreader.com 功能强, 免费版够用
RSSHub rsshub.app 聚合各类网站源
Feed43 feed43.com 免费, 自动发现新源
FeedBurner feedburner.com 需 Google 账号

大部分平台的提交流程都是: 注册 → 填入你的 RSS 地址 → 验证。

提交给搜索引擎

RSS 对搜索引擎的价值在于加速收录。sitemap 告诉搜索引擎”这些页面存在”, RSS 则是”这些是新的”。

RSS 和主动推送是两种不同机制, 各有各的用处:

方式 原理 触发时机
hexo-generator-baidu-sitemap 生成 baidusitemap.xml 提交给百度 手动在站长平台提交
hexo-baidu-url-submit 构建后自动 POST 新 URL 到百度 每次 hexo deploy 自动推送

sitemap 是”定期汇报全站有哪些页面”, 主动推送是”新文章一发布就通知”。两者不冲突, 有条件的话两个都配。

如果你的站目前只配了 sitemap 生成(也就是只生成 baidusitemap.xml 而没有自动推送), 那 RSS 作为另一种”告诉搜索引擎新内容”的途径, 同样有补充价值。

校验 RSS 是否有效

可以用在线工具检查:

校验报错别忽略。 常见问题是 XML 里有非法字符(比如文章里写了 & 但没转义), 这会导致某些阅读器整个源都读不出来。

常见问题

生成的是 atom.xml, 但别人要 rss.xml

前面说了, type 可以传数组同时输出两个。

订阅源里没有新文章

按顺序排查:

  1. 重新构建, 确认 XML 里有新 entry
  2. 检查 feed.limit 是不是太小(比如设成 1, 旧的被顶掉了)
  3. 确认 public/atom.xml 确实更新了
  4. 确认 CDN 缓存 —— 静态文件如果走了 CDN, 可能还在发旧版本

文章内容在订阅源里是乱码

先确认是真乱码还是显示问题。Atom 的规范格式要求 <?xml version="1.0" encoding="utf-8"?>, 插件生成时已经写死了 utf-8, 没有 charset 配置项可调。

所以如果你加了 charset: utf-8 却没效果 —— 因为这个配置项不存在, 加了也是被忽略。

真出现乱码, 查这两处:

  1. 文章源文件本身的编码是不是 UTF-8(GBK 存的中文站点很容易踩)
  2. description 字段里有没有非法字符 —— XML 里的 & 必须写成 &amp;, 直接放裸的会让整个订阅源解析失败

想让订阅源只显示摘要而不是全文

没有 excerpt 这个配置项(网上部分教程写错了)。控制全文还是摘要的开关叫 content, 默认 true(输出全文):

1
2
feed:
content: false

content: false 时, 每篇文章只输出摘要 —— 摘要优先取文章 front-matter 里的 description, 没有就取 excerpt, 都没有就截取正文前 content_limit 个字符(默认 140)。

想让摘要长一点:

1
2
3
feed:
content: false
content_limit: 300

全文订阅的好处是阅读器不跳转到你的站点, 体验好、也不会消耗你的服务器流量。摘要模式则相反, 能给站点导流。

两种都试试, 看看哪个数据好。

一个容易被忽略的点: RSS 也是 SEO 利器

前面提到 RSS 能加速收录, 这里展开说一下。

搜索引擎的爬虫会定期抓取你的 RSS。因为 RSS 里是纯文本 + 结构化的新文章链接, 比解析 HTML 页面高效得多。

而且 RSS 文件是静态的, 没有 JS 渲染的负担。百度对 JS 的处理能力本来就弱, RSS 相当于给它提供了一条不需要执行 JS 的抓取路径。

所以我的建议是:

  1. 务必开启 RSS
  2. 别忘了 autodiscovery: true —— 插件会自动往每个页面的 <head> 里插一条 <link rel="alternate" type="application/atom+xml">, 浏览器的”订阅此页”和大部分阅读器的自动发现都靠它, 这是最省事也最有效的入口
  3. robots.txt 里指向 sitemap(RSS 是自动发现的, 不用单独写)
  4. 保持 entry 的 updated 字段准确 —— 很多搜索引擎会把它当作内容新鲜度的信号

小结

接入 RSS 其实是 5 分钟的事:

  1. npm install hexo-generator-feed --save
  2. _config.yml 配 feed: 段(type 和 path 都写成数组, 注意是 rss2 不是 rss)
  3. 重新构建, 确认 public/atom.xml 和 public/rss.xml 都生成了
  4. 侧边栏加 RSS 图标(_config.butterfly.yml 的 social 加一行, 零代码改动, 全页可见)
  5. 想要更醒目的转化, 再改 themes/butterfly/layout/post.pug 加文章底部按钮(主题本身不带 RSS 功能, 别去找 rss: 配置)
  6. 提交到 Feedly / Inoreader 等平台

别小看这个文件。 它是静态博客里少数几个”让读者主动回访”的通道, 而且几乎没维护成本 —— 装一次插件, 之后每次构建自动更新。

最后提一个坑: 如果你的站点文件托管在对象存储/CDN 上, 不存在的文件可能不会返回 404,而是返回一个 200 的错误页(我们遇到的就是返回一张 16KB 的 JPEG 图片)。这种环境下, 别人提交你的 RSS 地址时, 对方会拿到图片而不是 XML, 直接判定订阅源无效。所以 RSS 上线后一定要自己 curl 一次, 确认拿回来的是 <?xml 开头的真 XML。

参考链接: