If your sitemap address returns the post-not-found page, no route exists for it and the sibling dynamic segment is matching the whole thing as a slug. The repair is a real route at that exact path, because a static segment outranks a dynamic one.
If a request for /blog/sitemap.xml comes back as your post-not-found page, there is no route serving that address and the sibling dynamic segment is matching it: the router hands sitemap.xml to the post page as a slug value, the page looks for a post with that slug, finds none, and returns a 404. The fix is a real route at that exact path. A literal segment outranks a dynamic one in the router's ordering, so adding the file is enough — nothing has to be special-cased inside the dynamic page.
How the router decides which route answers
The Next.js router scores each path segment for specificity and matches the most specific route first. A static segment scores zero, a single dynamic segment scores one, a catch-all scores two and an optional catch-all scores three, and lower wins. The server also checks the exact, non-dynamic route definitions before it considers any dynamic ones at all, so a literal sitemap.xml route is not merely preferred over a sibling slug route, it is tested first.
Segment forms in the Next.js App Router, ordered by how specific they are.
A sitemap for a blog usually lives under the same path prefix as the posts, which is exactly the position a dynamic segment covers. Anything served beneath /blog that is not a post is competing with the post route for the same shape of URL: sitemap.xml, an RSS feed, a JSON feed, a tag index, a page-two listing. The post route was written to match one segment and it does, faithfully, including the segments that were never meant to be posts.
What makes it easy to miss is that nothing errors. The dynamic page behaved correctly at every step — it received a slug, queried for it, found nothing and returned the not-found response the framework provides. From the outside it is a sitemap address returning 404, which is a Search Console error days later rather than a stack trace during development.
The submit call is a PUT that returns an empty body. Whether Google fetched the file, whether it parsed and how many URLs it holds all come back from a different call, on Google's schedule rather than yours.
Market4 turns one release note into a changelog page, a blog post, a mail-out and a week of social posts — and then tells you which of them brought anyone back.
No card to start. Cancel from the settings screen, not from an email.
Branching inside the dynamic page — checking whether the slug is sitemap.xml and returning XML instead — works and is the wrong shape. A page component in the App Router is rendered as HTML inside a layout, so returning a document with a different content type from it means working against the framework's conventions rather than with them. A route handler at the literal path is the supported way to serve a non-HTML document, and it lets the post page keep the single job it was written for.
There is also a metadata convention for sitemaps, where a sitemap file exports a list of URLs and the framework generates the XML. Choosing a literal route segment over the convention is reasonable when the XML is already built somewhere else, because the convention would mean maintaining a second generator over the same list of posts. The choice between them is about where the list of URLs lives, not about which one the router prefers.
How to catch the next one before Google does
The class of bug here is a URL that exists in your head and not in the route tree, and it can be tested for directly. A few checks catch most of it:
Fetch every non-post address under the blog prefix in a test and assert the status code and the content type, not the rendered output — a 404 and an HTML content type on a file that should be XML are both failures.
Walk the route tree and fail on any page nobody has decided about, so a new sibling of a dynamic segment cannot be added silently.
Ask Search Console what it fetched rather than what you submitted; a sitemap it could not read is reported there and nowhere else.
Check the response with a plain HTTP client, because a browser showing an error page and a browser showing a rendered 404 look similar enough to skim past.
The second of those is the one that generalises. A test that walks the route tree turns every future page into a decision somebody has to make, which is what stops the same collision happening the next time a feed or an index is added under a prefix that already has a dynamic segment in it.
Why does my sitemap.xml return the 404 page in Next.js?
Because no route serves that address and a sibling dynamic segment is matching it. A route such as /blog/[slug] matches any single segment under /blog, so a request for /blog/sitemap.xml arrives at the post page with sitemap.xml as the slug, no post is found, and the not-found response is returned. Adding a real route at /blog/sitemap.xml fixes it, because static segments are matched before dynamic ones.
Does a static route always win over a dynamic route in Next.js?
Yes. Segments are scored for specificity — static zero, dynamic one, catch-all two, optional catch-all three — and the lower score is matched first, with exact non-dynamic definitions checked before any dynamic definition is considered. That means adding a literal folder next to a dynamic one is enough to claim the path, and no conflict error is raised, because there is no ambiguity to report.
Should the sitemap be a route handler or the sitemap metadata convention?
Either works, and the deciding question is where the list of URLs already lives. The metadata convention is the shorter path when the framework app is where that list lives. A route handler at a literal path is the better fit when the XML is generated elsewhere — by an API, from a database — because the convention would otherwise mean writing a second generator over the same list and keeping the two in step.
How do you test that a sitemap URL is not being swallowed?
Request it with a plain HTTP client and assert two things: the status code is 200 and the content type is XML. A browser is a poor instrument here because a rendered not-found page and an unstyled XML document both look like something happened. Adding that assertion to the test suite means a future sibling route, or a rename, fails a test rather than quietly costing you crawl coverage.
Publishing a post sends two independent announcements. One IndexNow request reaches Bing, Yandex, Seznam, Naver and Yep. Google takes no part in IndexNow, so it is told by re-submitting the sitemap.
The bandwidth you save by blocking an AI crawler shows up in a log. The citations you lose do not show up anywhere, because nothing reports an answer that did not mention you.