关于 Next.js SSG 生成 RSS Feed 文件的另一思路
2025/3/6
本文将简要介绍除使用 Next.js Route Handler 添加 RSS Feed 文件外的另一种 Dirty 但实用的思路。
前言
众所周知,Next.js App Router 并没有像 sitemap.ts
一样提供专门的生成入口函数给 RSS Feed 文件。然而,对于博客等内容分享类的站点来说,这又是一种刚需。
大部分情况下,基于 Next.js 开发的站点都会像这篇文章一样,通过添加一个 /feed
路由,并在里面创建 route.ts
,利用 Next.js 的 Route Handler 功能生成 RSS Feed 文件。
一般来说,这种做法才是最符合 Best Practice 的做法,并且在 SSR 站点上,只要在 GET()
函数中构建 Response
对象的时候指定好 Content-Type:application/rss+xml
头就能很好地工作。
但是,我的博客——不管是之前的 Allenyou1126/blog-ng-next
还是现在的 Allenyou1126/aki-ssg
,使用的都是 Next.js 的 SSG Mode。这样会导致生成的 /feed
路由被 Nginx 默认作为 text/plain
或者 text/html
输出。
这很不优雅。尽管在 Nginx 配置文件里面手动配置 location ^/feed
规则块手动指定 add_header Content-Type application/rss+xml;
也能达到相同的效果,但能在 Next.js 里实现的功能为什么要扯上 Nginx 呢?
因此,我希望找到另一条思路,生成一个静态的 feed.xml
文件。这样,Nginx 自动识别的 MIME 就会是 text/xml
——起码这样,当用户直接打开 RSS Feed 链接时,浏览器能够正确地将其作为 XML 文件渲染,而不是作为纯文本或者 HTML 渲染了。
RSS 内容生成
首先,我照常写了一个函数 generateRssFeed()
来生成 RSS Feed 文件的内容。
export async function generateRssFeed() {
const feed = new RSS({
title: config.blog.title,
description: config.blog.description,
site_url: `https://${config.blog.hostname}/`,
feed_url: `https://${config.blog.hostname}/feed.xml`,
language: "zh-CN",
custom_elements:
config.follow === undefined
? undefined
: [
{
follow_challenge: [
{ feedId: config.follow.feed_id },
{ userId: config.follow.user_id },
],
},
],
generator: "Aki-SSG",
});
const cms = await initCMS();
cms.getPostId().forEach((id) => {
const post = cms.getPost(id)!;
feed.item({
title: post.title,
description: post.markdown_content.toRssFeed(),
url: `https://${config.blog.hostname}/post/${id}`,
date: post.modified_at,
});
});
// 调用 feed.xml() 即可获得 XML 文本格式的 RSS Feed 内容
}
RSS Feed 文件位置
有了内容,我们就可以考虑将 RSS Feed 文件整出来了。在 Next.js 没有像 sitemap.ts
一样开洞的情况下,我们要指定某个路由的生成结果只有三种途径:
- App Router 页面
page.tsx
- Route Handler
route.ts
- 生成对应的文件,塞到
public
目录下面,然后通过 Next.js 会自动将public
目录下文件原样复制到站点根目录下的机制将它打包进去
显然,第一条只能生成 HTML 页面,首先出局。第二条就是我们开头说过的思路,排除。那我们就只剩下第三条路径了。
在 generateRssFeed()
函数的末尾加上写入 RSS Feed 内容到 public/feed.xml
相关代码,现在这个函数长这样:
export async function generateRssFeed() {
const feed = new RSS({
title: config.blog.title,
description: config.blog.description,
site_url: `https://${config.blog.hostname}/`,
feed_url: `https://${config.blog.hostname}/feed.xml`,
language: "zh-CN",
custom_elements:
config.follow === undefined
? undefined
: [
{
follow_challenge: [
{ feedId: config.follow.feed_id },
{ userId: config.follow.user_id },
],
},
],
generator: "Aki-SSG",
});
const cms = await initCMS();
cms.getPostId().forEach((id) => {
const post = cms.getPost(id)!;
feed.item({
title: post.title,
description: post.markdown_content.toRssFeed(),
url: `https://${config.blog.hostname}/post/${id}`,
date: post.modified_at,
});
});
await fs.promises.writeFile(
path.join(process.cwd(), "public", "feed.xml"),
feed.xml(),
{
flag: "w",
}
);
}
看起来很完美,不是吗?
顺带一提,这个函数可以在 Allenyou1126/aki-ssg
仓库的 src/utils/generateRssFeed.ts
找到。
什么时候调用?
现在我们有了一个能生成 feed.xml
的函数了,剩下要做的就是找个合适的时间调用它。
很遗憾,Next.js 并没有提供类似 postbuild
的 Hook。那么我们只能自己想办法。
显然,这个函数应该且必须被调用一次,而且只能在 pnpm build
时调用,不应该被带到 Client 中。
那么我们就不能将他放在页面渲染逻辑中了,剩下的必定会被,且只会在服务器端被执行一次的逻辑,就只有 robots.txt
/sitemap.xml
的生成逻辑了。
最后我选择将它塞到了 sitemap.ts
文件中的 sitemap()
函数里调用,看起来长这样:
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
// ...
// 这里是 sitemap() 函数的其他逻辑
// 由于Next没有提供合适的Hook,所以在这里生成RSS
generateRssFeed();
// 这里是 sitemap() 函数的其他逻辑
// ...
}
你可以在 Allenyou1126/aki-ssg
仓库的 src/app/sitemap.ts
找到这个函数。
现在,运行 pnpm build
,在 out
文件夹里面我们就能看到 feed.xml
了。
后记
我知道,这个思路其实有点 Dirty,而且也不符合 Best Practice。但是这确实更符合我的需求——起码可以少配一条 Nginx 配置规则了不是(逃)。
关于 Next.js SSG 生成 RSS Feed 文件的另一思路
https://www.allenyou.wang/post/25本文作者
秋实-Allenyou
授权协议
CC BY-NC-SA 4.0
加载评论中……