Next.js Sitemap: app/sitemap.ts, generateSitemaps and next-sitemap
Short answer: Next.js doesn’t create a sitemap on its own. In the App Router, add app/sitemap.ts that returns a list of URLs, and Next.js serves it at /sitemap.xml. The feature arrived in Next.js 13.3. On the Pages Router, use the next-sitemap package or your own route. This site’s sitemap is built exactly this way, from app/sitemap.ts.
App Router: app/sitemap.ts
Export a default function that returns an array of entries. For dynamic routes, query your database or CMS inside the function, which can be async:
// app/sitemap.ts
import type { MetadataRoute } from 'next'
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await getPublishedPosts() // your CMS or database query
return [
{ url: 'https://example.com', lastModified: new Date('2026-09-01') },
{ url: 'https://example.com/pricing', lastModified: new Date('2026-08-12') },
...posts.map((post) => ({
url: `https://example.com/blog/${post.slug}`,
lastModified: post.updatedAt,
})),
]
}Three things to get right:
- Include the dynamic routes. A sitemap that lists only the hand-written marketing pages leaves every blog post and product page out.
- Filter at the query. Return only published, non-draft content, so drafts and future-dated posts never leak into the sitemap.
- Use real dates. Setting every
lastModifiedtonew Date()tells search engines everything changed on every build. Google says it uses lastmod only when it is consistently accurate, and it ignoreschangeFrequencyandpriority.
Caching: When Does the Sitemap Update?
According to the Next.js docs, sitemap.ts is a special Route Handler that is cached by default, unless it uses a request-time API or a dynamic config option. So a sitemap built from your database won’t pick up new posts on its own. Either set a revalidation period in the file, for example export const revalidate = 3600 to refresh hourly, or revalidate the sitemap when you publish.
Splitting Large Sitemaps
Google allows up to 50,000 URLs per sitemap file. Next.js gives you two ways to split: put sitemap.ts files in different route segments (for example app/sitemap.ts and app/products/sitemap.ts), or use generateSitemaps:
// app/products/sitemap.ts
import type { MetadataRoute } from 'next'
export async function generateSitemaps() {
return [{ id: 0 }, { id: 1 }, { id: 2 }] // one per 50,000 products
}
export default async function sitemap(props: {
id: Promise<string>
}): Promise<MetadataRoute.Sitemap> {
const id = Number(await props.id) // a promise since Next.js 16
const products = await getProducts({ offset: id * 50000, limit: 50000 })
return products.map((p) => ({
url: `https://example.com/products/${p.slug}`,
lastModified: p.updatedAt,
}))
}The files are served at paths like /products/sitemap/1.xml. The Next.js docs don’t describe an automatic sitemap index for these, so tell search engines where they are: submit each file in Search Console, list them with Sitemap: lines in robots.txt, or serve your own sitemap index.
Multilingual Sites
Since Next.js 14.2, each entry can carry alternates.languages, which Next.js turns into hreflang links inside the sitemap:
{
url: 'https://example.com/about',
lastModified: new Date('2026-09-01'),
alternates: {
languages: {
es: 'https://example.com/es/about',
de: 'https://example.com/de/about',
},
},
}Images and Videos
Entries also accept images (an array of image URLs) and videos, which Next.js outputs with Google’s image and video sitemap extensions.
Pages Router
The app/sitemap.ts convention belongs to the App Router. On the Pages Router, the common choice is the next-sitemap package, which generates sitemap.xml (and optionally robots.txt) into public/ after each build:
// next-sitemap.config.js
/** @type {import('next-sitemap').IConfig} */
module.exports = {
siteUrl: process.env.SITE_URL || 'https://example.com',
generateRobotsTxt: true,
exclude: ['/admin/*', '/api/*'],
}
// package.json
"scripts": { "postbuild": "next-sitemap" }Because it runs at build time, new content appears only after the next build. Don’t run it alongside an App Router sitemap.ts; pick one source.
Common Next.js Sitemap Problems
The sitemap shows localhost or preview URLs
The base URL comes from an environment variable that isn’t set in production. Set your production URL (for example SITE_URL=https://example.com) in the hosting environment and build every entry from it. Also check that preview deployments don’t serve a sitemap pointing at themselves.
New posts don’t appear
The sitemap is cached. Add a revalidation period or revalidate on publish (App Router), or rebuild (next-sitemap).
Only a handful of URLs are listed
The dynamic routes aren’t in the function. Query every source of pages (database, CMS, static routes) and merge the results.
Check the Result
Open /sitemap.xml in production, count the URLs, and compare them with the number of pages you expect. Then submit it in Google Search Console and Bing Webmaster Tools. For an outside check, the free sitemap finder reads your robots.txt, loads the sitemap, and tests 30 of its URLs for 404s, redirects, noindex tags and canonical problems.