你在博客模板里看到 <link rel="alternate" type="application/rss+xml" href="/rss.xml">,订阅者一点 404——这是 RSS 上线后第二常见的问题(最常见是订阅者反馈”没收到新文章”)。绝大多数原因不是 RSS 库有问题,而是路由文件根本没在 build 里跑出来、文件名和 link 标签不一致、或者部署平台的 rewrite 把 /rss.xml 当成”未匹配路径”打回了 SPA fallback。
本文针对 Astro / Next.js / Nuxt / Hugo 这几个最常见栈,给出能跑通的修复路径。
常见原因
按命中率从高到低:
1. 根本没生成 RSS 端点
很多模板的 <link rel="alternate"> 是默认硬编码的,但实际上没人加 RSS 集成。Astro 需要 @astrojs/rss + src/pages/rss.xml.ts;Next.js App Router 需要 app/rss.xml/route.ts;Hugo 是 config.toml 里启用 output formats。
如何判断:
find . -path ./node_modules -prune -o -type f \( -name "rss*" -o -name "feed*" \) -print
如果只有模板里的 link 标签,没有任何生成端点的文件,就是这种情况。
2. 文件名 / 路径与 link 标签不一致
HTML 里写的是 /rss.xml,但你建的是 src/pages/feed.xml.ts,或者放在了 src/pages/blog/rss.xml.ts——浏览器请求 /rss.xml 直接 404。
如何判断:从 HTML 源码里复制那个 href,再到 dist/ 里找对应文件:
ls dist/rss.xml dist/feed.xml dist/blog/rss.xml 2>/dev/null
任何对不上的就是这种问题。
3. 宿主 rewrite / SPA fallback 拦截
Vercel vercel.json 里如果有 "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }] 这种 SPA fallback,会把 /rss.xml 也吞掉(因为静态文件已经被前一步的检查放过、但如果 build 没产出 rss.xml,fallback 会接管)。Netlify _redirects 里的 /* /index.html 200 同理。
如何判断:
curl -I https://yourdomain.com/rss.xml
如果返回 200 但 content-type: text/html,几乎一定是 SPA fallback。
4. SSR 模式下没注册路由
如果你的 Astro / Next 项目是 SSR / hybrid,端点文件必须在 output: 'server' 的运行时也被识别。Astro 中需要 export const prerender = true; 让 RSS 在 build 时就出静态文件,否则部分边缘 runtime(Vercel Edge、Cloudflare Workers)不会执行复杂的 XML 序列化。
如何判断:本地 npm run preview 正常,但生产 404 / 500 → 通常是 SSR runtime 问题。
5. Cloudflare Pages / Workers 把 .xml 视为静态走 cache miss
Cloudflare Pages 默认所有静态文件直接 edge serve,但如果在 Functions 模式下,functions/rss.xml.js 的导出函数返回值缺少 content-type 头,会被浏览器和阅读器拒绝解析。
如何判断:curl -I 看到 200 但没有 content-type: application/xml 或 application/rss+xml → 阅读器会报”feed format invalid”。
最短修复路径
按收益从高到低,前 3 步通常就能解决 80% 的问题。
Step 1:确认路由文件确实存在并能生成 feed
按框架选模板。Astro:
// src/pages/rss.xml.ts
import rss from '@astrojs/rss';
import { getCollection } from 'astro:content';
export const prerender = true;
export async function GET(context) {
const posts = await getCollection('articles');
return rss({
title: 'Your Site',
description: 'Latest articles',
site: context.site,
items: posts.map((p) => ({
title: p.data.title,
pubDate: p.data.publishedAt,
description: p.data.description,
link: `/articles/${p.slug}/`,
})),
});
}
Next.js App Router:
// app/rss.xml/route.ts
export const dynamic = 'force-static';
export async function GET() {
const items = await fetchPosts();
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"><channel>
<title>Your Site</title>
<link>https://yourdomain.com</link>
${items.map(i => `<item><title>${i.title}</title><link>${i.url}</link></item>`).join('')}
</channel></rss>`;
return new Response(xml, {
headers: { 'content-type': 'application/xml; charset=utf-8' },
});
}
Step 2:本地构建 + preview 验证
npm run build
npm run preview
curl -I http://localhost:4321/rss.xml
curl -s http://localhost:4321/rss.xml | head -5
通过标准:
- 状态 200
content-type: application/xml或application/rss+xml- 内容以
<?xml version="1.0"开头
preview 通过、生产不通过 → 部署或 rewrite 问题,走 Step 3。preview 也不通过 → 路由文件本身问题,回 Step 1 检查。
Step 3:清掉拦截 /rss.xml 的 rewrite
打开 vercel.json / netlify.toml / _redirects,把任何能匹配到 /rss.xml 的 catch-all 改成跳过:
| 平台 | 修复 |
|---|---|
| Vercel | vercel.json rewrites 加 { "source": "/rss.xml", "destination": "/rss.xml" } 放在 catch-all 之前 |
| Netlify | _redirects 加 /rss.xml /rss.xml 200! 放在 SPA fallback 之前 |
| Cloudflare Pages | _redirects 同上,或确认 functions/ 目录里没冲突的 rss.xml.js |
重部署后:
curl -I "https://yourdomain.com/rss.xml?cb=$(date +%s)"
必须返回 200 + xml content-type。
Step 4:把 RSS 验证写进 CI
#!/usr/bin/env bash
set -e
URL="https://yourdomain.com/rss.xml"
ct=$(curl -sI "$URL" | grep -i "^content-type:" | tr -d '\r')
if [[ ! "$ct" =~ xml ]]; then echo "BAD content-type: $ct"; exit 1; fi
curl -s "$URL" | head -1 | grep -q "<?xml" || { echo "Body is not XML"; exit 1; }
echo "RSS OK"
挂在 GitHub Actions 的 deployment_status 上。再用 W3C Feed Validator 跑一次确保阅读器能解析。
预防建议
- HTML 模板里的
<link rel="alternate">href 和实际路由文件名保持唯一来源 - 部署后 curl 一次
/rss.xml,断言 200 + xml content-type - 用 W3C Feed Validator 跑一次,避免阅读器解析失败
- SSR 项目里给 RSS 路由显式
prerender = true,避免边缘 runtime 兼容性问题 - 别在 SPA fallback 之后才声明
/rss.xml,顺序错就被吞