本文最后更新于:2024年1月1日 早上
场景
吾辈最近从 docusaurus 迁移到了更好更快的 vitepress,由于也需要生成 rss,但又找不到合适的插件,所以参考网络教程写了一下。虽然之前使用 docusaurus 时也是自行实现,但使用 vitepress 的 build hooks 还是踩到了新的坑。
实现
参考 网络上的文章 和 vue blog,主要思路是在构建时能够拿到所有 html,然后使用 feed 生成需要的 rss。
安装依赖
然后在 .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: '', })
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), )
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 ---
|
看起来就完成了,对吧?
问题
不幸的是,还有一些边缘问题没有解决。
- 生成 rss 中包含
​
字符,这是错误的,vue blog 之前就有这个问题。
- 生成的 rss 中如果包含图片,那么链接不是正确的链接
例如在 markdown 中使用 ![cover](./assets/cover.jpg)
相对路径引用图片,在 rss 中看到的将是 <img src="./assets/cover.jpg" alt="cover">
,它并未并正确渲染为最终的地址。vue blog 的解决方法是永远使用绝对路径,将图片都放在 public
目录中。
- 如果你的站点目录在 node_modules 下,那么使用
createContentLoader
将无法获取任何 markdown,因为它默认会忽略 node_modules 目录,当然这是因为吾辈使用 vitepress 二次封装导致的。
解决
不生成 ​
这处理起来非常简单,只需要一个简单的字符串替换即可。
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('​', ''), 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 一个数量级。