秋实-Allenyou 的小窝

稻花香里说丰年,听取 WA 声一片

关于 Next.js SSG 生成 RSS Feed 文件的另一思路

本文将简要介绍除使用 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

加载评论中……