搜索 K
主题
@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 文档。这带来两个直接好处:
FeedItem[],publish 门控、排序口径都在自己手里pnpm add -D @vaultkit/vitepress-plugin-feed使用 @vaultkit/vitepress-theme-vault 主题则开箱即带:features.feed 缺省为 true,主题在 buildEnd 里把已发布笔记映射成条目自动调用本包,并在 <head> 里注入 rel="alternate" 的 RSS / JSON Feed 发现链接。想关闭时在 site 配置里显式置 false:
export default defineConfigWithTheme({
extends: createVaultTheme({
site: { /* ... */ },
features: { feed: false },
}),
})在 .vitepress/config.ts 的 buildEnd 钩子中调用,第一参数直接透传钩子拿到的 SiteConfig:
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 的结构化子集(只消费 outDir、site、userConfig.sitemap.hostname),免除对 vitepress 的类型依赖 |
FeedItem[] | (() => FeedItem[] | Promise<FeedItem[]>)await generateFeed(config, {
items: async () => {
const posts = await loadPostsFromDisk()
return posts.map(toFeedItem)
},
})string,默认取 VitePress sitemap.hostname/ 自动剥离。条目链接由 hostname + item.url 拼出。两处都没配时直接抛错(见常见问题),这是刻意的硬约束——feed 里的相对链接在阅读器中无法解析。string,默认分别取 site.title、site.description、site.lang ?? 'en'await generateFeed(config, { items, title: '数字花园 · 全文订阅' })string,默认 © 当前年份 标题(如 © 2026 数字花园)<copyright> 行。想声明 CC 协议时覆盖:copyright: 'CC BY-NC-SA 4.0'。string,默认 'feed.xml'outDir。改成 'rss.xml' 之类的传统命名也行,注意同步修改 head 里的发现链接。boolean,默认 truefalse,此时 feedLinks 中也不再声明 JSON 地址。string,默认 'feed.json'outDir;json: false 时无效。| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
title | string | 是 | 条目标题 |
url | string | 是 | 站内路径(如 '/my-post'),与 hostname 拼出条目链接,同时充当 <guid> |
date | string | Date | 否 | 发布日期(YYYY-MM-DD 或 Date);缺失时该条目被跳过并告警 |
description | string | 否 | 条目摘要,进 <description> / summary |
categories | string[] | 否 | 分类/标签名,RSS 里逐个成 <category>,JSON 里成 tags 数组 |
date 的可选性是「类型上可选、语义上强制」:RSS 阅读器按时间排序条目,无日期条目没有合法的呈现位置,所以插件选择跳过 + 打印 [feed] 跳过无日期条目:<标题>,而不是伪造一个当前时间——伪造日期会让老文章在订阅端「诈尸」置顶。
真实输出形态(feed 包渲染,含 atom:link 自引用与 CDATA 包裹的标题/摘要):
<?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 取全部条目中最新的发布日期,而非构建时刻——同一内容多次构建,输出逐字节一致。
{
"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 零点解析:
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:想输出全文而不是摘要?FeedItem 的 description 就是条目正文位,传净化后的全文 HTML 亦可;但注意全文 feed 体积增长快,摘要 + 原文链接是更常见的取舍。
Q:如何让浏览器/阅读器自动发现 feed? 在 head 里注入发现链接(用主题则已自动注入):
head: [
['link', { rel: 'alternate', type: 'application/rss+xml', title: '数字花园', href: '/feed.xml' }],
['link', { rel: 'alternate', type: 'application/feed+json', title: '数字花园', href: '/feed.json' }],
]