Skip to main content
Back to Blog
4 min read

Sitemap stability: why lastmod should never be new Date()

A common Next.js sitemap bug: setting lastmod to the current date on every build. Google sees every page as 'updated' on every deploy, diluting your crawl priority. Here's the fix.

next.jsperformance

I've seen this pattern in at least five Next.js codebases:

// app/sitemap.ts
export default function sitemap() {
  return pages.map((page) => ({
    url: `https://example.com${page.path}`,
    lastModified: new Date(), // ← The bug
  }))
}

Every build, every page gets today's date as lastModified. Deploy on Monday — Google thinks all 50 pages changed on Monday. Deploy on Tuesday — Google thinks all 50 pages changed on Tuesday.

This isn't just wrong — it actively hurts your SEO.

Why it matters

Google uses lastmod as a signal for crawl priority. When you tell Google a page was modified, it prioritizes re-crawling that page. If every page claims to be modified on every deploy, Google learns that your lastmod values are unreliable and starts ignoring them.

From Google's documentation: "We ignore <lastmod> values that are clearly inaccurate (for example, if you set all dates to the current date)."

Once Google stops trusting your lastmod, you lose the ability to signal which pages actually changed. A real content update — a new blog post, an updated pricing page — gets the same crawl priority as everything else.

The correct approach

lastmod should reflect when the content of that page actually changed. For different page types:

Static pages (about, contact, services)

These change rarely. Set lastmod to the date you last edited them, as a static string:

{
  url: 'https://example.com/about',
  lastModified: '2026-06-15', // Last time you actually edited this page
}

Or better: derive it from the file's git history:

import { execSync } from 'child_process'

function getLastModified(filePath: string): string {
  const date = execSync(
    `git log -1 --format=%cI -- ${filePath}`
  ).toString().trim()
  return date || new Date().toISOString()
}

// In sitemap.ts
{
  url: 'https://example.com/about',
  lastModified: getLastModified('app/about/page.tsx'),
}

This gives the actual date the file was last committed — accurate and automatic.

Blog posts

Blog posts have a date field in their frontmatter. Use it:

const posts = getAllPosts('en')

const blogEntries = posts.map((post) => ({
  url: `https://example.com/en/blog/${post.slug}`,
  lastModified: post.date, // From frontmatter
}))

If you update a blog post after initial publication, add a lastUpdated field to the frontmatter and use that instead:

lastModified: post.lastUpdated || post.date,

Dynamic pages (e-commerce products, user-generated content)

For pages backed by a database, store an updatedAt timestamp and use it:

const products = await db.products.findMany({
  select: { slug: true, updatedAt: true },
})

const productEntries = products.map((product) => ({
  url: `https://example.com/products/${product.slug}`,
  lastModified: product.updatedAt,
}))

Pages you can't date

For pages where you genuinely don't know when the content last changed: omit lastmod entirely. An absent lastmod is better than a wrong one. Google will use its own crawl data to determine freshness.

{
  url: 'https://example.com/some-page',
  // No lastModified — and that's fine
}

The full sitemap pattern

Here's the sitemap pattern I use on client projects:

// app/sitemap.ts
import { getAllPosts } from '@/lib/blog'

export default function sitemap() {
  const staticPages = [
    { url: 'https://example.com', lastModified: '2026-07-01' },
    { url: 'https://example.com/about', lastModified: '2026-06-15' },
    { url: 'https://example.com/services', lastModified: '2026-07-10' },
    // Update these dates when you edit the pages
  ]

  const posts = getAllPosts('en')
  const blogPages = posts.map((post) => ({
    url: `https://example.com/en/blog/${post.slug}`,
    lastModified: post.date,
  }))

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

Simple. Each page has an accurate date. No new Date() anywhere.

The deployment trap

The reason new Date() is so common: it's what you'd write if you think "the page was built just now, so it was modified just now." But building and modifying are different things.

A deploy rebuilds every page. That doesn't mean every page's content changed. The HTML might be byte-identical to yesterday's build. lastmod should track content changes, not build timestamps.

How to check

After deploying, fetch your sitemap and inspect the dates:

curl -s https://yoursite.com/sitemap.xml | head -30

If every <lastmod> shows today's date — you have the bug. If dates vary (some old, some recent) — you're doing it right.

I caught this exact bug on a client project where the sitemap was returning HTML instead of XML. The lastmod bug is subtler — the sitemap works, it just lies to Google.

The one-line fix

If you're currently using new Date() and don't have time for the full fix: just remove lastModified from entries where you don't have an accurate date.

// Before (wrong)
{ url: '...', lastModified: new Date() }

// After (correct, if you don't know the real date)
{ url: '...' }

Omitting lastmod is better than lying. Google uses its own crawl history as a fallback, which is more accurate than a build timestamp.


Want your technical SEO done right from day one? Let's talk — I catch these silent SEO bugs before they cost you crawl budget.