RSS feed 返回 404:3 个原因 + 修复路径

/rss.xml 或 /feed.xml 404——通常是端点没生成、文件名不对或宿主拦截。

你在博客模板里看到 <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 标签,没有任何生成端点的文件,就是这种情况。

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/xmlapplication/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/xmlapplication/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 改成跳过:

平台修复
Vercelvercel.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,顺序错就被吞

相关阅读

标签: #部署 / 托管 #排查 #排查