RSS订阅源虽然不如社交平台热闹,但对技术博客、产品更新日志和新闻站点来说仍是稳定的分发渠道。很多Next.js项目在实现RSS时容易走弯路:把XML字符串拼在客户端组件里,或者用一个静态文件手动更新,结果要么阅读器抓不到,要么每次发布文章都要重新构建。其实Next.js提供了成熟的服务端能力,可以在不增加额外服务器的情况下输出标准RSS 2.0格式。下面从服务端生成的原因、App Router和Pages Router的具体实现,以及缓存策略几个方面把流程拆开。

一、为什么RSS Feed必须放在服务端生成
RSS阅读器抓取订阅源时,发送的是一个普通HTTP请求,然后直接解析响应体里的XML内容。它不会加载页面里的JavaScript,也不会等待React组件完成渲染。如果把RSS生成逻辑写在客户端组件中,浏览器需要先下载脚本、执行代码、再把XML文本挂到页面上,但抓取机器人根本看不到这一步,最终只能拿到一个空的HTML壳。这是很多项目订阅源失效的主要原因。
Next.js的服务端能力正好可以解决这个问题。不管是用App Router的Route Handler,还是Pages Router的API Route,它们都运行在Node.js环境里,可以直接读取数据库、调用内部接口、拼装XML字符串并设置响应头。服务端渲染RSS Feed和渲染页面的思路一致:服务器在发送响应前把所有内容准备好,客户端只负责接收。
另一个考虑是维护成本。手动维护一份静态rss.xml文件虽然简单,但每发一篇文章就要改一次,容易漏掉。用服务端代码在每次请求时查询最新文章可以自动保持同步,不过也要引入缓存策略,避免数据库被频繁访问。因此,方案设计通常需要在实时性和资源消耗之间做取舍。
二、在App Router中使用Route Handler输出RSS
Next.js 13引入的App Router把接口处理和页面渲染统一到了文件系统路由下。要生成RSS Feed,可以新建一个app/feed/rss/route.ts文件,导出一个GET函数。这个函数在每次请求时执行,返回一个Response对象。相比传统API路由,Route Handler的写法更接近Web标准。
下面是一个基于xmlbuilder2库的完整示例。它能把XML节点结构转成字符串,避免手工拼接时忘记转义特殊字符。安装命令很简单,在项目根目录执行npm install xmlbuilder2即可。
import { create } from 'xmlbuilder2';
type Post = {
title: string;
slug: string;
description: string;
publishedAt: string;
};
async function getPosts(): Promise<Post[]> {
// 从CMS或数据库读取最近文章
return [];
}
export async function GET() {
const posts = await getPosts();
const doc = create({ version: '1.0', encoding: 'UTF-8' });
const rss = doc.ele('rss', { version: '2.0' });
const channel = rss.ele('channel');
channel.ele('title').txt('我的技术博客').up();
channel.ele('link').txt('https://ipipp.com').up();
channel.ele('description').txt('记录前端与后端工程实践').up();
channel.ele('language').txt('zh-cn').up();
for (const post of posts) {
const item = channel.ele('item');
item.ele('title').txt(post.title).up();
item.ele('link').txt(`https://ipipp.com/posts/${post.slug}`).up();
item.ele('guid').txt(`https://ipipp.com/posts/${post.slug}`).up();
item.ele('pubDate').txt(post.publishedAt).up();
item.ele('description').txt(post.description).up();
item.up();
}
const xml = doc.end({ prettyPrint: true });
return new Response(xml, {
headers: {
'Content-Type': 'application/rss+xml; charset=utf-8',
'Cache-Control': 's-maxage=60, stale-while-revalidate=300',
},
});
}
代码里最关键的设置是响应头。如果返回text/html,有些阅读器会把它当成普通网页处理,甚至拒绝解析。必须明确指定application/rss+xml; charset=utf-8。另外,Cache-Control头控制了CDN和浏览器的缓存时间,s-maxage=60表示CDN缓存60秒,stale-while-revalidate=300则允许在缓存过期后的5分钟内先返回旧内容,同时后台刷新,对RSS源来说性价比较高。
访问http://localhost:3000/feed/rss就能看到XML输出。开发环境启动后如果浏览器显示的是纯文本XML树,说明Content-Type正确;如果什么都没有或提示下载,需要检查路径和导出函数名。建议再用一个本地RSS阅读器测试,确认条目能正常解析。
三、Pages Router下的API路由与SSR页面实现
还在使用Pages Router的项目可以借助API路由来生成RSS Feed。创建pages/api/rss.ts文件,用NextApiRequest和NextApiResponse处理请求。下面的例子直接拼接了一个最小可用的RSS结构,适合文章数量较少或者不想引入额外依赖的场景。
import type { NextApiRequest, NextApiResponse } from 'next';
export default function handler(req: NextApiRequest, res: NextApiResponse) {
const posts = [
{
title: '示例文章',
link: 'https://ipipp.com/posts/example',
pubDate: 'Mon, 06 Jan 2025 12:00:00 GMT',
},
];
const items = posts
.map(post => {
return `<item><title>${post.title}</title><link>${post.link}</link><pubDate>${post.pubDate}</pubDate></item>`;
})
.join('');
const rss = `<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"><channel>${items}</channel></rss>`;
res.setHeader('Content-Type', 'application/rss+xml; charset=utf-8');
res.write(rss);
res.end();
}
注意这里虽然能正常运行,但标题或描述里如果包含<、>等字符,XML就会解析失败。生产环境建议使用专门的XML构建库,或者至少实现一个escapeXml函数把特殊字符替换成实体。
Pages Router还有一种更贴合“服务端渲染”概念的做法,就是在页面级使用getServerSideProps获取数据后直接写响应。创建pages/rss.xml.tsx文件,虽然扩展名是.tsx,但组件本身可以返回null。代码看起来像这样:
import { GetServerSideProps } from 'next';
type Post = {
title: string;
slug: string;
description: string;
publishedAt: string;
};
async function fetchPosts(): Promise<Post[]> {
return [];
}
export const getServerSideProps: GetServerSideProps = async ({ res }) => {
const posts = await fetchPosts();
const rss = buildRss(posts);
res.setHeader('Content-Type', 'application/rss+xml; charset=utf-8');
res.write(rss);
res.end();
return { props: {} };
};
export default function RssPage() {
return null;
}
这种写法复用了页面路由的请求生命周期,能使用中间件、权限控制等已有逻辑。但相比API Route,它的语义不够直观,而且需要额外处理buildRss函数。如果项目已经计划迁移到App Router,建议新的RSS接口直接写在Route Handler中,避免后期维护两套约定。
四、缓存策略与XML字段细节
RSS源一旦上线,很可能被大量阅读器周期性抓取。如果每次都实时查询数据库,除了自身压力,还容易被CDN或代理缓存放大成多个请求。推荐在响应头里显式设置缓存策略。除了前面提到的s-maxage,还可以使用stale-while-revalidate,让旧内容先返回给阅读器,同时触发后台更新,这样用户不会遇到延迟,源站也不会被突发流量打垮。
如果文章发布频率很低,也可以考虑在构建阶段预生成静态XML文件。比如写一个脚本在next build前运行,把生成的RSS写入public/rss.xml。这种方式不消耗运行时数据库资源,但发布新文章必须重新构建或触发部署。对于内容更新非常快的站点,动态生成加短缓存是更合适的组合。
XML结构本身也需要注意几个字段。<guid>应该使用文章的唯一URL而不是标题,这样阅读器才能正确判断条目是否已读。<pubDate>需要符合RFC 822日期格式,例如Mon, 06 Jan 2025 12:00:00 GMT。时间统一使用GMT时区,避免本地时区差异导致阅读器排序混乱。最后别忘了在<channel>里提供站点标题、链接和描述,这三个字段是RSS 2.0的基础信息。
整体来说,Next.js中实现服务端渲染RSS Feed并不复杂。App Router的Route Handler是当前最推荐的方式,Pages Router下也可以用API Route平稳过渡。关键是选好生成XML的工具、设置正确的响应头,并给接口加上合理的缓存规则,这样RSS订阅源才能稳定、及时地被阅读器抓取。