跳转到内容
关系图谱

feed:给博客配上 RSS 与 JSON Feed

1,921 字 7 分钟

属性 1
series
VaultKit 包指南

@vaultkit/vitepress-plugin-feed 是一个在 VitePress buildEnd 钩子里调用的 RSS + JSON Feed 生成器:一次调用同时产出 feed.xml(RSS 2.0)与 feed.json(JSON Feed 1.1),频道元数据全部从 site 配置与 sitemap.hostname 自动推导,零重复配置。

它的职责切分刻意保持克制:插件只管「频道怎么构造、文件怎么写」,「条目从哪来」完全委托宿主——博客主题传笔记集合、文档站可以传 changelog,插件不对内容模型做任何假设。这是纯构建期包,没有一行运行时代码,本文所有输出示例都以代码围栏呈现真实产物形态。

为什么需要它

RSS 是博客与读者之间最长寿的契约:不依赖任何平台的推荐算法,读者订阅一次,更新就直达阅读器。VitePress 内置了 sitemap 却没有 feed 能力,社区方案又常常把「扫描 md 文件」硬编码进插件里——目录结构稍有不同就水土不服。

本包换了一种切法:条目集合由调用方给,插件只负责把它变成规范的 RSS 2.0 与 JSON Feed 1.1 文档。这带来两个直接好处:

  1. 内容模型自由——你可以从 frontmatter、CMS、甚至数据库拼出 FeedItem[],publish 门控、排序口径都在自己手里
  2. 双格式一次搞定——JSON Feed 对程序化消费(做友链聚合、做 API)比 XML 友好得多,缺省一起输出

安装

sh
pnpm add -D @vaultkit/vitepress-plugin-feed

使用 @vaultkit/vitepress-theme-vault 主题则开箱即带:features.feed 缺省为 true,主题在 buildEnd 里把已发布笔记映射成条目自动调用本包,并在 <head> 里注入 rel="alternate" 的 RSS / JSON Feed 发现链接。想关闭时在 site 配置里显式置 false

ts
export default defineConfigWithTheme({
  extends: createVaultTheme({
    site: { /* ... */ },
    features: { feed: false },
  }),
})

快速上手

.vitepress/config.tsbuildEnd 钩子中调用,第一参数直接透传钩子拿到的 SiteConfig

.vitepress/config.ts
ts
import { generateFeed } from '@vaultkit/vitepress-plugin-feed'
import { defineConfig } from 'vitepress'

export default defineConfig({
  sitemap: { hostname: 'https://example.com' }, // hostname 从这里自动读取
  async buildEnd(config) {
    await generateFeed(config, {
      items: posts.map(post => ({
        title: post.title,
        url: post.url, // 站内路径,与 hostname 拼出条目链接
        date: post.date, // 'YYYY-MM-DD' 或 Date
        description: post.excerpt,
        categories: post.tags,
      })),
    })
  },
})

构建结束时控制台会打印 [feed] feed.xml + feed.json 已生成(N 条)

公开导出一览

导出形态说明
generateFeed异步函数(具名导出)生成入口,(config, options) => Promise<void>
默认导出同上import generateFeed from '...' 亦可
FeedOptions类型选项接口
FeedItem类型条目结构
FeedSiteConfig类型SiteConfig 的结构化子集(只消费 outDirsiteuserConfig.sitemap.hostname),免除对 vitepress 的类型依赖

FeedOptions 逐项详解

items(必填)

  • 类型:FeedItem[] | (() => FeedItem[] | Promise<FeedItem[]>)
  • 语义:条目集合,或按需计算的函数(构建期只调用一次,支持异步)。惰性函数形态适合「条目要读盘/请求才能拿到」的场景,避免配置文件顶层做重活:
ts
await generateFeed(config, {
  items: async () => {
    const posts = await loadPostsFromDisk()
    return posts.map(toFeedItem)
  },
})

hostname

  • 类型:string,默认取 VitePress sitemap.hostname
  • 语义:站点源,尾部 / 自动剥离。条目链接由 hostname + item.url 拼出。两处都没配时直接抛错(见常见问题),这是刻意的硬约束——feed 里的相对链接在阅读器中无法解析。

title / description / language

  • 类型:均为 string,默认分别取 site.titlesite.descriptionsite.lang ?? 'en'
  • 语义:频道级元数据。站点配置已有的信息不必重复写,只在「feed 想用不同文案」时覆盖,比如给 feed 单独起名:
ts
await generateFeed(config, { items, title: '数字花园 · 全文订阅' })
  • 类型:string,默认 © 当前年份 标题(如 © 2026 数字花园
  • 语义:RSS 频道的 <copyright> 行。想声明 CC 协议时覆盖:copyright: 'CC BY-NC-SA 4.0'

path

  • 类型:string,默认 'feed.xml'
  • 语义:RSS 输出文件名,相对 outDir。改成 'rss.xml' 之类的传统命名也行,注意同步修改 head 里的发现链接。

json

  • 类型:boolean,默认 true
  • 语义:是否同步输出 JSON Feed 1.1。只想要经典 RSS 时置 false,此时 feedLinks 中也不再声明 JSON 地址。

jsonPath

  • 类型:string,默认 'feed.json'
  • 语义:JSON Feed 输出文件名,相对 outDirjson: false 时无效。

FeedItem 逐字段详解

字段类型必填说明
titlestring条目标题
urlstring站内路径(如 '/my-post'),与 hostname 拼出条目链接,同时充当 <guid>
datestring | Date发布日期(YYYY-MM-DDDate);缺失时该条目被跳过并告警
descriptionstring条目摘要,进 <description> / summary
categoriesstring[]分类/标签名,RSS 里逐个成 <category>,JSON 里成 tags 数组

date 的可选性是「类型上可选、语义上强制」:RSS 阅读器按时间排序条目,无日期条目没有合法的呈现位置,所以插件选择跳过 + 打印 [feed] 跳过无日期条目:<标题>,而不是伪造一个当前时间——伪造日期会让老文章在订阅端「诈尸」置顶。

产物解析

feed.xml(RSS 2.0)

真实输出形态(feed 包渲染,含 atom:link 自引用与 CDATA 包裹的标题/摘要):

feed.xml
xml
<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>数字花园</title>
    <link>https://example.com/</link>
    <description>基于 Obsidian vault 的个人博客</description>
    <lastBuildDate>Thu, 09 Jul 2026 00:00:00 GMT</lastBuildDate>
    <language>zh-CN</language>
    <copyright>© 2026 数字花园</copyright>
    <atom:link href="https://example.com/feed.xml" rel="self" type="application/rss+xml"/>
    <item>
      <title><![CDATA[feed:给博客配上 RSS 与 JSON Feed]]></title>
      <link>https://example.com/feed-rss-json-feed</link>
      <guid>https://example.com/feed-rss-json-feed</guid>
      <pubDate>Thu, 09 Jul 2026 00:00:00 GMT</pubDate>
      <description><![CDATA[介绍 RSS 插件的设计与用法]]></description>
      <category>VaultKit</category>
      <category>RSS</category>
    </item>
  </channel>
</rss>

频道 lastBuildDate 取全部条目中最新的发布日期,而非构建时刻——同一内容多次构建,输出逐字节一致。

feed.json(JSON Feed 1.1)

feed.json
json
{
  "version": "https://jsonfeed.org/version/1.1",
  "title": "数字花园",
  "home_page_url": "https://example.com/",
  "feed_url": "https://example.com/feed.json",
  "description": "基于 Obsidian vault 的个人博客",
  "items": [
    {
      "id": "https://example.com/feed-rss-json-feed",
      "url": "https://example.com/feed-rss-json-feed",
      "title": "feed:给博客配上 RSS 与 JSON Feed",
      "summary": "介绍 RSS 插件的设计与用法",
      "date_modified": "2026-07-09T00:00:00.000Z",
      "tags": ["VaultKit", "RSS"]
    }
  ]
}

一个实现细节:底层 feed 包的 json1() 输出的是 version 1 标识,本插件解析后把 version 提升为 https://jsonfeed.org/version/1.1(1 与 1.1 的字段差异如 authors 对本内容模型无影响),并以 2 空格缩进 + 末尾换行落盘,产物对 diff 友好。

设计机制

UTC 零点:可复现构建

字符串日期统一按 UTC 零点解析:

ts
function toDate(value: string | Date): Date {
  return value instanceof Date ? value : new Date(`${value}T00:00:00Z`)
}

如果直接 new Date('2026-07-09') 交给本地时区解释,同一份内容在东八区 CI 和 UTC CI 上构建会得到不同的 pubDate——产物 diff 出现无意义噪音,CDN 缓存校验也会误判更新。锚定 UTC 零点后,构建在任何机器上输出一致。

无日期条目:跳过而非兜底

前文已述其订阅端语义;从工程视角看,这也是「fail loud」的温和版本——告警会出现在每次构建日志里,作者迟早会补上日期,而伪造日期的错误会静默存在到有读者投诉为止。

频道与条目的职责边界

FeedSiteConfig 类型只声明用到的三个字段,因此不产生对 vitepress 的编译期依赖;任何能凑齐 outDir + site + sitemap.hostname 形状的对象都可以驱动它,甚至能在 VitePress 之外单独使用。

常见问题

Q:构建报 [feed] 缺少 hostname 错? 未配置 VitePress sitemap.hostname 且未传 options.hostname。推荐配前者——sitemap、robots、feed 三件套共享一份 hostname,一处配置全站受益。

Q:某些文章没出现在 feed 里? 检查构建日志里的 [feed] 跳过无日期条目 告警,给对应文章的 frontmatter 补上日期即可。

Q:想输出全文而不是摘要?FeedItemdescription 就是条目正文位,传净化后的全文 HTML 亦可;但注意全文 feed 体积增长快,摘要 + 原文链接是更常见的取舍。

Q:如何让浏览器/阅读器自动发现 feed? 在 head 里注入发现链接(用主题则已自动注入):

ts
head: [
  ['link', { rel: 'alternate', type: 'application/rss+xml', title: '数字花园', href: '/feed.xml' }],
  ['link', { rel: 'alternate', type: 'application/feed+json', title: '数字花园', href: '/feed.json' }],
]

相关链接