How to generate a Next.js sitemap for SSR apps

Modern SSR React apps ship fast, but search engines still need a clean map of your URLs. A reliable sitemap removes guesswork for crawlers and prevents stale or missing pages from slipping through.
This guide explains Next.js sitemap generation for SSR apps. It is for developers and SaaS teams who want crawlable, up to date URLs without manual steps. The key takeaway: build a deterministic, automated pipeline that outputs a compressed, index friendly sitemap and keeps it fresh with revalidation hooks.
What is a sitemap and why it matters in SSR
SSR frameworks already render HTML, but search engines still benefit from structured discovery.
Sitemap basics
- XML files that list canonical URLs with optional lastmod, changefreq, and priority.
- Usually referenced in robots.txt and submitted in Search Console.
- For larger sites, use a sitemap index that points to multiple child sitemaps.
Why SSR apps still need sitemaps
- Dynamic routes and incremental builds can hide long tail URLs.
- Staging flags and feature toggles may alter visibility; sitemaps keep a source of truth.
- Faster discovery for new product pages, docs, and blog posts in programmatic SEO content.
Choosing a strategy for Next.js sitemap generation
How you generate the sitemap depends on your routing and data shape.
Static build time
- Generate during next build using Node scripts or next-sitemap.
- Best for sites whose URL inventory rarely changes between deploys.
- Pro: fast and simple. Con: stale between builds.
On demand server route
- Implement an API or app route that fetches slugs and streams XML at request time.
- Pro: always fresh. Con: hits your data source; cache wisely.
Hybrid with cache and revalidation
- Build a route that regenerates periodically or via webhooks, then cache the XML.
- Pro: fresh without heavy runtime load. Con: needs cache invalidation strategy.
Quick start with next-sitemap
If you want a batteries included setup for common cases, a plugin can get you live quickly.
Install and configure
npm i next-sitemap -D
Create next-sitemap.config.js:
/** @type {import('next-sitemap').IConfig} */
module.exports = {
siteUrl: process.env.SITE_URL || 'https://example.com',
generateRobotsTxt: true,
sitemapSize: 45000,
exclude: ['/admin/**', '/api/**'],
robotsTxtOptions: {
policies: [{ userAgent: '*', allow: '/', disallow: ['/admin'] }],
},
};
Add a build script to package.json:
{
"scripts": {
"postbuild": "next-sitemap"
}
}
When to use next-sitemap
- Your URLs are largely file system routes or fetchable at build.
- You prefer files on disk (sitemap.xml, robots.txt) deployed with your app.
- You do not need per request generation.
Rolling your own dynamic sitemap in App Router
For SSR apps with dynamic inventory, implement a runtime route that emits XML.
Route file structure
In app/sitemap.xml/route.ts for Next.js App Router:
import { NextResponse } from 'next/server';
// Example data fetcher
async function getUrls() {
// Replace with DB, CMS, or API calls
const posts = await fetch(`${process.env.API}/posts`).then(r => r.json());
const docs = await fetch(`${process.env.API}/docs`).then(r => r.json());
return [
{ loc: '/', lastmod: new Date().toISOString() },
...posts.map((p: any) => ({ loc: `/blog/${p.slug}`, lastmod: p.updatedAt })),
...docs.map((d: any) => ({ loc: `/docs/${d.slug}`, lastmod: d.updatedAt })),
];
}
function toXml(urls: { loc: string; lastmod?: string }[]) {
const body = urls
.map(u => `<url><loc>${process.env.SITE_URL}${u.loc}</loc>${u.lastmod ? `<lastmod>${u.lastmod}</lastmod>` : ''}</url>`)
.join('');
return `<?xml version="1.0" encoding="UTF-8"?>` +
`<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">${body}</urlset>`;
}
export async function GET() {
const urls = await getUrls();
const xml = toXml(urls);
return new NextResponse(xml, {
headers: {
'Content-Type': 'application/xml; charset=utf-8',
'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400',
},
});
}
Caching and edge considerations
- Serve with Cache-Control headers for CDN caching.
- Prefer server runtime; consider edge only if your data fetches are edge compatible.
- Keep the response under memory limits by paging or using a sitemap index.
Building a sitemap index for large sites
Once you have more than ~50k URLs or a multi section IA, split sitemaps.
Index structure
- sitemap.xml is an index that points to child sitemaps.
- Each child file should contain up to 50k URLs or stay under 50 MB uncompressed.
Example implementation
Create app/sitemap.xml/route.ts as index and app/sitemaps/[name]/route.ts for children:
// app/sitemap.xml/route.ts
import { NextResponse } from 'next/server';
export async function GET() {
const sitemaps = ['static', 'blog', 'docs'];
const urls = sitemaps
.map(n => `<sitemap><loc>${process.env.SITE_URL}/sitemaps/${n}.xml</loc></sitemap>`)
.join('');
const xml = `<?xml version="1.0" encoding="UTF-8"?>` +
`<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">${urls}</sitemapindex>`;
return new NextResponse(xml, { headers: { 'Content-Type': 'application/xml' } });
}
// app/sitemaps/blog.xml/route.ts
import { NextResponse } from 'next/server';
async function getBlogUrls() {
const posts = await fetch(`${process.env.API}/posts`).then(r => r.json());
return posts.map((p: any) => ({ loc: `/blog/${p.slug}`, lastmod: p.updatedAt }));
}
export async function GET() {
const items = await getBlogUrls();
const body = items
.map(u => `<url><loc>${process.env.SITE_URL}${u.loc}</loc><lastmod>${u.lastmod}</lastmod></url>`)
.join('');
const xml = `<?xml version="1.0" encoding="UTF-8"?>` +
`<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">${body}</urlset>`;
return new NextResponse(xml, { headers: { 'Content-Type': 'application/xml' } });
}
Integrating Next.js Metadata API and canonicals
Sitemap quality depends on canonical signals and consistent metadata.
Canonical URLs and trailing slashes
- Pick a single canonical scheme: https, non www or www, trailing slash policy.
- In app router, use the metadata export to set alternates.canonical.
// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }) {
const post = await fetchPost(params.slug);
const url = `${process.env.SITE_URL}/blog/${post.slug}`;
return {
title: post.title,
description: post.excerpt,
alternates: { canonical: url },
};
}
Robots, noindex, and excluded paths
- Exclude admin and auth routes from the sitemap.
- Add noindex to gated or duplicate pages to avoid mixed signals.
export const metadata = { robots: { index: false, follow: false } };
Keeping the sitemap fresh with revalidation
Dynamic content needs timely updates without rebuilds.
On demand revalidation hooks
- When content changes in your CMS, call a webhook that pings a route to purge sitemap cache.
- Recompute XML on next request; pair with ISR revalidation for pages.
// app/api/revalidate-sitemap/route.ts
import { NextResponse } from 'next/server';
export async function POST() {
// Example: invalidate CDN key or flip a KV timestamp
// await kv.set('sitemap:updated', Date.now());
return NextResponse.json({ ok: true });
}
Handling large diffs safely
- Recompute per section to keep latency predictable.
- Use idempotent jobs and queues to avoid duplicate work during spikes.
Programmatic SEO patterns with sitemaps
Sitemaps are a backbone for programmatic SEO in SaaS and docs heavy sites.
URL generation rules
- Deterministic slug rules per entity type.
- Stable categories and paginated indexes with canonicals.
- Avoid thin or parameter only pages without unique value.
Internal linking and discovery
- Use collections pages to surface long tail URLs.
- Keep sitemap and internal links aligned to avoid orphan pages.
If you want a partner for custom development that scales with clean SEO foundations, teams often rely on expert implementers like Bayline Digital for custom websites and platform work. See their custom website services for more.
Comparing sitemap approaches in Next.js
Here is a concise comparison to help you choose.
| Approach | Freshness | Complexity | Performance | Best for |
|---|---|---|---|---|
| Build time (next-sitemap) | Low | Low | High | Small to medium sites with stable URLs |
| Server route cached | Medium | Medium | High | Dynamic catalogs and blogs |
| Server route uncached | High | Medium | Medium | Small sites where accuracy beats speed |
| Hybrid with revalidation | High | Medium | High | Large SSR apps with CMS triggers |
| Sitemap index split | High | Medium | High | Big sites or multi section IA |
Testing, monitoring, and troubleshooting
Make validation a habit to avoid silent failures.
Local validation
- Open sitemap.xml in the browser to confirm structure.
- Use XML validators and lint for invalid characters.
- Verify absolute URLs and protocol correctness.
Search Console and logs
- Submit your sitemap index in Google Search Console.
- Monitor the Sitemaps report for read errors and discovered URLs.
- Track 404s in server logs that originate from sitemap entries.
Production checklist for Next.js sitemap generation
Lock in a minimal, repeatable process.
Pre launch checks
- robots.txt references the correct sitemap path.
- All canonical hosts and protocols are consistent.
- Excluded routes are not present in the XML files.
Post launch checks
- Search Console shows successful fetches.
- CDN cache is serving fresh XML after edits.
- New content appears in the sitemap within expected SLAs.
Key Takeaways
- Use a simple plugin for small sites or a dynamic route with caching for SSR apps.
- Split into a sitemap index for large inventories and keep URLs canonical.
- Wire revalidation so sitemaps refresh on content changes without full rebuilds.
- Validate locally and in Search Console, and monitor logs for broken links.
A clean, automated sitemap pipeline keeps crawlers focused on your best URLs and reduces SEO toil over time.
Frequently Asked Questions
- What is the best way to generate a sitemap in Next.js?
- For stable sites, generate at build with next-sitemap. For dynamic SSR apps, create a server route that emits XML and cache it with revalidation.
- How often should I update my sitemap?
- Update on content changes. Use webhooks to trigger cache invalidation so the sitemap refreshes on the next request.
- Do I need a sitemap index?
- Use an index when you exceed about 50k URLs per file or want to separate sections like blog, docs, and products.
- Should I include noindex pages in the sitemap?
- No. Exclude admin, auth, and any page with robots noindex to avoid mixed signals for crawlers.
- Where should I reference my sitemap?
- Add Sitemap: https://your-domain.com/sitemap.xml in robots.txt and submit the sitemap index in Google Search Console.