A sitemap lists the URLs you want search engines to crawl, and the App Router generates one from a file. Put sitemap.ts in the app directory, return an array of URL objects, and Next.js serves valid sitemap XML at /sitemap.xml. A static sitemap.xml file works too, but the generated version is what keeps up with your content.
The examples were verified with Next.js 16.3 in an App Router project, reading the responses from a production build that uses the default caching model, with the Cache Components flag off. The file runs on the server during the build, never in the browser.
The smallest generated sitemap
The default export returns an array. The type is exported by Next.js, so the field names and allowed values are checked as you write them.
// app/sitemap.ts
import type { MetadataRoute } from 'next'
export default function sitemap(): MetadataRoute.Sitemap {
return [
{ url: 'https://acme.com', lastModified: new Date('2026-08-01'), priority: 1 },
{ url: 'https://acme.com/about', lastModified: new Date('2026-08-01') },
]
}Requesting /sitemap.xml returns an XML document with one url element per entry, served as application/xml. A Date is rendered as an ISO timestamp, while a plain string is passed through as written, which matters if you prefer a date-only format.
Only the url field is required. Everything else is optional, and adding fields you cannot maintain is worse than leaving them out.
A static file works as well. Put a hand-written sitemap.xml in the app directory when the site has a handful of fixed pages, and switch to the generated form when the list starts changing.
The fields worth setting
Four optional fields cover most sites, and two of them are frequently misused.
| Field | Use it for |
|---|---|
| lastModified | The real last edit time, from your data |
| changeFrequency | A hint, one of always through never |
| priority | Relative importance inside your own site, 0 to 1 |
| alternates | Language versions of the same page |
The priority field is relative to your own pages, not a ranking signal against other sites. Setting every page to 1 tells a crawler nothing, so either vary it meaningfully or leave it out.
Building it from real data
A hardcoded list goes stale the day someone publishes a post. The useful version reads your content source and maps it, with the base URL kept in one place.
// app/sitemap.ts
import type { MetadataRoute } from 'next'
import { getPublishedPosts } from '@/app/lib/posts'
const base = 'https://acme.com'
const staticRoutes = ['', '/about', '/blog']Those two constants are the parts you maintain by hand. Everything else comes from the content source, so publishing a post updates the sitemap without anyone editing this file.
// app/sitemap.ts
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await getPublishedPosts()
return [
...staticRoutes.map((path) => ({ url: `${base}${path}` })),
...posts.map((post) => ({
url: `${base}/blog/${post.slug}`,
lastModified: post.updatedAt,
})),
]
}The generated file now lists the static routes followed by every published post, with each post carrying its own modification date. Filter in the query rather than in the map, so drafts and unlisted records never reach the file.
Read the base URL from an environment variable in a real project, the same value used for canonical URLs and metadataBase. A sitemap that points at the wrong host is worse than no sitemap.
Splitting by section
A nested file serves the path of its own segment, which is a simple way to keep a large site organized without any extra API.
// app/docs/sitemap.ts
import type { MetadataRoute } from 'next'
export default function sitemap(): MetadataRoute.Sitemap {
return [{ url: 'https://acme.com/docs/intro', lastModified: '2026-08-01' }]
}This is served at /docs/sitemap.xml, independent of the root sitemap. Each file is its own route, so a slow documentation query does not delay the main sitemap.
Splitting by section also keeps ownership clear. The team that owns the docs owns the file that lists them, and a mistake in one Next.js sitemap cannot break the others.
Once one section outgrows 50,000 URLs, the format itself requires splitting, which is what generateSitemaps exists for. A nested file and that function solve different problems, so reach for the nested file first.
When the file is generated
The sitemap route is a Route Handler under the hood, prerendered at build and cached, which is why the build output lists it as a static entry. That is the behavior you want, since crawlers hit it rarely and the content changes on deploy.
Reading request-time data changes that. A sitemap that awaits a request-time API is marked as rendered on demand in the build output, so it runs per request.
├ ○ /sitemap.xml
├ ƒ /live/sitemap.xmlThe circle is the prerendered file and the other symbol marks the on-demand one. Enabling Cache Components changes this table, so read the legend your own build prints. If your sitemap depends on content that changes between deploys, prefer revalidation over per-request rendering, which the article on Next.js cache revalidation covers.
Tell crawlers where it is
A sitemap nobody knows about does very little, because crawlers do not guess paths beyond the conventional one at the site root. The robots file is the standard place to announce it, and its sitemap field accepts several entries.
// app/robots.ts
import type { MetadataRoute } from 'next'
export default function robots(): MetadataRoute.Robots {
return {
rules: { userAgent: '*', allow: '/' },
sitemap: ['https://acme.com/sitemap.xml', 'https://acme.com/docs/sitemap.xml'],
}
}The generated robots.txt now ends with one Sitemap line per entry, which was confirmed by requesting the file from a production build. Submitting the URL in a search console is still worth doing for the reporting, but the robots file is what every crawler reads first.
Keep the two in sync. Every Next.js sitemap route you add should appear in this list, since nothing discovers a nested sitemap automatically.
Common mistakes
Each of these produces a Next.js sitemap that validates and still works against you, which is why they survive review.
- Writing relative paths, which the sitemap format does not accept.
- Listing pages that carry a noindex tag, which sends conflicting signals about the same URL.
- Setting lastModified to the current date on every build, so every URL looks freshly edited.
- Including query-string variants of the same page instead of the canonical path.
- Leaving a hardcoded list in place after the content moved into a database.
- Building URLs from a hardcoded domain that no longer matches the site, which is easy to miss because the file still validates.
Rune AI
Key Insights
- app/sitemap.ts returns an array typed as MetadataRoute.Sitemap and is served at /sitemap.xml.
- URLs must be absolute, so build them from one base URL constant.
- lastModified accepts a Date or a string, and a Date is rendered as an ISO timestamp.
- A nested sitemap file serves its own segment path, such as /docs/sitemap.xml.
- The route is prerendered and cached unless it reads request-time data.
Frequently Asked Questions
Where does the sitemap file go?
Do the URLs have to be absolute?
Is the sitemap regenerated on every request?
How many URLs can one sitemap hold?
Conclusion
A sitemap in the App Router is one file that returns an array of URL objects. Keep the URLs absolute and built from a base constant, include only pages you want indexed, add lastModified where you actually track it, and split the file once the catalog outgrows a single sitemap.
More in this topic
`generateMetadata` Explained with Real Examples
What generateMetadata does, when it runs, and how to use it for real routes: awaited params, deduplicated data fetching, extending parent metadata, and returning a 404 from metadata.
Canonical URLs in Next.js: `metadataBase`, `alternates.canonical`, and Dynamic Pages
How canonical URLs work in the Next.js App Router: setting metadataBase once, writing alternates.canonical per route, handling dynamic segments, and what happens when the base URL is missing.
Open Graph and Twitter Card Metadata in Next.js
How to write Open Graph and Twitter card metadata in the Next.js App Router: the openGraph and twitter fields, automatic card defaults, article tags, and image merge rules.