Skip to content
Consultation

Two Commits I Reverted: generateSitemaps vs Next 16

Adityo Guni Waluyo

Next 16 rejects a sitemap.xml route folder that collides with the metadata convention. I reverted two commits; the old /sitemaps/ rewrite setup was enough.

The build failed because of a folder name

The commit looked good that night: sitemap split into 200-URL child files with generateSitemaps(), plus a hand-written app/sitemap.xml/route.ts to serve a sitemap index at the root. Two hours later I reverted both. The Next 16 build rejects a route folder named sitemap.xml because it collides with the sitemap.(xml|js|ts) metadata file convention, and once I caught my breath I noticed something more embarrassing: the older system in this repo already did all of it. Through a rewrite.

Context: this blog moved to Vercel two days earlier, and a sitemap system had been living at /sitemaps/ all along — /sitemaps/index.xml as the sitemap index, children at /sitemaps/pages.xml and /sitemaps/articles-1.xml, each capped at 200 URLs. The root /sitemap.xml was never a route at all, just a rewrite in next.config.ts. Working, indexed, robots.txt already pointing at it. Then I built a parallel version with the built-in metadata API and forgot to retire the old one.

The collision is not new

What calmed me down: this class of collision is documented. Next.js treats sitemap.ts as a metadata convention file that produces /sitemap.xml, and a route folder named exactly sitemap.xml gets caught by a special case. In vercel/next.js#94041, a route handler inside a sitemap.xml folder was never registered in the app output map, and requests fell through to the dynamic route next to it. The reporter found that renaming the folder fixed the build immediately. A sibling case exists in vercel/next.js#78609: sitemap.ts conflicting with a page at /sitemap under Turbopack, only cleaned up in 16.2.0. This repo runs 16.2.12, so I am not alone, and the ground is still moving.

Protocol-wise, the old setup was already correct too. The sitemaps protocol only asks that every file live on the same host as its index, and Google’s guidance adds: sitemaps referenced by an index must sit in the same directory or deeper. /sitemap.xml (the rewrite) points at /sitemaps/*.xml. Rules satisfied, one URL to submit to Search Console.

The revert, and what it looks like now

So I took the two commits apart: the generateSitemaps() implementation went back to a plain sitemap.ts that just lists static routes, and app/sitemap.xml/route.ts got deleted outright. What remains in production is tiny:

// next.config.ts — /sitemap.xml itu rewrite, bukan route
async rewrites() {
  return [
    {
      source: "/sitemap.xml",
      destination: "/sitemaps/index.xml",
    },
  ];
}
// app/sitemaps/index.xml/route.ts — index beneran di sini
export const revalidate = 120;

export async function GET() {
  const siteUrl = await getSiteUrl();
  const chunks = chunkEntries(
    articleEntries(siteUrl, await fetchAllArticles()),
  );

  return xmlResponse(
    renderSitemapIndex([
      {
        loc: `${siteUrl}/sitemaps/pages.xml`,
        lastModified: newestOf(staticPageEntries(siteUrl)),
      },
      ...chunks.map((chunk, i) => ({
        loc: `${siteUrl}/sitemaps/articles-${i + 1}.xml`,
        lastModified: newestOf(chunk),
      })),
    ]),
  );
}

How I check that everything still behaves: curl the root index, confirm it lists /sitemaps/pages.xml and /sitemaps/articles-1.xml, then open each child and count the <loc> entries against the number of published articles in the API. If a child comes up short, an article is leaking out of the crawl. I read robots.txt in the same pass: a single Sitemap: line pointing at the index, not at child files. Three commands, thirty seconds, and unlike waiting on Google Search Console’s weekly report, it surfaces drift almost immediately.

The lesson I took home is not really about Next. It’s about checking what already exists before writing a new version of it. I wrote up the full story of why this sitemap got split into 200-URL files in an earlier article, including the drama of /sitemap.xml 404-ing because Next emits no index for generateSitemaps(). Had I reread my own post before committing that night, two hours of work could have been five minutes. The grand generateSitemaps plan stays shelved, at least until issue #94041 closes and the behavior of a sitemap.xml folder settles.

Related articles