Architecting Discoverability: A Comprehensive Guide to Sitemap Implementation in Next.js
· 4 min read

Table of contents
A sitemap is a structured roadmap of a website's important pages. It tells search engines what exists, when it last changed, and how often it changes — so they can crawl efficiently. Treat it not as an optional SEO accessory but as core discoverability infrastructure.
When Do You Actually Need a Sitemap?
Sitemaps matter most when:
- Your site is large (500+ pages).
- It's a new domain with few inbound links.
- It has a complex architecture where some pages are hard to reach by crawling alone.
- It's rich in media (images, video) you want indexed.
App Router: the sitemap.ts Convention
The App Router ships a first-class, type-safe convention. Create
app/sitemap.ts and export a default function returning a MetadataRoute.Sitemap:
import type { MetadataRoute } from 'next';
export default function sitemap(): MetadataRoute.Sitemap {
const baseUrl = 'https://example.com';
return [
{
url: baseUrl,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 1,
},
{
url: `${baseUrl}/blog`,
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 0.8,
},
];
}To include dynamic routes, fetch your data and map it:
import type { MetadataRoute } from 'next';
import { getAllPosts } from '@/lib/posts';
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = 'https://example.com';
const posts = await getAllPosts();
const blogEntries = posts.map((post) => ({
url: `${baseUrl}/blog/${post.slug}`,
lastModified: new Date(post.date),
changeFrequency: 'monthly' as const,
priority: 0.6,
}));
return [
{ url: baseUrl, lastModified: new Date(), priority: 1 },
...blogEntries,
];
}Pages Router: getServerSideProps
In the Pages Router you generate XML yourself, typically from a route that streams the response:
// pages/sitemap.xml.tsx
import type { GetServerSideProps } from 'next';
function generateSiteMap(posts: { slug: string }[]) {
return `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url><loc>https://example.com</loc></url>
${posts
.map((p) => `<url><loc>https://example.com/blog/${p.slug}</loc></url>`)
.join('')}
</urlset>`;
}
export const getServerSideProps: GetServerSideProps = async ({ res }) => {
const posts = await fetchPosts();
res.setHeader('Content-Type', 'text/xml');
res.write(generateSiteMap(posts));
res.end();
return { props: {} };
};
export default function SiteMap() {
return null;
}Enterprise Scaling with generateSitemaps()
A single sitemap is capped at 50,000 URLs. For very large sites, the App
Router's generateSitemaps() splits content into multiple sitemap files
behind a sitemap index:
import type { MetadataRoute } from 'next';
export async function generateSitemaps() {
// Return one entry per chunk of ~50k URLs.
return [{ id: 0 }, { id: 1 }, { id: 2 }];
}
export default async function sitemap({
id,
}: {
id: number;
}): Promise<MetadataRoute.Sitemap> {
const start = id * 50000;
const end = start + 50000;
const products = await getProducts(start, end);
return products.map((p) => ({ url: `https://example.com/p/${p.id}` }));
}Native vs. Third-Party (next-sitemap)
- Native App Router wins for small-to-medium projects: zero dependencies, type-safe, and dead simple.
next-sitemapand similar libraries become worthwhile for enterprise scale where you need automatic sitemap indexing, splitting, and richer config out of the box.
Architectural Guidance
- Prefer dynamic generation over hand-maintained static XML for content-driven sites.
- Use ISR (Incremental Static Revalidation) to balance freshness against performance — regenerate periodically instead of on every request.
Deployment & Maintenance
- Validate via Google Search Console.
- Advertise your sitemap in
robots.txt:
User-agent: *
Allow: /
Sitemap: https://example.com/sitemap.xml- For CMS-driven content, trigger webhook-based revalidation so new content shows up in the sitemap quickly.
Conclusion
A good sitemap is quiet infrastructure: invisible to users, invaluable to crawlers. In Next.js, the App Router makes the common case trivial and the enterprise case tractable — so there's no excuse to skip it.