Back to blog

How to generate a Next.js sitemap for SEO

How to generate a Next.js sitemap for SEO
Next.js SEOTechnical SEO

Modern SSR apps live or die by crawl efficiency. If search engines cannot discover your URLs quickly, rankings stall and new pages languish.

This guide shows developers how to implement fast, reliable Next.js sitemap generation for production apps. It is for React and Next.js teams shipping SSR or hybrid sites. The key takeaway: wire a deterministic sitemap pipeline that updates on every deploy or data change, validates URLs, and scales with your routes and content sources.

What is a sitemap and why Next.js apps need one

A sitemap is a machine-readable index of URLs that helps crawlers find your pages. Next.js apps often render many paths at build or request time, so an accurate sitemap ensures fresh coverage.

Benefits for technical SEO

  • Faster discovery for new or updated routes
  • Clear signal of canonical URLs across locales and variants
  • Helps crawlers prioritize with lastmod and changefreq

Common pitfalls in React and SSR apps

  • Forgetting to include dynamic routes from headless data
  • Shipping stale links due to long cache or manual builds
  • Duplicating URLs across domains without a canonical plan

Choosing a Next.js sitemap approach

You have multiple patterns depending on your Next.js version and hosting model.

Next.js 13+ App Router route handlers

Use app routes to serve sitemap.xml at runtime. This is flexible for SSR and allows on-demand data.

// app/sitemap.ts (Next.js 13+ App Router)
import { MetadataRoute } from 'next'

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const baseUrl = process.env.SITE_URL || 'https://example.com'
  const posts = await fetch(`${baseUrl}/api/posts`).then(r => r.json()) as { slug: string, updatedAt: string }[]

  const staticUrls: MetadataRoute.Sitemap = [
    { url: `${baseUrl}/`, lastModified: new Date() },
    { url: `${baseUrl}/about`, lastModified: new Date('2024-01-01') },
  ]

  const postUrls = posts.map(p => ({
    url: `${baseUrl}/blog/${p.slug}`,
    lastModified: new Date(p.updatedAt),
  }))

  return [...staticUrls, ...postUrls]
}

Next.js 12 or Pages Router API route

Generate XML manually in an API route if you are on the Pages Router.

// pages/sitemap.xml.ts
import type { NextApiRequest, NextApiResponse } from 'next'

function toXml(urls: { loc: string; lastmod?: string; changefreq?: string; priority?: number }[]) {
  const body = urls.map(u => `\n  <url>\n    <loc>${u.loc}</loc>${u.lastmod ? `\n    <lastmod>${u.lastmod}</lastmod>` : ''}${u.changefreq ? `\n    <changefreq>${u.changefreq}</changefreq>` : ''}${u.priority != null ? `\n    <priority>${u.priority}</priority>` : ''}\n  </url>`).join('')
  return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">${body}\n</urlset>`
}

export default async function handler(_req: NextApiRequest, res: NextApiResponse) {
  const baseUrl = process.env.SITE_URL || 'https://example.com'
  const products = await fetch(`${baseUrl}/api/products`).then(r => r.json()) as { slug: string, updatedAt: string }[]

  const urls = [
    { loc: `${baseUrl}/` },
    ...products.map(p => ({ loc: `${baseUrl}/products/${p.slug}`, lastmod: new Date(p.updatedAt).toISOString() })),
  ]

  res.setHeader('Content-Type', 'application/xml')
  res.write(toXml(urls))
  res.end()
}

Implementing nextjs sitemap generation end to end

This section outlines a reliable workflow from URL collection to serving XML.

Step 1: Define URL sources and canonical rules

  • Static routes: pages like home, pricing, contact
  • Dynamic content: blog posts, docs, products
  • Internationalized routes: locales and alternates
  • Canonical plan: one preferred URL per page with optional x-default

Step 2: Create a URL assembly layer

Centralize a function that returns typed entries with lastModified, priority, and change frequency.

// lib/urls.ts
export type UrlEntry = { path: string; lastModified?: Date; priority?: number; changefreq?: 'daily' | 'weekly' | 'monthly' }

export async function getAllUrlEntries(): Promise<UrlEntry[]> {
  const staticEntries: UrlEntry[] = [
    { path: '/', priority: 1.0, changefreq: 'daily' },
    { path: '/pricing', changefreq: 'weekly' },
  ]

  const posts = await fetch('https://example.com/api/posts').then(r => r.json()) as { slug: string, updatedAt: string }[]
  const postEntries: UrlEntry[] = posts.map(p => ({ path: `/blog/${p.slug}`, lastModified: new Date(p.updatedAt), changefreq: 'weekly' }))

  return [...staticEntries, ...postEntries]
}

Step 3: Render to the format your router expects

  • App Router: return MetadataRoute.Sitemap from app/sitemap.ts
  • Pages Router: serialize as XML in an API route
// app/sitemap.ts
import { MetadataRoute } from 'next'
import { getAllUrlEntries } from '@/lib/urls'

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const baseUrl = process.env.SITE_URL!
  const entries = await getAllUrlEntries()
  return entries.map(e => ({
    url: `${baseUrl}${e.path}`,
    lastModified: e.lastModified ?? new Date(),
  }))
}

Step 4: Add a sitemap index when you exceed limits

If you are near 50k URLs or large XML size, split into multiple files and reference them in an index.

// app/sitemap-index.ts
import { MetadataRoute } from 'next'

export default function sitemap(): MetadataRoute.SitemapIndex {
  const base = process.env.SITE_URL!
  return [
    { url: `${base}/sitemaps/static.xml`, lastModified: new Date() },
    { url: `${base}/sitemaps/blog.xml`, lastModified: new Date() },
  ]
}

Handling i18n, alternates, and canonical strategy

Internationalization needs hreflang alternates and a clear canonical.

i18n with App Router alternates

Use Next.js Metadata API to expose alternates per route.

// app/[locale]/blog/[slug]/page.tsx
import { Metadata } from 'next'

export async function generateMetadata({ params }): Promise<Metadata> {
  const { slug } = params
  const base = process.env.SITE_URL!
  const locales = ['en', 'de', 'fr']
  const alternates: Record<string, string> = {}
  for (const l of locales) alternates[l] = `${base}/${l}/blog/${slug}`

  return {
    alternates: {
      canonical: `${base}/en/blog/${slug}`,
      languages: alternates,
    },
  }
}

Emitting hreflang in sitemaps

If you manage XML directly, include xhtml:link entries for alternates.

<url>
  <loc>https://example.com/en/blog/my-post</loc>
  <xhtml:link rel="alternate" hreflang="en" href="https://example.com/en/blog/my-post"/>
  <xhtml:link rel="alternate" hreflang="de" href="https://example.com/de/blog/my-post"/>
</url>

Automating updates with ISR and webhooks

You want the sitemap to refresh when content changes, not just on deploy.

Revalidate on content writes

  • If using Incremental Static Regeneration, call revalidatePath('/sitemap.xml') in a route handler after content writes.
  • For external CMS or database events, trigger a webhook that hits a revalidation endpoint.
// app/api/revalidate/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { revalidatePath } from 'next/cache'

export async function POST(req: NextRequest) {
  const secret = req.nextUrl.searchParams.get('secret')
  if (secret !== process.env.REVALIDATE_SECRET) return NextResponse.json({ ok: false }, { status: 401 })
  revalidatePath('/sitemap.xml')
  return NextResponse.json({ revalidated: true })
}

Queueing for scale

For large catalogs, queue URL refresh jobs. Store last updates and only touch changed groups to avoid heavy recomputes.

// pseudo: enqueue partial sitemap rebuild per content type
await queue.add('rebuild-sitemap', { type: 'blog', changedIds })

Validating sitemap correctness

Trust but verify. Add automated checks in CI or during runtime.

Structural and URL validation

  • Ensure absolute URLs with https
  • Confirm no 404s or 301 chains
  • Keep <= 50k URLs per file and under typical size limits
// simple runtime check
import fetch from 'node-fetch'

export async function assertOk(urls: string[]) {
  const results = await Promise.all(urls.slice(0, 50).map(u => fetch(u, { method: 'HEAD' })))
  const failures = results.filter(r => r.status >= 400)
  if (failures.length) throw new Error('Broken URLs in sitemap')
}

Monitoring freshness

  • Sample pages and compare lastmod to content updatedAt
  • Alert if drift exceeds a threshold

nextjs seo checklist items tied to the sitemap

A sitemap is one piece of technical seo nextjs work. Tie these items into your pipeline.

Robots.txt and submit to Search Console

  • Serve robots.txt with a Sitemap: directive
  • Submit sitemap index in Google Search Console for visibility
User-agent: *
Allow: /
Sitemap: https://example.com/sitemap.xml

Metadata and schema alignment

  • Use the Next.js metadata API for titles, canonicals, and alternates
  • Emit schema markup for articles or products and ensure URLs match sitemap entries
// app/blog/[slug]/page.tsx
export const dynamic = 'force-static'
export async function generateMetadata() { return { title: 'Post title' } }

Comparing sitemap generation strategies

The table below compares common approaches and when to use each.

StrategyRouterData FreshnessComplexityBest for
App Router route handlerAppReal timeMediumSSR or hybrid sites needing live data
Pages Router API routePagesReal timeMediumLegacy Pages apps with API routes
Build-time static fileAnyOn deployLowSmall sites with rare updates
External generator serviceAnyVariesLow to MediumVery large catalogs or monorepos

Programmatic seo patterns for large catalogs

When you manage thousands of pages, think in systems, not scripts.

Shard into multiple sitemaps and index

Group by content type or date bucket. Keep each file under limits and parallelize generation.

/sitemaps/static.xml
/sitemaps/blog.xml
/sitemaps/products-0001.xml
/sitemaps/products-0002.xml

Idempotent jobs and retry safety

Make sitemap builds idempotent. Use content fingerprints to avoid rewriting identical XML and to safely retry failed shards.

// compute a hash of URL list to short-circuit writes
import { createHash } from 'crypto'
const hash = createHash('sha1').update(JSON.stringify(urls)).digest('hex')

Integrating with automated blog publishing workflows

If you already automate metadata, schema, and internal linking, plug sitemap updates into the same queue.

Hook sitemap refresh into publish events

  • On post publish: revalidate blog sitemap shard
  • On bulk import: rebuild index after shards complete

Guardrails for seo for ssr applications

  • Never include noindex pages
  • Emit canonical URLs only, not preview or staging hosts
  • Keep internal linking consistent with sitemap paths

Troubleshooting and performance tips

Issues usually stem from environment config or data drift.

404s or mixed hosts in production

  • Verify SITE_URL is correct per environment
  • Enforce https and primary domain in your URL builder

Large memory use or slow responses

  • Cache assembled URL lists for a short TTL
  • Stream XML responses for very large lists on Pages Router
// streaming XML on Pages Router
res.setHeader('Content-Type', 'application/xml')
res.write('<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">')
for await (const chunk of streamUrls()) res.write(chunk)
res.end('</urlset>')

Key Takeaways

  • Implement nextjs sitemap generation using App Router route handlers or a Pages API route.
  • Keep URLs canonical, absolute, and updated with lastmod, and shard with an index at scale.
  • Automate refresh via ISR, webhooks, and queues to avoid stale entries.
  • Validate structure, check for 404s, and integrate with metadata and schema.
  • Treat the sitemap as part of a broader programmatic seo pipeline.

A clean, automated sitemap flow helps crawlers keep pace with your shipping velocity and protects long term search performance.

Frequently Asked Questions

What is the best way to create a sitemap in Next.js?
Use an App Router route handler in app/sitemap.ts to return MetadataRoute.Sitemap. For Pages Router, generate XML in an API route.
How often should I update my sitemap?
Update whenever content changes. Trigger revalidation via webhooks or on publish events so crawlers see updates quickly.
Do I need a sitemap index for large sites?
Yes if you approach 50k URLs or large XML sizes. Split by content type or date and reference files in a sitemap index.
How do I handle i18n in sitemaps?
Provide hreflang alternates via Next.js Metadata API or emit xhtml:link entries in XML. Keep a single canonical per page.
Should preview or noindex pages be in the sitemap?
No. Only include canonical, indexable URLs from your primary production domain.
Powered byautoblogwriter