๐ค Ghostwritten by Claude ยท Curated by Tom Hundley
This article was written by Claude and curated for publication by Tom Hundley.
By Tom Hundley, Elegant Software Solutions โ debugged in collaboration with Claude Code (Opus 4.5)
We had a problem: our dynamic blog sitemap at /sitemap-blog-1.xml was returning 404. Google could not fetch it. Our blog posts were not getting indexed.
The route existed. The code was correct. But Next.js 15 refused to match it.
We built a paginated sitemap system for our blog. With 50+ posts, we needed dynamic sitemaps that could scale:
src/app/sitemap-blog-[id].xml/route.tsThis should generate:
/sitemap-blog-1.xml/sitemap-blog-2.xmlThe main sitemap.xml index referenced these URLs. Standard stuff.
But every request to /sitemap-blog-1.xml returned 404.
The investigation started with a simple health check using Google Search Console API:
# The symptom
curl -sI https://www.elegantsoftwaresolutions.com/sitemap-blog-1.xml
# HTTP/2 404
# x-matched-path: /_not-foundThat x-matched-path: /_not-found header was the clue. Next.js was not matching our route at all.
We verified:
.next/app-path-routes-manifest.jsonNext.js 15 App Router has a limitation (or bug) where dynamic segments [param] inside folder names containing file extensions do not get captured properly.
The folder structure:
src/app/sitemap-blog-[id].xml/route.ts
^ dynamic segment
^ file extensionNext.js generates a regex pattern to match routes. When you combine [id] with .xml in the same folder name, the regex breaks. The dynamic parameter is never extracted.
This is a known edge case that is not documented.
Move from a folder name with extension:
# Before (broken)
src/app/sitemap-blog-[id].xml/route.ts
# After (works)
src/app/sitemap-blog/[id]/route.tsThe clean structure sitemap-blog/[id] works because there is no file extension confusing the router.
We still want the SEO-friendly .xml URLs. Add a rewrite in next.config.ts:
async rewrites() {
return [
{
// Map /sitemap-blog-1.xml to /sitemap-blog/1
source: /sitemap-blog-:id.xml,
destination: /sitemap-blog/:id,
},
];
}This intercepts the .xml request and routes it internally to the clean path.
Before the fix:
curl -sI https://example.com/sitemap-blog-1.xml
# HTTP/2 404
# x-matched-path: /_not-foundAfter the fix:
curl -sI https://example.com/sitemap-blog-1.xml
# HTTP/2 200
# content-type: application/xml; charset=utf-8Google Search Console successfully fetched the sitemap within minutes of deployment.
Avoid dynamic segments in folder names with extensions โ Next.js 15 App Router does not handle this well
Use rewrites to preserve URL structure โ You can have SEO-friendly URLs while using clean internal routing
Check the x-matched-path header โ It tells you exactly what Next.js matched (or did not)
Test sitemaps before relying on them โ A 404 sitemap silently breaks your SEO
AI-assisted debugging works โ Claude Code traced this issue through logs, headers, and route manifests faster than manual debugging
If you need dynamic routes with file extensions in Next.js 15:
// next.config.ts
async rewrites() {
return [
{
source: /your-route-:param.xml,
destination: /your-route/:param,
},
];
}// Folder structure
src/app/your-route/[param]/route.tsThis pattern maintains external URL compatibility while working around the routing limitation.
This bug cost us indexing time. But finding it with AI assistance took minutes instead of hours. The combination of systematic debugging and Claude Code ability to trace through manifests, headers, and code simultaneously made this a 30-minute fix instead of a half-day investigation.
That is the vibe coding advantage: AI does not just write code โ it debugs alongside you.
This bug fix was discovered and implemented during a site health check using Google Search Console MCP tools and Claude Code. The fix was deployed to Vercel and verified in production on December 14, 2025.
Discover more content: