Direct Answer: Next.js App Router (sitemap.ts / Route Handlers) Sitemap Architecture & Failure Modes
Next.js App Router (Next.js 13.3+, Next.js 14, and Next.js 15+) provides a built-in file-based convention for programmatic sitemaps via `app/sitemap.ts`. Exporting a default function returning `MetadataRoute.Sitemap` automatically builds a compliant XML sitemap with valid `application/xml` MIME headers, incremental static regeneration (ISR), and runtime edge caching. Common Google Search Console validation failures occur when developers author custom Route Handlers (`app/sitemap.xml/route.ts`) that return `text/html` instead of `application/xml`, when unescaped query string ampersands break XML parsers, or when dynamic catalogs exceed 50,000 URLs without using Next.js `generateSitemaps()` for automated sitemap index chunking.
Generating Dynamic, Edge-Cached sitemap.ts in Next.js App Router
Technical deep dive into Next.js App Router (sitemap.ts / Route Handlers) sitemap quirks, sub-sitemap splitting, and crawl waste elimination
Modern Next.js applications require programmatic sitemap generation to handle dynamic database routes, multi-locale subpaths, and headless CMS webhooks without manual file maintenance.
1. The Native app/sitemap.ts Convention
Instead of manually creating XML strings, Next.js provides the MetadataRoute.Sitemap TypeScript interface in app/sitemap.ts (or src/app/sitemap.ts):
// app/sitemap.ts (Next.js App Router)
import { MetadataRoute } from 'next';
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await fetch('https://api.example.com/posts').then(res => res.json());
return posts.map((post: any) => ({
url: `https://example.com/blog/${post.slug}`,
lastModified: new Date(post.updatedAt),
changeFrequency: 'weekly',
priority: 0.8,
}));
}
2. Scaling Past 50,000 URLs with generateSitemaps()
If your application has more than 50,000 dynamic URLs (e.g. large programmatic SEO directories or e-commerce stores), Next.js App Router supports automated sitemap index partitioning using the generateSitemaps() export:
// app/sitemap.ts (Automated Sitemap Index Partitioning)
import { MetadataRoute } from 'next';
export async function generateSitemaps() {
// Return array of sitemap IDs based on database count
return [{ id: 0 }, { id: 1 }, { id: 2 }];
}
export default async function sitemap({ id }: { id: number }): Promise<MetadataRoute.Sitemap> {
const limit = 50000;
const start = id * limit;
const items = await getItemsFromDb(start, limit);
return items.map((item) => ({
url: `https://example.com/product/${item.slug}`,
lastModified: new Date(item.updatedAt),
}));
}
Next.js automatically exposes a parent sitemap index at /sitemap.xml referencing /sitemap/0.xml, /sitemap/1.xml, and /sitemap/2.xml.
3. Edge Caching & Revalidation
To prevent heavy database queries on every crawler hit, export export const revalidate = 86400; (24 hours) at the top of app/sitemap.ts. Next.js will cache the compiled XML response at the Vercel/Cloudflare edge, serving it instantly with zero server execution delay.
// app/sitemap.ts (Next.js App Router with Dynamic generateSitemaps Partitioning)
import { MetadataRoute } from 'next';
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://yourdomain.com';
export const revalidate = 86400; // Cache XML at the edge for 24 hours
export async function generateSitemaps() {
// Divide total catalog into chunks of 50,000 URLs
return [{ id: 0 }, { id: 1 }];
}
export default async function sitemap({
id,
}: {
id: number;
}): Promise<MetadataRoute.Sitemap> {
const limit = 50000;
const start = id * limit;
const dynamicPosts = await fetchPostsFromDatabase(start, limit);
const entries = dynamicPosts.map((post) => ({
url: `${BASE_URL}/blog/${post.slug}`,
lastModified: new Date(post.updatedAt),
changeFrequency: 'weekly' as const,
priority: 0.8,
}));
if (id === 0) {
return [
{
url: BASE_URL,
lastModified: new Date(),
changeFrequency: 'daily',
priority: 1.0,
},
...entries,
];
}
return entries;
}How to Configure & Validate Next.js Sitemaps (Step-by-Step)
Five proven engineering steps to deploy error-free XML sitemaps in Next.js App Router (sitemap.ts / Route Handlers)
- 1
Create app/sitemap.ts in App Router
Add a new file at `src/app/sitemap.ts` or `app/sitemap.ts` in your Next.js project.
- 2
Import MetadataRoute and Type Return
Import `import { MetadataRoute } from 'next';` and ensure your default async export returns `Promise<MetadataRoute.Sitemap>`.
- 3
Fetch Dynamic Slugs from Database or CMS
Fetch your published posts, products, or programmatic items, formatting ISO `Date` instances for `lastModified`.
- 4
Configure generateSitemaps() for >50k Datasets
If your catalog exceeds 50,000 entries, export `generateSitemaps()` alongside `sitemap({ id })` to generate automated sub-sitemaps.
- 5
Set Edge Revalidation and Validate in OmniSEO
Export `export const revalidate = 86400;` to enable edge caching, then paste your live XML endpoint into our validator to confirm compliant serialization.
Next.js XML Sitemap FAQ
Troubleshooting Google Search Console validation errors, 50k limits, and platform configuration
How do I handle more than 50,000 URLs in Next.js App Router sitemap.ts?
In Next.js App Router, export an asynchronous `generateSitemaps()` function in `app/sitemap.ts` that returns an array of sitemap ID objects (e.g., `return [{ id: 0 }, { id: 1 }];`). Then update your default sitemap function to accept `{ id }: { id: number }` as props to paginate data fetching. Next.js will automatically build a sitemap index at `/sitemap.xml` pointing to `/sitemap/0.xml` and `/sitemap/1.xml`.
Why does my custom Next.js Route Handler sitemap fail Google Search Console validation?
If building a custom Route Handler (`app/sitemap.xml/route.ts`), you must return a `Response` object with the header `'Content-Type': 'application/xml; charset=utf-8'`. Returning plain text or `text/html` causes Google Search Console to reject the sitemap with a MIME type mismatch error.
How do I set dynamic cache headers or ISR revalidation on Next.js sitemaps?
Export `export const revalidate = 86400;` (time in seconds) or `export const dynamic = 'force-dynamic'` directly inside `app/sitemap.ts`. This controls how Vercel and Next.js cache and revalidate your XML sitemap at the network edge.
CMS & Framework XML Sitemap Generators
Switch presets to inspect, debug, and generate XML sitemaps for your specific tech stack
Inspect, debug, and generate clean Shopify XML sitemaps. Audit locked sitemap_products_1.xml files, filter out noindexed tags, and resolve sub-collection indexation bloat.
Audit WordPress sitemap index files. Resolve conflicts between native wp-sitemap.xml, Yoast SEO, and RankMath, and validate XML namespace headers.
Validate dynamic Next.js App Router sitemap.ts files. Debug static generation limits, dynamic slug pagination, and edge runtime XML serialization errors.