afaqrashid.com  ·  portfolio · 2026 ed.

Next.js sitemap.ts: Revalidate, lastmod and Splitting, Tested

A Next.js app/sitemap.ts is built once, when you run next build, unless you tell it otherwise. That one fact explains most sitemap problems: new posts that don't show up, and a lastmod that's the same on every URL.

I tested sitemap.ts, revalidate, alternates and generateSitemaps on a fresh Next.js 16.4.4 app with next build and next start. Here's what each one did.

Your sitemap doesn't update after you publish

My async sitemap function was marked as static in the build output:

└ ○ /sitemap.xml

So posts published after the deploy aren't in it until the next build. Add revalidate to rebuild it in the background:

// app/sitemap.ts
export const revalidate = 3600; // seconds

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const posts = await getPosts();
  return posts.map((post) => ({
    url: `https://example.com/blog/${post.slug}`,
    lastModified: post.updatedAt,
  }));
}

I set it to 5 seconds to watch it work. The first request after the window still got the old file and started a rebuild in the background. The request after that got the new one. That's normal stale-while-revalidate behaviour, and for a crawler that comes back regularly it's fine.

If your CMS can call a webhook on publish, revalidate the sitemap from a route handler instead. In my test the very next request after this ran got the new file:

// app/api/revalidate/route.ts
import { revalidatePath } from "next/cache";

export function POST() {
  revalidatePath("/sitemap.xml");
  return Response.json({ ok: true });
}

Protect that route with a secret your CMS sends, so strangers can't trigger it.

Every lastmod is the build time

The common shortcut is lastModified: new Date(). In a static sitemap, that's evaluated during the build. Every URL in my sitemap got the moment the build ran, and the next build moved them all again.

That makes lastmod useless. Google says it uses lastmod only if it's "consistently and verifiably" accurate. Use the date the content really changed, from your CMS or your data files, or leave lastModified out.

Don't spend time on priority and changeFrequency either. Next.js will output them, but Google says it ignores both.

hreflang alternates do work

If alternates seem missing, check the raw XML rather than a browser's pretty view. This entry:

{
  url: "https://example.com",
  alternates: {
    languages: { de: "https://example.com/de", "x-default": "https://example.com" },
  },
}

came out with the xhtml namespace on the urlset and one link per language:

<url>
<loc>https://example.com</loc>
<xhtml:link rel="alternate" hreflang="de" href="https://example.com/de" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://example.com" />
</url>

Every language version needs its own entry listing all the alternates, including itself. Use full URLs: a sitemap has no metadataBase to fill them in.

generateSitemaps doesn't make an index

One sitemap file can hold at most 50,000 URLs or 50MB. Past that, generateSitemaps splits it. I put one in app/products/sitemap.ts that returned ids 0 and 1:

/products/sitemap/0.xml   200
/products/sitemap/1.xml   200
/products/sitemap.xml     404

Next.js didn't create a sitemap index that lists them, so nothing points to these files yet. The simplest fix is to list them all in robots.ts, which accepts an array:

// app/robots.ts
export default function robots(): MetadataRoute.Robots {
  return {
    rules: { userAgent: "*", allow: "/" },
    sitemap: [
      "https://example.com/sitemap.xml",
      "https://example.com/products/sitemap/0.xml",
      "https://example.com/products/sitemap/1.xml",
    ],
  };
}

That printed one Sitemap: line per file. Submit the same URLs in Search Console so you can see each one's status.

Check the live file

curl -s https://example.com/sitemap.xml | head -20

Look for your newest page, a real lastmod, and your production domain in every loc. If you get a 404 or a login page instead, the robots.txt and sitemap.xml post below covers the causes.

Related

Sources