為 VitePress 部落格加上 RSS Feed 訂閱功能
晚上花了一點時間實作日語文章的切換,剛好 Cash 大就在 X (Twitter) 上敲碗問我什麼時候要加 RSS Feed XD:

想想 RSS Reader 還是追蹤部落格最方便的方式,不用每天手動檢查有沒有新文章,於是決定著手加上這個功能。 但 VitePress 本身沒有內建 RSS 功能,但透過 buildEnd hook 和 feed 這個 npm 套件,其實實作起來並不複雜。
這篇文章就來記錄一下整個實作過程。
為什麼要提供 RSS Feed?
在社群媒體主導的年代,RSS 看起來有點過時,但其實還是有不少優點:
- 無演算法干擾:RSS 閱讀器會照時間順序顯示所有文章,不會被演算法過濾
- 隱私友善:不需要註冊帳號,不會被追蹤
- 跨平台:可以在各種裝置、各種 RSS 閱讀器中訂閱
- 離線閱讀:很多 RSS 閱讀器支援離線下載
- 方便整合:RSS 是標準格式,可以串接到各種自動化工具
對於技術部落格來說,讀者群中用 RSS 的比例還蠻高的,所以這個功能還是很值得做。
實作架構
整個 RSS 生成系統分成兩個部分:
生成腳本 (
scripts/generateRssFeed.ts):- 載入所有文章資料
- 建立 RSS Feed 物件
- 生成三種格式的 feed 檔案
VitePress 整合 (
docs/.vitepress/config.ts):- 在
buildEndhook 呼叫生成腳本 - 確保每次建置都會產生最新的 feed
- 在
技術選擇:
- feed 套件:業界標準的 RSS feed 生成工具
- VitePress
createContentLoader:複用現有的文章載入機制 - buildEnd hook:在建置完成後自動執行
第一步:安裝 feed 套件
首先要安裝 feed 這個 npm 套件,它可以幫我們產生符合標準的 RSS、Atom 和 JSON Feed。
pnpm add -D feed如果你用 npm 或 yarn:
npm install --save-dev feed
# or
yarn add -D feed第二步:建立 RSS 生成腳本
在專案中建立 scripts/generateRssFeed.ts:
import { Feed } from 'feed'
import { createContentLoader, type SiteConfig } from 'vitepress'
import path from 'path'
import { writeFileSync } from 'fs'
const siteUrl = 'https://kurohsu.dev'
const siteTitle = 'Kuro Hsu 的筆記'
const siteDescription = '技術筆記、學習紀錄與生活記錄'
export async function generateRssFeed(config: SiteConfig) {
// 建立 Feed 物件
const feed = new Feed({
title: siteTitle,
description: siteDescription,
id: siteUrl,
link: siteUrl,
language: 'zh-TW',
favicon: `${siteUrl}/favicon.ico`,
copyright: 'Copyright © 2025 Kuro Hsu',
feedLinks: {
rss: `${siteUrl}/feed.xml`,
atom: `${siteUrl}/atom.xml`,
json: `${siteUrl}/feed.json`,
},
author: {
name: 'Kuro Hsu',
link: siteUrl,
}
})
// 載入所有文章(包含 excerpt 和 rendered HTML)
const posts = await createContentLoader([
'notes/**/*.md',
'learn/**/*.md',
'misc/**/*.md'
], {
excerpt: true, // 載入摘要
render: true, // 產生完整 HTML
transform(rawData) {
return rawData
.filter(({ url }) => !url.endsWith('/index.html')) // 排除索引頁
.filter(({ url }) => !url.includes('.ja.html')) // 排除日文文章
.filter(({ frontmatter }) =>
frontmatter.title && frontmatter.date // 必須有標題和日期
)
.filter(({ frontmatter }) => !frontmatter.draft) // 排除草稿
.filter(({ frontmatter }) =>
frontmatter.lang !== 'ja' // 再次確認不是日文
)
.map(({ url, frontmatter, excerpt, html }) => {
// 將 URL 轉換為絕對路徑
const rewrittenUrl = url.replace(
/\/(notes|learn|misc)\/\d{4}\/\d{2}\/(.+\.html)/,
'/$1/$2'
)
return {
title: frontmatter.title,
url: `${siteUrl}${rewrittenUrl}`,
date: new Date(frontmatter.date),
description: excerpt || frontmatter.description || '',
content: html || '',
author: frontmatter.author || 'Kuro Hsu',
category: frontmatter.tags?.map((tag: string) => ({ name: tag })) || [],
}
})
.sort((a, b) => b.date.getTime() - a.date.getTime()) // 按日期降冪排序
}
}).load()
// 將文章加入 feed
for (const post of posts) {
feed.addItem({
title: post.title,
id: post.url,
link: post.url,
description: post.description,
content: post.content,
author: [
{
name: post.author,
link: siteUrl,
}
],
date: post.date,
category: post.category,
})
}
// 寫入三種格式的 feed 檔案
const outDir = config.outDir
writeFileSync(path.join(outDir, 'feed.xml'), feed.rss2())
writeFileSync(path.join(outDir, 'atom.xml'), feed.atom1())
writeFileSync(path.join(outDir, 'feed.json'), feed.json1())
console.log(`✅ RSS feeds generated with ${posts.length} posts:`)
console.log(` - ${siteUrl}/feed.xml (RSS 2.0)`)
console.log(` - ${siteUrl}/atom.xml (Atom 1.0)`)
console.log(` - ${siteUrl}/feed.json (JSON Feed 1.0)`)
}這個腳本做了幾件重要的事:
- 建立 Feed 物件:設定網站基本資訊和元資料
- 載入文章資料:使用 VitePress 的
createContentLoaderAPI - 過濾和轉換:排除不需要的頁面,轉換 URL 格式
- 產生三種格式:RSS 2.0、Atom 1.0、JSON Feed 1.0
第三步:整合到 VitePress 建置流程
在 docs/.vitepress/config.ts 加入 buildEnd hook:
import { defineConfig } from 'vitepress'
import { generateRssFeed } from '../../scripts/generateRssFeed'
export default defineConfig({
// 建置完成後生成 RSS feed
async buildEnd(siteConfig) {
await generateRssFeed(siteConfig)
},
// ... 其他設定
})就這麼簡單!每次執行 pnpm build 時,VitePress 會在建置完成後自動呼叫 generateRssFeed,產生最新的 RSS feed。
第四步:測試
執行建置命令:
pnpm build如果一切順利,你會在 console 看到:
✅ RSS feeds generated with 46 posts:
- https://kurohsu.dev/feed.xml (RSS 2.0)
- https://kurohsu.dev/atom.xml (Atom 1.0)
- https://kurohsu.dev/feed.json (JSON Feed 1.0)檢查 docs/.vitepress/dist 目錄,應該會看到三個檔案:
feed.xml- RSS 2.0 格式atom.xml- Atom 1.0 格式feed.json- JSON Feed 1.0 格式
你可以用 RSS 閱讀器測試這些 feed,確認內容正確。
為什麼要產生三種格式?
你可能會好奇,為什麼要同時產生 RSS、Atom 和 JSON Feed 三種格式?
RSS 2.0:
- 最古老也最廣泛支援的格式
- 幾乎所有 RSS 閱讀器都支援
- 檔案較小,適合傳輸
Atom 1.0:
- 較新的標準,規範更嚴謹
- 支援更多元資料(例如作者資訊)
- 某些現代工具偏好使用 Atom
JSON Feed:
- 最新的格式,使用 JSON 而非 XML
- 對開發者更友善,容易解析
- 適合用於 API 整合
feed 套件可以同時產生三種格式,幾乎沒有額外成本,所以就全部提供了。 這樣無論讀者用什麼工具,都能找到適合的格式。
關於文章過濾
在我的實作中,我刻意排除了日文文章:
.filter(({ url }) => !url.includes('.ja.html')) // 排除日文文章
.filter(({ frontmatter }) => frontmatter.lang !== 'ja')這是因為:
- 目標讀者:RSS 訂閱者主要是繁體中文讀者
- 避免重複:有些文章同時有中文和日文版本,不希望在 feed 中出現兩次
- 保持簡潔:Feed 只包含中文內容,訂閱者不會收到看不懂的文章
如果你的 VitePress 網站是單一語言,這段可以跳過當作沒看到,因為這是我自己的需求。
實用技巧:URL Rewriting
我的部落格文章檔案是按年月組織的(notes/2025/11/article.md),但對外的 URL 是扁平的(/notes/article.html)。
這是透過 VitePress 的 rewrites 功能實現的,但在 RSS feed 中也需要做同樣的轉換:
const rewrittenUrl = url.replace(
/\/(notes|learn|misc)\/\d{4}\/\d{2}\/(.+\.html)/,
'/$1/$2'
)這個正則表達式會:
- 匹配
/notes/2025/11/article.html這種格式 - 轉換成
/notes/article.html - 確保 RSS feed 中的連結跟實際 URL 一致
如果你的網站沒有用 rewrites,可以直接使用原始的 url。
踩過的坑
1. createContentLoader 的路徑問題
一開始我寫成:
const posts = await createContentLoader([
'docs/notes/**/*.md', // ❌ 錯誤
'docs/learn/**/*.md',
'docs/misc/**/*.md'
])結果載入不到任何文章。後來發現 createContentLoader 的路徑是相對於 docs 目錄,應該寫成:
const posts = await createContentLoader([
'notes/**/*.md', // ✅ 正確
'learn/**/*.md',
'misc/**/*.md'
])2. 忘記設定 excerpt 和 render
如果沒有設定這兩個選項:
{
excerpt: true, // 取得摘要
render: true, // 產生完整 HTML
}feed 中就不會有文章內容,只有標題和連結,體驗很差。
3. 建置時間變長
加入 RSS 生成後,建置時間會增加幾秒(我的部落格約增加 2-3 秒)。
這是正常的,因為需要:
- 載入所有文章資料
- 渲染完整的 HTML
- 產生三個 feed 檔案
如果建置時間成為問題,可以考慮:
- 只產生一種格式(例如只產生 RSS 2.0)
- 限制 feed 中的文章數量(例如只包含最新 20 篇)
- 不要 render 完整 HTML,只提供摘要
進階:只顯示最新文章
如果你的部落格文章很多,可以考慮只在 feed 中顯示最新的幾篇:
.sort((a, b) => b.date.getTime() - a.date.getTime())
.slice(0, 20) // 只取前 20 篇這樣可以:
- 減少 feed 檔案大小
- 加快建置速度
- 降低 RSS 閱讀器的負擔
不過我個人選擇包含所有文章,因為有些讀者可能想從頭開始閱讀,而且我的文章數量還不算太多。
讓讀者知道有 RSS
產生 RSS feed 後,別忘了讓讀者知道可以訂閱!
可以在網站上加個訂閱連結,例如在導覽列或頁尾:
<a href="/feed.xml">
<svg><!-- RSS icon --></svg>
訂閱 RSS
</a>或者在 <head> 加入 RSS 自動探索標籤,讓瀏覽器和 RSS 閱讀器可以自動找到 feed:
<link rel="alternate" type="application/rss+xml"
title="Kuro Hsu 的筆記"
href="https://kurohsu.dev/feed.xml" />VitePress 可以透過 head 設定來加入:
export default defineConfig({
head: [
['link', {
rel: 'alternate',
type: 'application/rss+xml',
title: 'Kuro Hsu 的筆記',
href: 'https://kurohsu.dev/feed.xml'
}]
]
})總結
為 VitePress 加上 RSS feed 功能其實很簡單,主要步驟就是:
- 安裝
feed套件 - 建立 RSS 生成腳本
- 在
buildEndhook 呼叫腳本 - 測試並部署
整個實作大約花了一小時左右,包含測試和調整。
使用 VitePress 的 createContentLoader API 讓事情變得更簡單,不需要自己去掃描和解析 markdown 檔案,也不需要擔心 frontmatter 格式。 而且因為是在打包建置時產生,所以不會影響網站的執行效能。
雖然裡面有超多客製化的部分是因應我自己的需求,但整體架構是通用的, 實作時可以根據自己的情況做調整。
如果你也在用 VitePress 架部落格,非常推薦加上這個功能。 雖然如今 RSS 已經不算是主流,但還是最方便的追蹤方式。
相關閱讀: