RSS Feed Returns 404 — /rss.xml Missing Fix

/rss.xml or /feed.xml 404 — endpoint missing, wrong filename, or host caching.

Your blog template ships <link rel="alternate" type="application/rss+xml" href="/rss.xml">, a subscriber clicks it, and the URL 404s. This is the second-most-common RSS bug (the first is “subscribers report they aren’t getting new posts”). Almost never is the RSS library at fault — usually the route file isn’t actually in the build, the filename doesn’t match the link tag, or the host’s rewrite rules treat /rss.xml as an unmatched path and route it through the SPA fallback.

This article covers Astro / Next.js / Nuxt / Hugo specifically, with working fixes per stack.

Common causes

Ordered by hit rate, highest first.

1. No RSS endpoint actually exists

Many templates hardcode the <link rel="alternate"> tag but no one wires up the integration. Astro needs @astrojs/rss + src/pages/rss.xml.ts; Next.js App Router needs app/rss.xml/route.ts; Hugo needs output formats configured in config.toml.

How to spot it:

find . -path ./node_modules -prune -o -type f \( -name "rss*" -o -name "feed*" \) -print

If you only find the link tag in templates and no generating endpoint, that’s it.

The HTML says /rss.xml but you created src/pages/feed.xml.ts, or stashed it at src/pages/blog/rss.xml.ts. A browser hitting /rss.xml gets a clean 404.

How to spot it: Copy the href from your HTML source, then look for the matching file in dist/:

ls dist/rss.xml dist/feed.xml dist/blog/rss.xml 2>/dev/null

Anything that doesn’t line up is the bug.

3. Host rewrite / SPA fallback intercepting

A Vercel vercel.json with "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }] will swallow /rss.xml if the build didn’t actually produce that file (static files are checked first, but a missing one falls through to the rewrite). Netlify _redirects with /* /index.html 200 does the same.

How to spot it:

curl -I https://yourdomain.com/rss.xml

Status 200 with content-type: text/html is almost certainly the SPA fallback.

4. SSR route not registered at runtime

If your Astro / Next project runs in SSR / hybrid mode, the RSS endpoint must be picked up by the runtime, not just the build. In Astro, add export const prerender = true; so the feed is emitted as a static file at build time — some edge runtimes (Vercel Edge, Cloudflare Workers) won’t execute the heavier XML serialization paths.

How to spot it: Local npm run preview works, production 404s or 500s → usually an SSR runtime mismatch.

5. Cloudflare Pages / Workers serving with no content-type

In Cloudflare Pages Functions mode, a functions/rss.xml.js that forgets to set the content-type header will return 200 with the right bytes — but readers reject it as “feed format invalid.”

How to spot it: curl -I shows 200 but no content-type: application/xml or application/rss+xml. The feed loads in a browser but fails in any RSS client.

Shortest path to fix

Ordered by ROI. The first three usually solve 80% of cases.

Step 1: Make sure the route file exists and emits a valid feed

Per framework. 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: Build + preview locally and verify

npm run build
npm run preview
curl -I http://localhost:4321/rss.xml
curl -s http://localhost:4321/rss.xml | head -5

Pass criteria:

  • Status 200
  • content-type: application/xml or application/rss+xml
  • Body begins with <?xml version="1.0"

Preview passes but production fails → deploy or rewrite issue (Step 3). Preview also fails → the route file itself is wrong, back to Step 1.

Step 3: Remove any rewrite intercepting /rss.xml

Open vercel.json / netlify.toml / _redirects and explicitly allow /rss.xml before any catch-all:

PlatformFix
VercelIn vercel.json rewrites, add { "source": "/rss.xml", "destination": "/rss.xml" } before the catch-all
NetlifyIn _redirects, put /rss.xml /rss.xml 200! before the SPA fallback line
Cloudflare PagesSame as Netlify, or confirm there’s no conflicting functions/rss.xml.js

After redeploy:

curl -I "https://yourdomain.com/rss.xml?cb=$(date +%s)"

Must return 200 + xml content-type.

Step 4: Wire RSS validation into 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"

Hook it into GitHub Actions’ deployment_status event. Then run W3C Feed Validator once to confirm readers can parse it.

Prevention

  • Keep a single source of truth: the <link rel="alternate"> href and the actual route filename must always match
  • Post-deploy, curl /rss.xml and assert 200 + xml content-type
  • Run W3C Feed Validator once before announcing the feed
  • In SSR projects, explicitly mark the RSS route prerender = true to dodge edge-runtime quirks
  • Never put /rss.xml behind a SPA fallback — declare it (or pin its rewrite) above the catch-all

Tags: #Hosting #Debug #Troubleshooting