Describe the bug
The RSS feed route added in #18146 returns an infinite redirect loop on every locale and trailing-slash variant. Visiting /latest/feed.xml ping-pongs between Netlify's edge and Next.js middleware until the browser aborts with ERR_TOO_MANY_REDIRECTS.
To reproduce
- Open
https://dev.ethereum.org/latest/feed.xml in any browser
- Observe "redirected you too many times" error
Or via curl:
curl -sI -L --max-redirs 10 https://dev.ethereum.org/latest/feed.xml
The same loop occurs on /en/latest/feed.xml, /latest/feed.xml/, and /<other-locale>/latest/feed.xml.
Expected behavior
The URL should serve the RSS feed XML with Content-Type: application/rss+xml on the first or second hop.
Root cause
trailingSlash: true in next.config.js combined with an extension-bearing route path (feed.xml) puts three layers in conflict:
- Netlify's
@netlify/plugin-nextjs-generated redirect rule adds a trailing slash to extension paths: /en/latest/feed.xml -> 301 -> /latest/feed.xml/
next-intl middleware internally rewrites the unprefixed path to add the default locale: x-middleware-rewrite: /en/latest/feed.xml/
- Next.js's framework-level trailing-slash logic has an extension-exception and strips the trailing slash from the rewritten path, emitting
308 Location: /en/latest/feed.xml (no slash)
- Back to step 1. Forever.
Trace (relevant headers from a fresh curl against dev.ethereum.org):
GET /latest/feed.xml -> 308 location: /en/latest/feed.xml (middleware)
x-middleware-rewrite: /en/latest/feed.xml/
GET /en/latest/feed.xml -> 301 location: /latest/feed.xml/ (Netlify edge)
GET /latest/feed.xml/ -> 308 location: /en/latest/feed.xml (middleware again)
(loop)
Recommended fix
Rename the route directory so the URL no longer has a file extension:
app/[locale]/latest/feed.xml/route.ts -> app/[locale]/latest/feed/route.ts
Resulting URL: /latest/feed/ (trailing-slash, no extension). Netlify and Next.js agree on this canonical form, so the loop is impossible.
Touch list:
- Directory rename in App Router
- Drop the
.replace(/\/$/, "") slash-stripping hack on feedUrl inside the route handler -- getFullUrl now produces the correct shape directly
- Update the metadata alternate in
app/[locale]/latest/page.tsx to the new URL
- Remove the second matcher in
proxy.ts ("/:locale?/latest/feed.xml") -- it becomes dead code once the URL has no extension, since the first matcher already includes non-extension paths
- Add a permanent redirect in
next.config.js from /latest/feed.xml -> /latest/feed/ to preserve any external links to the old URL
Tradeoff: losing the .xml extension
feed.xml is the established convention for RSS endpoints across the codebase's referenced external feeds (Vitalik, Solidity, ethpandaops, the old blog.ethereum.org/en/feed.xml). Renaming to /feed/ is a visible departure from that convention.
In practice this doesn't break anything:
- Feed readers route by
Content-Type: application/rss+xml, not URL suffix
- HTML feed discovery uses
<link rel="alternate" type="application/rss+xml" href="...">, not extension
- Major platforms split both ways: Substack uses
/feed, Hashnode uses /rss.xml, Medium uses /feed
The optional permanent redirect from /latest/feed.xml -> /latest/feed/ covers any external links that were already shared with the old URL.
Alternatives considered
These were investigated and considered less desirable than the rename:
- Skip middleware for
.xml paths via the proxy.ts matcher. Doesn't fix the loop on its own -- Netlify's edge redirect (step 1) is the trailing-slash adder; middleware exclusion alone leaves the same cycle, just half-decoupled.
- Custom
netlify.toml redirects to override the plugin's auto-redirect. Brittle: the plugin regenerates redirect rules on every build, ordering matters, and the override would need maintenance every time @netlify/plugin-nextjs changes its output.
- Custom
next.config.js rewrite/redirect for /latest/feed.xml. Runs after the Netlify edge redirect in the request lifecycle, so the loop still triggers before the rewrite gets a chance.
- Disable
trailingSlash: true site-wide. Site-wide regression across thousands of URLs; not justified by this one route.
- Pre-render
feed.xml as a static asset in public/ via a build script. Loses the App Router route pattern, requires per-locale generation logic outside getBlogPostsData, and creates a second source of truth.
Acceptance criteria
curl -sI https://dev.ethereum.org/latest/feed/ returns 200 OK with Content-Type: application/rss+xml; charset=utf-8 on the first hop (or one clean redirect from the locale-prefixed variant)
curl -sI https://dev.ethereum.org/en/latest/feed/ returns one 301 to /latest/feed/, no loop
curl -sI https://dev.ethereum.org/es/latest/feed/ returns the Spanish-locale feed directly with 200 OK
- Feed validates as RSS 2.0 in a feed validator (e.g. W3C Feed Validation Service)
- HTML page at
/latest/ still includes <link rel="alternate" type="application/rss+xml" href="..."> pointing at the new URL
- If the optional redirect is added:
curl -sI https://dev.ethereum.org/latest/feed.xml returns one 301 to /latest/feed/
Out of scope
- Migrating to a different feed format (Atom, JSON Feed) -- keep RSS 2.0
- Adding
<media:content> or <enclosure> for post images -- separate follow-up
- Changing trailing-slash behavior site-wide
Want to contribute?
Describe the bug
The RSS feed route added in #18146 returns an infinite redirect loop on every locale and trailing-slash variant. Visiting
/latest/feed.xmlping-pongs between Netlify's edge and Next.js middleware until the browser aborts withERR_TOO_MANY_REDIRECTS.To reproduce
https://dev.ethereum.org/latest/feed.xmlin any browserOr via curl:
The same loop occurs on
/en/latest/feed.xml,/latest/feed.xml/, and/<other-locale>/latest/feed.xml.Expected behavior
The URL should serve the RSS feed XML with
Content-Type: application/rss+xmlon the first or second hop.Root cause
trailingSlash: trueinnext.config.jscombined with an extension-bearing route path (feed.xml) puts three layers in conflict:@netlify/plugin-nextjs-generated redirect rule adds a trailing slash to extension paths:/en/latest/feed.xml->301->/latest/feed.xml/next-intlmiddleware internally rewrites the unprefixed path to add the default locale:x-middleware-rewrite: /en/latest/feed.xml/308 Location: /en/latest/feed.xml(no slash)Trace (relevant headers from a fresh curl against
dev.ethereum.org):Recommended fix
Rename the route directory so the URL no longer has a file extension:
Resulting URL:
/latest/feed/(trailing-slash, no extension). Netlify and Next.js agree on this canonical form, so the loop is impossible.Touch list:
.replace(/\/$/, "")slash-stripping hack onfeedUrlinside the route handler --getFullUrlnow produces the correct shape directlyapp/[locale]/latest/page.tsxto the new URLproxy.ts("/:locale?/latest/feed.xml") -- it becomes dead code once the URL has no extension, since the first matcher already includes non-extension pathsnext.config.jsfrom/latest/feed.xml->/latest/feed/to preserve any external links to the old URLTradeoff: losing the
.xmlextensionfeed.xmlis the established convention for RSS endpoints across the codebase's referenced external feeds (Vitalik, Solidity, ethpandaops, the oldblog.ethereum.org/en/feed.xml). Renaming to/feed/is a visible departure from that convention.In practice this doesn't break anything:
Content-Type: application/rss+xml, not URL suffix<link rel="alternate" type="application/rss+xml" href="...">, not extension/feed, Hashnode uses/rss.xml, Medium uses/feedThe optional permanent redirect from
/latest/feed.xml->/latest/feed/covers any external links that were already shared with the old URL.Alternatives considered
These were investigated and considered less desirable than the rename:
.xmlpaths via theproxy.tsmatcher. Doesn't fix the loop on its own -- Netlify's edge redirect (step 1) is the trailing-slash adder; middleware exclusion alone leaves the same cycle, just half-decoupled.netlify.tomlredirects to override the plugin's auto-redirect. Brittle: the plugin regenerates redirect rules on every build, ordering matters, and the override would need maintenance every time@netlify/plugin-nextjschanges its output.next.config.jsrewrite/redirect for/latest/feed.xml. Runs after the Netlify edge redirect in the request lifecycle, so the loop still triggers before the rewrite gets a chance.trailingSlash: truesite-wide. Site-wide regression across thousands of URLs; not justified by this one route.feed.xmlas a static asset inpublic/via a build script. Loses the App Router route pattern, requires per-locale generation logic outsidegetBlogPostsData, and creates a second source of truth.Acceptance criteria
curl -sI https://dev.ethereum.org/latest/feed/returns200 OKwithContent-Type: application/rss+xml; charset=utf-8on the first hop (or one clean redirect from the locale-prefixed variant)curl -sI https://dev.ethereum.org/en/latest/feed/returns one301to/latest/feed/, no loopcurl -sI https://dev.ethereum.org/es/latest/feed/returns the Spanish-locale feed directly with200 OK/latest/still includes<link rel="alternate" type="application/rss+xml" href="...">pointing at the new URLcurl -sI https://dev.ethereum.org/latest/feed.xmlreturns one301to/latest/feed/Out of scope
<media:content>or<enclosure>for post images -- separate follow-upWant to contribute?