Plainform

Mobile Sidebar

Mobile Sidebar

How to Generate a Sitemap in Next.js

11 Jun 2026
8 minute read
R
Ronald SolticzkiBackend Engineer
How to Generate a Sitemap in Next.js

A Next.js sitemap is an XML file that tells search engines which pages on your site exist, when they were last updated, and how often they change. It is one of the most straightforward SEO improvements you can make — a sitemap does not improve your rankings directly, but it makes sure search engines can discover and index all your pages.

Next.js gives you two main approaches for generating a sitemap: the built-in sitemap.ts file introduced in the App Router, and the next-sitemap package that runs as a post-build step. This post covers both, when to use each, how to handle dynamic content like blog posts and documentation, and how to exclude pages that should not appear in search results.

Option 1: The Built-in sitemap.ts

Since Next.js 13.3, the App Router supports a sitemap.ts (or sitemap.js) file placed in the app/ directory. Next.js picks it up automatically and serves it at /sitemap.xml.

Basic static sitemap

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

export default function sitemap(): MetadataRoute.Sitemap {
  return [
    {
      url: 'https://yoursite.com',
      lastModified: new Date(),
      changeFrequency: 'weekly',
      priority: 1,
    },
    {
      url: 'https://yoursite.com/pricing',
      lastModified: new Date(),
      changeFrequency: 'monthly',
      priority: 0.8,
    },
    {
      url: 'https://yoursite.com/blog',
      lastModified: new Date(),
      changeFrequency: 'daily',
      priority: 0.7,
    },
  ];
}

This file exports a function that returns an array of sitemap entries. Next.js serializes it to XML and serves it at /sitemap.xml with the correct application/xml content type. No additional package needed.

Dynamic sitemap for blog posts

For a blog or documentation site where pages are generated from content files, make the sitemap function async and fetch the list of pages:

// app/sitemap.ts
import { MetadataRoute } from 'next';
import { blogSource } from '@/lib/source';

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const baseUrl = process.env.NEXT_PUBLIC_SITE_URL!;

  // Static pages
  const staticPages: MetadataRoute.Sitemap = [
    {
      url: baseUrl,
      lastModified: new Date(),
      changeFrequency: 'weekly',
      priority: 1,
    },
    {
      url: `${baseUrl}/pricing`,
      changeFrequency: 'monthly',
      priority: 0.8,
    },
    {
      url: `${baseUrl}/blog`,
      changeFrequency: 'daily',
      priority: 0.7,
    },
  ];

  // Dynamic blog posts
  const blogPages: MetadataRoute.Sitemap = blogSource
    .getPages()
    .map((post) => ({
      url: `${baseUrl}${post.url}`,
      lastModified: post.data.date ? new Date(post.data.date) : new Date(),
      changeFrequency: 'monthly' as const,
      priority: 0.6,
    }));

  return [...staticPages, ...blogPages];
}

Multiple sitemaps with generateSitemaps

For large sites, a single sitemap file has a 50,000 URL limit. Split it using generateSitemaps:

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

export async function generateSitemaps() {
  // Return the IDs for each sitemap chunk
  return [{ id: 0 }, { id: 1 }];
}

export default async function sitemap({
  id,
}: {
  id: number;
}): Promise<MetadataRoute.Sitemap> {
  // Return different pages based on the id
  if (id === 0) {
    return [/* static pages */];
  }
  return [/* dynamic pages */];
}

Next.js generates /sitemap/0.xml and /sitemap/1.xml, plus a /sitemap.xml index file that references both.

Option 2: next-sitemap

next-sitemap is a popular package that generates the sitemap as a post-build step, writing public/sitemap.xml and public/robots.txt as static files. Plainform uses this approach.

Installation and setup

npm install next-sitemap

Add the post-build script to package.json:

{
  "scripts": {
    "build": "next build",
    "postbuild": "next-sitemap"
  }
}

The postbuild script runs automatically after next build. Every production build regenerates the sitemap.

Configuration

Create next-sitemap.config.js at the project root:

// next-sitemap.config.js
/** @type {import('next-sitemap').IConfig} */
module.exports = {
  siteUrl: process.env.NEXT_PUBLIC_SITE_URL,
  generateRobotsTxt: true,
  exclude: [
    '/order',
    '/order*',
    '/sign-in',
    '/sign-in*',
    '/sign-up',
    '/sign-up*',
    '/forgot-password',
    '/forgot-password*',
    '/sso-callback',
    '/sso-callback*',
    '/icon*.png',
    '/icon*.svg',
    '/apple-icon*',
    '/favicon.ico',
    '/opengraph-image*',
    '*.json',
  ],
  additionalPaths: async () => {
    return [
      {
        loc: '/blog',
        changefreq: 'daily',
        priority: 0.7,
        lastmod: new Date().toISOString(),
      },
    ];
  },
};

This is Plainform's actual configuration. A few things worth noting:

  • generateRobotsTxt: true — automatically generates public/robots.txt referencing the sitemap
  • exclude — auth pages, the order confirmation page, and generated assets are excluded. Pages that require authentication should never appear in a sitemap
  • additionalPaths — adds paths that next-sitemap might not discover automatically from the file system, like a blog index route that is dynamically rendered

Gitignoring generated files

The generated sitemap and robots.txt should not be committed to source control — they are build artifacts regenerated on every deploy:

/public/sitemap*.xml
/public/robots.txt

They will be present in your deployed environment (generated by the postbuild step) but not cluttering your git history.

Dynamic Sitemaps for Content Pages

Static sitemaps generated at build time work well for most sites. But for frequently updated content — blog posts published after the build — you may want a server-side sitemap that always reflects the current state of your content.

Server-side sitemap with next-sitemap

next-sitemap supports server-side sitemaps via a route handler:

// app/server-sitemap.xml/route.ts
import { getServerSideSitemap } from 'next-sitemap';
import { blogSource } from '@/lib/source';

export async function GET() {
  const baseUrl = process.env.NEXT_PUBLIC_SITE_URL!;

  const posts = blogSource.getPages();

  const fields = posts.map((post) => ({
    loc: `${baseUrl}${post.url}`,
    lastmod: post.data.date
      ? new Date(post.data.date).toISOString()
      : new Date().toISOString(),
    changefreq: 'monthly' as const,
    priority: 0.6,
  }));

  return getServerSideSitemap(fields);
}

Register this in next-sitemap.config.js as an additional sitemap:

module.exports = {
  siteUrl: process.env.NEXT_PUBLIC_SITE_URL,
  generateRobotsTxt: true,
  exclude: ['/server-sitemap.xml'],
  robotsTxtOptions: {
    additionalSitemaps: [
      `${process.env.NEXT_PUBLIC_SITE_URL}/server-sitemap.xml`,
    ],
  },
};

The main sitemap.xml handles static routes. The server-sitemap.xml handles dynamic content and is always up to date.

Excluding Pages That Should Not Be Indexed

Not everything should appear in your sitemap. A few categories to always exclude:

Auth pages

Sign-in, sign-up, forgot password, and SSO callback routes should never be indexed. They serve no purpose in search results and can create confusing entries.

exclude: [
  '/sign-in', '/sign-in*',
  '/sign-up', '/sign-up*',
  '/forgot-password', '/forgot-password*',
  '/sso-callback', '/sso-callback*',
],

Post-purchase and session-dependent pages

Pages like order confirmation that require a valid session parameter in the URL should also be excluded. The URL only makes sense with a specific session_id attached — a generic entry in a sitemap is useless and potentially confusing.

Private or gated content

If your app has pages only accessible to logged-in users, exclude them from the sitemap. They cannot be crawled anyway, and including them wastes your crawl budget.

Excluding via page metadata

For pages that are in the app directory and not explicitly excluded from the config, you can also set robots in the page metadata to prevent indexing:

// app/(auth)/sign-in/[[...sign-in]]/page.tsx
export const metadata = {
  robots: {
    index: false,
    follow: false,
  },
};

This sets <meta name="robots" content="noindex, nofollow"> in the page head, which tells search engines not to index the page even if they find it via a link.

Generating robots.txt

A robots.txt file tells search crawlers which parts of your site they are allowed to access and references your sitemap. With next-sitemap, it is generated automatically when generateRobotsTxt: true is set.

The default output looks like:

# *
User-agent: *
Allow: /

# Host
Host: https://yoursite.com

# Sitemaps
Sitemap: https://yoursite.com/sitemap.xml

You can customize it in next-sitemap.config.js:

module.exports = {
  siteUrl: process.env.NEXT_PUBLIC_SITE_URL,
  generateRobotsTxt: true,
  robotsTxtOptions: {
    policies: [
      {
        userAgent: '*',
        allow: '/',
        disallow: ['/api/', '/order', '/sign-in', '/sign-up'],
      },
    ],
    additionalSitemaps: [
      `${process.env.NEXT_PUBLIC_SITE_URL}/server-sitemap.xml`,
    ],
  },
};

Built-in sitemap.ts vs. next-sitemap

Both approaches generate a valid sitemap. The right choice depends on your situation.

Use the built-in sitemap.ts when

  • You want zero additional dependencies
  • Your content is fully known at build time or dynamically fetched at request time
  • You need the sitemap to always reflect the current state (server-rendered)
  • You are building a smaller site with a manageable number of routes

Use next-sitemap when

  • You want automatic discovery of all routes from the file system without listing them manually
  • You need robots.txt generated alongside the sitemap
  • You want a static sitemap file that loads instantly from the CDN
  • Your build pipeline already generates the full set of pages at build time

Plainform uses next-sitemap because it automatically discovers all static routes from the built output, generates robots.txt in the same step, and the sitemap content is consistent with what was deployed — no drift between the sitemap and the actual built pages.

Submitting Your Sitemap to Google

Generating the sitemap is only half the job. You also need to tell Google it exists.

  1. Go to Google Search Console
  2. Add and verify your domain
  3. Navigate to Sitemaps in the left sidebar
  4. Enter sitemap.xml and click Submit

Google will begin crawling the sitemap and indexing your pages. You can monitor indexing status, see which pages have been indexed, and spot any crawl errors from the Search Console interface.

If you have a server-side sitemap at /server-sitemap.xml, submit that separately as well.

Summary

Generating a Next.js sitemap comes down to two solid options:

  • Built-in sitemap.ts — zero dependencies, server-rendered if needed, works well for dynamic content
  • next-sitemap — automatic route discovery, generates robots.txt, static files served from CDN

Either way, always:

  • Exclude auth pages, post-purchase pages, and private content
  • Set robots: { index: false } on pages that should not be crawled
  • Gitignore generated sitemap and robots.txt files
  • Submit your sitemap to Google Search Console after deployment

A properly configured sitemap ensures search engines can discover every public page on your site without guessing.

Comments on this page

Leave comment

Stay up to date with our latest product updates. Unsubscribe anytime!

2026 © All rights reserved