使用 vitepress 时生成 rss

本文最后更新于:2024年1月1日 早上

场景

吾辈最近从 docusaurus 迁移到了更好更快的 vitepress,由于也需要生成 rss,但又找不到合适的插件,所以参考网络教程写了一下。虽然之前使用 docusaurus 时也是自行实现,但使用 vitepress 的 build hooks 还是踩到了新的坑。

实现

参考 网络上的文章vue blog,主要思路是在构建时能够拿到所有 html,然后使用 feed 生成需要的 rss。

安装依赖

1
pnpm i -D feed

然后在 .vitepress/config.ts 中编写部分逻辑即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import { createContentLoader, defineConfig } from 'vitepress'
import { Feed } from 'feed'
import { writeFile } from 'fs/promises'
import * as path from 'path'

export default defineConfig({
title: 'My Awesome Project',
description: 'A VitePress Site',
async buildEnd(siteConfig) {
const hostname = 'https://example.com'
const feed = new Feed({
id: hostname,
title: siteConfig.site.title,
description: siteConfig.site.description,
link: hostname,
copyright: '',
})

// 过滤出所有的 markdown 文件
const posts = await createContentLoader('*.md', {
excerpt: true,
render: true,
}).load()

// 按照时间排序
posts.sort(
(a, b) =>
+new Date(b.frontmatter.date as string) -
+new Date(a.frontmatter.date as string),
)

// 添加到 feed 中
for (const { url, excerpt, frontmatter, html } of posts) {
feed.addItem({
title: frontmatter.title,
id: `${hostname}${url}`,
link: `${hostname}${url}`,
description: excerpt,
content: html,
author: feed.options.author ? [feed.options.author] : undefined,
date: frontmatter.date,
})
}

// 生成并写入文件
await writeFile(path.join(siteConfig.outDir, 'feed.rss'), feed.rss2())
},
// 其他配置...
})

现在使用 pnpm vitepress build 时能够在 .vitepress/dist 下找到 feed.rss 了,控制生成 rss 的排序可以在 markdown 中使用 frontmatter 来声明日期。

1
2
3
---
date: 2024-01-01
---

看起来就完成了,对吧?

问题

不幸的是,还有一些边缘问题没有解决。

  1. 生成 rss 中包含 ​ 字符,这是错误的,vue blog 之前就有这个问题。
    vitepress-rss-error-demo.jpg
  2. 生成的 rss 中如果包含图片,那么链接不是正确的链接
    例如在 markdown 中使用 ![cover](./assets/cover.jpg) 相对路径引用图片,在 rss 中看到的将是 <img src="./assets/cover.jpg" alt="cover">,它并未并正确渲染为最终的地址。vue blog 的解决方法是永远使用绝对路径,将图片都放在 public 目录中。
  3. 如果你的站点目录在 node_modules 下,那么使用 createContentLoader 将无法获取任何 markdown,因为它默认会忽略 node_modules 目录,当然这是因为吾辈使用 vitepress 二次封装导致的。

解决

不生成 &ZeroWidthSpace;

这处理起来非常简单,只需要一个简单的字符串替换即可。

1
2
3
4
5
6
7
8
9
10
feed.addItem({
title: frontmatter.title,
id: `${hostname}${url}`,
link: `${hostname}${url}`,
description: excerpt,
- content: html,
+ content: html?.replaceAll('&ZeroWidthSpace;', ''),
author: feed.options.author ? [feed.options.author] : undefined,
date: frontmatter.date,
})

关于这个错误 vue blog 也存在,所以简单提了 一个 pr 修复了它。

正确处理图片

图片的处理相对较为复杂,createContentLoader 拿到的 markdown 渲染的 html 并不是最终的 html,所以它们的图片链接也是不对的。在与 vitepress 维护者沟通 之后,了解到需要通过 transformHtml hooks 来获取最终渲染的图片地址。

安装处理 html 需要的依赖

1
pnpm i -D node-html-parser

由于 html 解析和序列化相对较慢,所以判断 markdown 中是否包含图片,如果包含才去处理它。其中重要的一点是如何映射在 transformHtml 和 createContentLoader 获得的 html,幸运的是可以通过一些转换得到。另一件事就是在 transformHtml 的 html 中包含了 ssr 相关的代码,在 rss 中不需要它们,需要清理掉。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
const map: Record<string, string> = {}
export default defineConfig({
// 其他代码...
transformHtml(code, id, ctx) {
if (!/[\\/]404\.html$/.test(id)) {
map[id] = code
}
},
async buildEnd(siteConfig) {
// 其他代码...

function getAbsPath(outDir: string, p: string): string {
if (p.endsWith('.html')) {
return path.join(outDir, p)
}
if (p.endsWith('/')) {
return path.join(outDir, p, 'index.html')
}
return p
}
async function cleanHtml(
html: string,
baseUrl: string,
): Promise<string | undefined> {
const { parse } = await import('node-html-parser')
const dom = parse(html).querySelector('main > .vp-doc > div')
dom?.querySelectorAll('img').forEach((it) => {
it.setAttribute(
'src',
new URL(it.getAttribute('src')!, baseUrl).toString(),
)
})
return dom?.innerHTML
}
for (let { url, excerpt, frontmatter, html } of posts) {
if (html?.includes('<img')) {
const htmlUrl = getAbsPath(siteConfig.outDir, url)
if (map[htmlUrl]) {
const baseUrl = path.join(hostname, siteConfig.site.base)
html = await cleanHtml(map[htmlUrl], baseUrl)
}
}
// 其他代码...
}

// 其他代码...
},
})

不要忽略 node_modules

一般直接将 vitepress 作为依赖生成文档站不会有问题,但如果你从 node_modules 下某个临时目录生成网站,那么 createContentLoader 就不能正常工作,但修复也很简单,只需要覆盖 glob.ignore 选项即可。

1
2
3
4
5
6
7
const posts = await createContentLoader('**/*.md', {
excerpt: true,
render: true,
+ globOptions: {
+ ignore: ['dist', ...(rss.ignore ?? [])],
+ },
}).load()

总结

vitepress 的性能可能是目前最好的,仍然有各种小问题,但仍然非常棒,在之前的性能测试中,甚至超过了 docusaurus 一个数量级。


使用 vitepress 时生成 rss
https://blog.rxliuli.com/p/8956b229025844859429e252f9591080/
作者
rxliuli
发布于
2023年12月31日
许可协议