Skip to content

Sitemap Generation

A sitemap lists every URL on your site that you want search engines to discover, along with metadata like how often it changes. This lesson covers generating one automatically with the sitemap.ts convention.

sitemap.ts: A Sitemap as Code

Creating a file named sitemap.ts directly inside app/ and exporting a default function that returns an array of URL entries is all it takes — Next.js automatically serves the result as a properly formatted sitemap.xml at /sitemap.xml, with no manual XML writing required.

Because it's a regular TypeScript function, it can fetch data — meaning a sitemap for a blog can include every published post's URL dynamically, staying in sync with your actual content without manual updates.

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

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

Visiting /sitemap.xml now returns a properly formatted XML sitemap generated from this array.

Sitemap Entry Fields

{
  url: string;
  lastModified?: string | Date;
  changeFrequency?: "always" | "hourly" | "daily" | "weekly" | "monthly" | "yearly" | "never";
  priority?: number; // 0.0 to 1.0
}
  • url is the only required field; it must be the fully-qualified URL, including the domain.
  • changeFrequency is a hint to crawlers about how often content typically changes.
  • priority is a relative hint (0 to 1) about a page's importance compared to others in the sitemap.
  • Search engines treat these fields as suggestions, not guarantees, but they're still worth setting accurately.

Sitemap Cheat Sheet

Key facts about the sitemap.ts convention.

Question Answer
File location app/sitemap.ts
Resulting URL /sitemap.xml
Can it be async? Yes, for fetching dynamic content
Required field per entry url
Multiple sitemaps for large sites Supported via a generateSitemaps export

Building a Sitemap from Dynamic Content

For a blog or e-commerce site, combine static entries (home, about, contact) with dynamically generated entries for every post or product, fetched the same way you would in any Server Component.

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

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const posts = await getAllPosts();

  const postEntries = posts.map((post: any) => ({
    url: `https://example.com/blog/${post.slug}`,
    lastModified: post.updatedAt,
  }));

  return [
    { url: "https://example.com", lastModified: new Date() },
    ...postEntries,
  ];
}

Splitting Very Large Sitemaps

Sitemaps have a practical size limit (traditionally 50,000 URLs per file). For very large sites, Next.js supports generating multiple sitemap files via a generateSitemaps export, which Next.js then indexes automatically.

Common Mistakes

  • Using relative URLs instead of fully-qualified URLs (including the domain) in sitemap entries.
  • Forgetting to update lastModified when content actually changes, reducing the sitemap's usefulness.
  • Including URLs that are blocked by robots.txt, which is contradictory to search engines.
  • Not paginating extremely large sitemaps, risking exceeding practical size limits.

Key Takeaways

  • app/sitemap.ts generates /sitemap.xml automatically from an exported function.
  • The function can be async, allowing dynamic entries built from real content.
  • Only url is required per entry; lastModified, changeFrequency, and priority are optional hints.
  • Very large sites can split sitemaps using a generateSitemaps export.
  • Keep sitemap entries and robots.txt rules consistent with each other.

Pro Tip

Regenerate lastModified values from your actual content's update timestamps (not just new Date() at build time) — an accurate lastModified gives search engines a genuinely useful signal about which pages have meaningfully changed since their last crawl.