Fix Your Sitemap for Next.js

Updated April 2026·By SitemapFixer Team

Next.js gives you two ways to ship a sitemap: the App Router's built-in sitemap.ts convention, or the next-sitemap package on Pages Router. Both work. Neither is zero-config for sites with dynamic routes, ISR, or i18n.

Analyze your Next.js sitemap nowTry Sitemap Fixer Free

The most common Next.js sitemap failure isn't a bug - it's silence. You ship a sitemap.ts that returns static routes only, forget to query the CMS for dynamic ones, and Google indexes exactly what you listed (usually the 5 marketing pages) while ignoring the 2,000 blog posts you actually want ranked.

Recently helped a SaaS company running Next.js 14 App Router with 8,600 programmatic "compare X vs Y" pages. Their sitemap had 12 URLs. The sitemap.ts file listed the static marketing pages and missed the dynamic route entirely because the dev had never added the DB query. After fixing, GSC went from 9 indexed pages to 6,100 in about six weeks.

App Router sitemap.ts - the good way

// app/sitemap.ts
import type { MetadataRoute } from 'next';
import { db } from '@/lib/db';

export const revalidate = 3600; // rebuild hourly

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const staticPages = [
    { url: 'https://example.com', lastModified: new Date(), priority: 1 },
    { url: 'https://example.com/about', lastModified: new Date() },
    { url: 'https://example.com/pricing', lastModified: new Date() },
  ];

  const posts = await db.post.findMany({
    where: { published: true, publishedAt: { lte: new Date() } },
    select: { slug: true, updatedAt: true },
  });

  const blogPages = posts.map((p) => ({
    url: `https://example.com/blog/${p.slug}`,
    lastModified: p.updatedAt,
    changeFrequency: 'weekly' as const,
    priority: 0.7,
  }));

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

Sharding past 50k URLs

// app/sitemap.ts
export async function generateSitemaps() {
  const count = await db.post.count();
  const shards = Math.ceil(count / 40000);
  return Array.from({ length: shards }, (_, id) => ({ id }));
}

export default async function sitemap({ id }: { id: number }) {
  const posts = await db.post.findMany({
    skip: id * 40000,
    take: 40000,
    orderBy: { id: 'asc' },
  });
  return posts.map(p => ({
    url: `https://example.com/blog/${p.slug}`,
    lastModified: p.updatedAt,
  }));
}

Next.js auto-generates /sitemap.xml as the index and /sitemap/0.xml, /sitemap/1.xml... as shards. Submit only the index URL to GSC.

Common Next.js Sitemap Issues

i18n alternates

// app/sitemap.ts - with hreflang
const locales = ['en', 'de', 'fr'];

return posts.flatMap(p => locales.map(loc => ({
  url: `https://example.com/${loc}/blog/${p.slug}`,
  lastModified: p.updatedAt,
  alternates: {
    languages: Object.fromEntries(
      locales.map(l => [l, `https://example.com/${l}/blog/${p.slug}`])
    ),
  },
})));

Vercel preview deploys and robots

Every Vercel preview URL (branch-name-team.vercel.app) is crawlable by default. Vercel adds X-Robots-Tag: noindex automatically on preview/development environments - verify with curl -I https://branch.vercel.app and look for the header. If you've customized vercel.json or removed defaults, you can accidentally expose previews. On custom Next.js deployments (not Vercel), add the header via middleware based on process.env.VERCEL_ENV !== 'production'.

Step-by-Step Fix Guide

  1. Create app/sitemap.ts (App Router) or install next-sitemap (Pages Router)
  2. Query every data source for live URLs (CMS, DB, headless content)
  3. Filter drafts and future-dated content at the query level
  4. Set revalidate so the sitemap rebuilds on a sane schedule (1-24 hours)
  5. For 50k+ URLs, use generateSitemaps() to shard
  6. Add alternates.languages for i18n routes
  7. Confirm Vercel preview URLs return X-Robots-Tag: noindex
  8. Verify with curl https://yoursite.com/sitemap.xml
  9. Submit the index URL to Google Search Console

Frequently Asked Questions

App Router sitemap.ts vs Pages Router next-sitemap - which?
On App Router (Next 13+), use the built-in sitemap.ts - it's part of Next.js core. On Pages Router, use the next-sitemap package, which auto-generates sitemap.xml at build time. Don't mix both in one codebase.
How do I split a Next.js sitemap over 50k URLs?
Use generateSitemaps() in sitemap.ts to return an array of shard IDs. Next.js will render each as /sitemap/0.xml, /sitemap/1.xml and expose a sitemap index at /sitemap.xml automatically.
Does Vercel ISR affect sitemap freshness?
Yes. If you use revalidate in sitemap.ts, the sitemap stays cached until the interval expires or you trigger on-demand revalidation. For frequently-changing catalogs, call revalidatePath('/sitemap.xml') after content changes.
Analyze your Next.js sitemap
Find all issues in your sitemap - free, no credit card needed
Analyze My Sitemap Free
Other platform guides