Next.js Sitemap Not Updating? Here's the Real Fix
If your Next.js sitemap is not updating after you publish new content, you're dealing with a cache-coherence bug that almost nobody writes up. It has an exact symptom, a reproducible root cause, and a one-line fix. This is the guide you'll wish you had the moment you notice /sitemap.xml serving fewer entries than your real site.
The symptom: your sitemap lags behind your published content
The mismatch is impossible to miss once you look. On our own site, /lab lists 11 published posts, yet /sitemap.xml shows only 7. Same database, same deploy, two different answers. If that gap sounds familiar, you're in the right place.
You might have checked your afterChange hook, verified that revalidateTag('posts') fires, and even confirmed that the tagged data refreshes — only to find the sitemap still frozen. That's because the problem lives between two cache layers, not inside the data fetch.
Why revalidateTag doesn't fix a stale Next.js sitemap
The answer lies in what sitemap.ts actually is. According to the Next.js Metadata Files: sitemap.xml documentation, it's a special Route Handler. And like any Route Handler, Next.js caches its rendered output by default.
Here's what happened in our own repository (this bug is documented in a comment at the top of app/(frontend)/sitemap.ts because it cost real indexation time):
- We read content from Payload using
unstable_cache, tagged with the collection slugposts. - An
afterChangehook calledrevalidateTag('posts')whenever a post was published. - That call did work — it invalidated the inner
unstable_cachedata entry. - But the route's statically-rendered outer XML output was never re-run. The frozen route output kept serving the old XML built from the old data, long after the inner cache was refreshed.
Two cache layers. Tag-based revalidation busted the inner one, but the outer route handler cache was never told to re-execute. That's the missing piece.
The one-line fix: route-level ISR on the sitemap
Tell the sitemap route to re-render on a bounded interval:
// app/(frontend)/sitemap.ts
export const revalidate = 3600; // seconds — re-render at most once per hour
Enter fullscreen mode Exit fullscreen mode
That's it. With this line, the whole route re-executes after 3600 seconds and always picks up newly published documents. The sitemap is crawler-only traffic, so an hour of staleness is irrelevant. Even better, the bounded interval deduplicates the Payload read to at most once per hour, so you're not hammering the database on every bot request.
Once route-level ISR is in place, the inner unstable_cache becomes redundant. The route itself is now invalidating on a schedule, so you can drop the wrapper and simplify your data fetching.
Drop the inner cache after adding revalidate
Before, your sitemap might have looked like this:
import { unstable_cache } from 'next/cache';
import { getPayloadPosts } from '@/lib/payload';
export async function GET() {
const posts = await unstable_cache(
getPayloadPosts,
['sitemap-posts'],
{ tags: ['posts'] }
)();
// generate XML…
}
Enter fullscreen mode Exit fullscreen mode
After setting export const revalidate = 3600, you can remove the unstable_cache wrapper entirely:
export const revalidate = 3600;
export async function GET() {
const posts = await getPayloadPosts();
// generate XML…
}
Enter fullscreen mode Exit fullscreen mode
The route-level ISR already ensures the handler re-runs periodically, and on-demand revalidation can still be triggered with revalidatePath('/sitemap.xml') from your publish hook if you need an instant refresh. No more stale XML.
The general lesson: tag-based revalidation invalidates data, not rendered output
This bug taught us a rule that applies to every part of Next.js where you stack a data cache inside a route cache:
revalidateTag clears your data, not the route that renders it. If the route itself is cached (static generation, full route cache, etc.), invalidating inner data leaves the outer rendered output untouched.
Whenever you have both layers, you must either:
- set route-level
revalidateto give the outer layer a time bound, or - call
revalidatePathon the route itself to manually bust the rendered output.
This is the piece that the revalidateTag documentation doesn't spell out, and it's the difference between a sitemap that stays fresh and one that silently sabotages your indexation.
After fixing your sitemap, tell search engines about the new content immediately — our IndexNow integration for Next.js pushes updates the moment you publish.
Struggling with caching or Next.js performance in general? Our team builds products where this stuff is ironed out before it bites you — start a project and we'll make sure your site's infrastructure stays boringly reliable.
FAQ
Why is my Next.js sitemap not updating even though revalidateTag works?
revalidateTag only clears data caches tagged by unstable_cache. Your sitemap route's rendered output is cached separately as a static route handler. To update the sitemap, you need to either revalidate the path with revalidatePath('/sitemap.xml') or set route-level ISR with export const revalidate.
Should I use unstable_cache in my sitemap route after adding revalidate?
No. After setting revalidate on the route, Next.js will re-render the whole handler periodically, making the inner data cache redundant. Remove unstable_cache to simplify.
What's the recommended revalidate interval for a sitemap?
An hour (3600 seconds) is safe. Sitemaps are consumed by crawlers sporadically, so a small delay won't hurt indexation, while it protects your database from unnecessary reads.
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.