--- title: "How to Add a Sitemap in Next.js" description: "Add a sitemap in Next.js with the App Router's app/sitemap.ts: generate entries from your data, set lastModified, split large sitemaps, and submit to GSC." author: "Alec Lindsay" date: "2026-06-23" tags: "Next.js, SEO, SEO for Developers" url: "https://seoagent.com/blog/nextjs-sitemap" --- # How to Add a Sitemap in Next.js **TL;DR —** In the Next.js App Router, add a `app/sitemap.ts` file that default-exports a function returning `MetadataRoute.Sitemap` — an array of URL objects. Next serves it at `/sitemap.xml` automatically. Generate the entries from your real data source so new pages show up on their own, then reference it in `robots.txt` and submit it once in Google Search Console. ```ts // app/sitemap.ts import type { MetadataRoute } from 'next'; export default function sitemap(): MetadataRoute.Sitemap { return [{ url: 'https://example.com', lastModified: new Date() }]; } ``` That's the whole convention. The rest of this guide is about doing it well — pulling entries from your content, setting the optional fields correctly, splitting large sitemaps, and making sure search engines find it. It's the hands-on companion to the broader [Next.js SEO](/blog/nextjs-seo) guide. ## Why a sitemap matters for indexing A sitemap is a machine-readable list of the URLs you want indexed, with optional hints about when each one last changed. It doesn't *guarantee* indexing — Google decides that — but it gives crawlers a complete, authoritative map of your site instead of making them discover pages by following links. That matters most where link discovery is weakest: brand-new pages with few inbound links, deep pages buried several clicks from the homepage, and large sites where crawl budget is finite. For a small static site the payoff is modest, but for a blog or catalog that adds pages over time, a sitemap generated from your data is the difference between new content getting found in hours versus weeks. ## Step 1: Create `app/sitemap.ts` The App Router has a file convention for this. Drop a `sitemap.ts` (or `.js`) file in your `app/` directory that default-exports a function returning `MetadataRoute.Sitemap`. Next builds `/sitemap.xml` from it — no XML, no third-party package, no manual `` wiring. The function can be synchronous or `async`, which is the key feature: you can `await` your CMS, database, or filesystem and build the URL list from real data. ## Step 2: Generate entries dynamically from your content The point of doing this in code is that static sitemaps go stale. Pull your routes from the same source your pages render from, so adding a post automatically adds a sitemap entry. ```ts // app/sitemap.ts import type { MetadataRoute } from 'next'; import { getAllPosts } from '@/lib/posts'; const BASE_URL = 'https://example.com'; export default async function sitemap(): Promise { const posts = await getAllPosts(); const staticRoutes: MetadataRoute.Sitemap = [ { url: BASE_URL, lastModified: new Date(), changeFrequency: 'weekly', priority: 1 }, { url: `${BASE_URL}/blog`, lastModified: new Date(), changeFrequency: 'daily', priority: 0.8 }, { url: `${BASE_URL}/pricing`, lastModified: new Date(), changeFrequency: 'monthly', priority: 0.5 }, ]; const postRoutes: MetadataRoute.Sitemap = posts.map((post) => ({ url: `${BASE_URL}/blog/${post.slug}`, lastModified: post.updatedAt, changeFrequency: 'monthly', priority: 0.7, })); return [...staticRoutes, ...postRoutes]; } ``` A few things worth getting right: - **Use absolute URLs.** Entries must be fully qualified (`https://example.com/...`), not relative paths. Keep the origin in one constant. - **List the canonical URL only.** One entry per page, matching the canonical in your metadata. Don't list trailing-slash and non-trailing-slash variants, or both `www` and apex. - **Pull `lastModified` from real timestamps.** A meaningful `updatedAt` is a useful crawl hint; `new Date()` on every build just tells crawlers "everything changed," which they learn to ignore. ## Step 3: Understand `lastModified`, `changeFrequency`, and `priority` Each entry supports three optional fields beyond `url`: | Field | Type | What it does | | --- | --- | --- | | `lastModified` | `Date \| string` | When the page last meaningfully changed. The most useful hint — keep it honest. | | `changeFrequency` | `'always' \| 'hourly' \| 'daily' \| 'weekly' \| 'monthly' \| 'yearly' \| 'never'` | How often the page tends to change. A weak hint. | | `priority` | `0.0`–`1.0` | Relative importance *within your own site*. Also a weak hint. | Be realistic about what these do. Google has said for years that it largely **ignores `changeFrequency` and `priority`** and leans on `lastModified` — and only when it's accurate. Invest in a truthful `lastModified`, set the other two roughly if you like (homepage high, deep utility pages low), and don't agonize over them. They won't rank a weak page higher. ## Step 4: Split sitemaps for large sites The sitemap spec caps a single file at **50,000 URLs or 50 MB uncompressed**. If you're near either limit, split into multiple sitemaps with a sitemap index. Next supports this via `generateSitemaps`. You return a list of ids, and Next calls your `sitemap` function once per id, serving each chunk at `/sitemap/[id].xml`: ```ts // app/sitemap.ts import type { MetadataRoute } from 'next'; import { getProductCount, getProducts } from '@/lib/products'; const BASE_URL = 'https://example.com'; const PER_SITEMAP = 50_000; export async function generateSitemaps() { const total = await getProductCount(); const pages = Math.ceil(total / PER_SITEMAP); return Array.from({ length: pages }, (_, id) => ({ id })); } export default async function sitemap({ id }: { id: number }): Promise { const start = id * PER_SITEMAP; const products = await getProducts({ offset: start, limit: PER_SITEMAP }); return products.map((product) => ({ url: `${BASE_URL}/products/${product.slug}`, lastModified: product.updatedAt, })); } ``` Below 50,000 URLs, don't bother — a single sitemap is simpler and just as effective. Most blogs and SaaS sites never come close. ## Step 5: Reference it in robots.txt and submit to Search Console A sitemap that nothing points to is harder for crawlers to find. Two steps close the loop: **1. Reference it from `robots.txt`.** In the App Router, add a `app/robots.ts` that points at the sitemap. If you split into multiple files, point at the index URL (`/sitemap.xml`), which Next generates for you. ```ts // app/robots.ts import type { MetadataRoute } from 'next'; export default function robots(): MetadataRoute.Robots { return { rules: { userAgent: '*', allow: '/' }, sitemap: 'https://example.com/sitemap.xml', }; } ``` **2. Submit it once in Google Search Console.** Go to **Indexing → Sitemaps**, enter `sitemap.xml`, and submit. You only do this once; from then on GSC re-fetches it periodically and reports any parsing errors or pages it chose not to index. (If you deploy on [Vercel](/vercel), `/sitemap.xml` is served automatically from your `sitemap.ts` — there's nothing extra to configure.) ## Common mistakes - **Forgetting dynamic routes.** A `[slug]` route renders fine, but if your `sitemap.ts` only lists static pages, none of those dynamic URLs appear. Generate them from the same data the pages use — that's the entire reason to do this in code. - **Including noindex or private pages.** Don't list pages marked `noindex`, auth-gated dashboards, thank-you pages, or staging routes. A sitemap is a list of pages you *want* indexed; mixing in pages you don't sends a contradictory signal. - **Letting a static sitemap go stale.** A hand-written `public/sitemap.xml` drifts out of sync over time. The `app/sitemap.ts` convention regenerates on each build (or per request, for dynamic data), so it stays current on its own. - **Listing non-canonical or wrong-origin URLs.** Trailing-slash duplicates, `http` vs `https`, query-string versions, or stray `localhost` entries all muddy your signals. Pick the canonical production form and list only that. ## Frequently asked questions ### Do I need a sitemap if my pages are already linked internally? Not strictly — crawlers can discover well-linked pages on their own. But a sitemap helps for new pages, deep pages, and large sites, and it's the cleanest way to declare your canonical URLs to Search Console. For a content site that grows over time, it's worth the few lines of code. ### What's the difference between `sitemap.ts` and `sitemap.xml`? `app/sitemap.ts` is the source file you write; `/sitemap.xml` is the route Next.js generates from it and serves to crawlers. You author TypeScript, your users (and Googlebot) get valid XML. You never write the XML by hand. ### Does Next.js generate the sitemap automatically? It generates the XML *from your code*, not magically from your routes. Next won't crawl your `app/` directory and infer URLs — you decide which pages to include by returning them from the `sitemap` function. The convention handles the XML serialization and serving; you handle the URL list. ### How do I add a sitemap in the Pages Router instead? The App Router convention (`app/sitemap.ts`) doesn't apply to the Pages Router. There, the common pattern is an API route or a `getServerSideProps` page at `pages/sitemap.xml.tsx` that writes the XML to the response manually. If you're starting fresh, use the App Router — the built-in convention is far less code. ## Conclusion Adding a sitemap in Next.js is a small file with an outsized payoff: one `app/sitemap.ts` that generates URLs from your real content, an honest `lastModified` on each entry, a `robots.txt` reference, and a one-time Search Console submission. Keep it generated from data so it never goes stale, and don't over-invest in `changeFrequency` and `priority` — `lastModified` is the field that earns its keep. If you want a second set of eyes on how your pages read to crawlers and AI assistants, [SEOAgent](/blog/seo-agent) runs inside your coding agent to audit sitemaps, metadata, and internal links right in the repo, and you can spot-check a page's AI readiness with the [OKF checker](/okf-checker). For the full picture of building search-ready sites in code, start with the [SEO for developers](/blog/seo-for-developers) guide.