Type something to search...
Next.js sitemap.xml

Next.js sitemap.xml

A sitemap is how you hand search engines a structured, authoritative list of every URL on your site worth crawling — rather than relying entirely on link-following discovery, which can miss pages that aren't well-linked internally. Next.js supports the Sitemaps XML format as a first-class file convention, with the same static-file-or-generated-code duality as its other metadata conventions, plus a specific mechanism for splitting a sitemap across multiple files once a site grows past what one file can reasonably hold.

Static Sitemap

For smaller sites, a literal sitemap.xml at the root of app works exactly as you'd expect from any standard sitemap:

<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>https://acme.com</loc>
    <lastmod>2023-04-06T15:02:24.021Z</lastmod>
    <changefreq>yearly</changefreq>
    <priority>1</priority>
  </url>
  <url>
    <loc>https://acme.com/about</loc>
    <lastmod>2023-04-06T15:02:24.021Z</lastmod>
    <changefreq>monthly</changefreq>
    <priority>0.8</priority>
  </url>
</urlset>

Generating a Sitemap with Code

sitemap.js or sitemap.ts can default-export a function returning an array of URL entries, which Next.js serializes into the same XML shape automatically:

import type { MetadataRoute } from "next";

export default function sitemap(): MetadataRoute.Sitemap {
  return [
    {
      url: "https://acme.com",
      lastModified: new Date(),
      changeFrequency: "yearly",
      priority: 1,
    },
    {
      url: "https://acme.com/about",
      lastModified: new Date(),
      changeFrequency: "monthly",
      priority: 0.8,
    },
    {
      url: "https://acme.com/blog",
      lastModified: new Date(),
      changeFrequency: "weekly",
      priority: 0.5,
    },
  ];
}

This is where a sitemap genuinely earns the "generate it with code" approach over a hand-maintained static file: a blog with hundreds of posts, a product catalog with thousands of SKUs — anything where the URL list changes as often as your content does — should build this array from your actual data source (a database query, a CMS API call) rather than someone remembering to manually update a static XML file every time a page is published. Like the other metadata-file conventions, sitemap.js is a special Route Handler, cached by default unless it reaches for a Request-time API or dynamic config.

Image and Video Sitemaps

Beyond plain URLs, individual entries can carry images and videos arrays, which Google's crawlers use specifically for image and video search indexing — a meaningfully different (and often overlooked) discovery surface from ordinary web search:

export default function sitemap(): MetadataRoute.Sitemap {
  return [
    {
      url: "https://example.com",
      lastModified: "2021-01-01",
      changeFrequency: "weekly",
      priority: 0.5,
      images: ["https://example.com/image.jpg"],
    },
  ];
}
export default function sitemap(): MetadataRoute.Sitemap {
  return [
    {
      url: "https://example.com",
      lastModified: "2021-01-01",
      changeFrequency: "weekly",
      priority: 0.5,
      videos: [
        {
          title: "example",
          thumbnail_loc: "https://example.com/image.jpg",
          description: "this is the description",
        },
      ],
    },
  ];
}

If a meaningful share of your traffic potential comes from image or video search rather than standard web search — a recipe site, a video tutorial platform — populating these fields is a genuinely underused lever most teams never get around to, precisely because it's easy to forget these fields exist at all when just listing plain URLs already satisfies the basic sitemap requirement.

Localized Sitemaps

For internationalized sites, alternates.languages lets a single URL entry declare its translated counterparts, generating the xhtml:link hreflang tags search engines use to serve the right language variant to the right audience:

export default function sitemap(): MetadataRoute.Sitemap {
  return [
    {
      url: "https://acme.com",
      lastModified: new Date(),
      alternates: {
        languages: {
          es: "https://acme.com/es",
          de: "https://acme.com/de",
        },
      },
    },
  ];
}

This produces <xhtml:link rel="alternate" hreflang="es" .../> and its German counterpart alongside the primary URL entry — a single, coherent way to declare a page's full set of language variants directly in your sitemap, rather than relying solely on hreflang tags scattered across individual page <head> elements.

Splitting a Sitemap Across Multiple Files

A single sitemap file works fine until a site's URL count approaches search engines' per-file limits — Google specifically caps sitemaps at 50,000 URLs each. There are two supported ways to split beyond that:

Nesting sitemap.(xml|js|ts) in multiple route segmentsapp/sitemap.xml alongside app/products/sitemap.xml, each covering its own section.

Using generateSitemaps for programmatic splitting within a single route, which is the better fit when the split needs to be data-driven rather than structurally tied to your route hierarchy:

import type { MetadataRoute } from "next";
import { BASE_URL } from "@/app/lib/constants";

export async function generateSitemaps() {
  // Fetch the total number of products and calculate the number of sitemaps needed
  return [{ id: 0 }, { id: 1 }, { id: 2 }, { id: 3 }];
}

export default async function sitemap(props: {
  id: Promise<string>;
}): Promise<MetadataRoute.Sitemap> {
  const id = await props.id;
  // Google's limit is 50,000 URLs per sitemap
  const start = id * 50000;
  const end = start + 50000;
  const products = await getProducts(
    `SELECT id, date FROM products WHERE id BETWEEN ${start} AND ${end}`,
  );
  return products.map((product) => ({
    url: `${BASE_URL}/product/${product.id}`,
    lastModified: product.date,
  }));
}

generateSitemaps returns an array of { id } objects up front — effectively declaring how many sitemap "chunks" exist — and the sitemap function is then invoked once per id, receiving it as a promise, to generate that specific chunk's content. The resulting files are served at /.../sitemap/[id].xml — in this example, /product/sitemap/1.xml and so on, one file per generated id.

The Full Return Shape

type Sitemap = Array<{
  url: string;
  lastModified?: string | Date;
  changeFrequency?:
    "always" | "hourly" | "daily" | "weekly" | "monthly" | "yearly" | "never";
  priority?: number;
  alternates?: { languages?: Languages<string> };
  images?: string[];
  videos?: Videos[];
}>;

Every field beyond url is optional, but lastModified and changeFrequency in particular are worth setting deliberately rather than omitting — they're the fields search engines actually use to decide how aggressively to re-crawl a given URL, and an accurate changeFrequency can meaningfully affect how quickly updated content gets re-indexed.

Version History

VersionChanges
v16.0.0id (in generateSitemaps-driven sitemaps) became a promise resolving to a string
v14.2.0Localization support added
v13.4.14changeFrequency and priority attributes added
v13.3.0sitemap introduced

Key Takeaways

AspectDetail
Static formapp/sitemap.xml, hand-written and served as-is
Dynamic formapp/sitemap.ts/.js, default-exporting a function returning an array of URL entries
Per-sitemap URL limit50,000 (Google's limit) — split beyond that via nested segments or generateSitemaps
Image/video sitemapsThe images/videos fields feed a separate, often-overlooked search discovery surface
Localizationalternates.languages generates hreflang link tags per URL entry
CachingCached by default, like every other metadata-file Route Handler

For most sites, a hand-maintained static sitemap.xml covers the need adequately. The moment your URL count is driven by content rather than fixed routes — blog posts, products, any growing dataset — generating it from your actual data source with sitemap.ts (and generateSitemaps once you cross the 50,000-URL ceiling) is the pattern that scales without anyone having to remember to update a static file by hand.

Tags :
Share :

Related Posts

Can Next.js Be Used with GraphQL?

Can Next.js Be Used with GraphQL?

Next.js and GraphQL are two powerful technologies that have gained significant traction in the web development community. Next.js, a React-based fram

Dive Deeper
How does Next.js differ from Create React App?

How does Next.js differ from Create React App?

In the world of modern web development, React.js has emerged as a dominant force due to its flexibility, performance, and extensive ecosystem. Two po

Dive Deeper
How does Next.js handle image optimization?

How does Next.js handle image optimization?

In modern web development, image optimization plays a critical role in enhancing user experience and improving site performance. Large, unoptimized i

Dive Deeper