Adding lastmod Accurately From CMS Timestamps
A lastmod that advances on every deploy is worse than no lastmod at all, because it teaches crawlers to ignore the one field that would otherwise tell them what to recrawl.
When to Use This Approach
Fix lastmod when:
- Every URL in your sitemap shares the same timestamp, which means it is the build time.
- Updated pages are recrawled no faster than untouched ones.
- The CMS
updatedAtfield advances when an editor changes a tag, an internal note, or a workflow state.
What Counts as a Change
The CMS knows when a record was written. It does not know whether the write changed anything a reader would notice, and that gap is the whole problem.
Implementation Steps
Step 1: Define the content fields explicitly
// lib/content-fields.js — per type, the fields a reader can see
export const CONTENT_FIELDS = {
article: ['title', 'standfirst', 'body', 'hero', 'author', 'embeds'],
product: ['title', 'description', 'images', 'price', 'availability', 'specs'],
category: ['title', 'introCopy', 'heroImage'],
};
export const EXCLUDED_ALWAYS = ['tags', 'workflowState', 'internalNotes', 'reviewer', 'seoScore'];
Validation: Ask an editor to change a field you excluded and confirm the sitemap timestamp does not move. That is the test that matters, and it is worth running with a real editor rather than in code.
Step 2: Compute a content hash
// lib/content-hash.js
import { createHash } from 'node:crypto';
import { CONTENT_FIELDS } from './content-fields';
export function contentHash(entry) {
const fields = CONTENT_FIELDS[entry.type] ?? [];
const payload = fields
.map((f) => `${f}=${stableStringify(entry[f])}`)
.join('�');
return createHash('sha256').update(payload).digest('hex').slice(0, 16);
}
function stableStringify(v) {
if (v === null || v === undefined) return '';
if (Array.isArray(v)) return v.map(stableStringify).join('');
if (typeof v === 'object') {
return Object.keys(v).sort().map((k) => `${k}:${stableStringify(v[k])}`).join('');
}
return String(v);
}
SEO impact: A save that changes nothing visible produces an identical hash, so the timestamp holds and the crawler is not asked to revisit an unchanged page.
Validation: Save an entry twice with no edits and confirm the hash is stable.
Step 3: Persist the timestamp when the hash changes
// On publish — the only place lastmod is written
export async function recordPublish(entry) {
const hash = contentHash(entry);
const previous = await store.get(`hash:${entry.id}`);
if (previous === hash) return; // nothing visible changed
const now = new Date().toISOString();
await store.set(`hash:${entry.id}`, hash);
await store.set(`lastmod:${entry.id}`, now);
}
SEO impact: The stored value is the moment the content genuinely changed, independent of how many times it was saved or rebuilt afterwards.
Validation: The stored lastmod for an untouched entry should survive a full rebuild unchanged.
Step 4: Roll up referenced content
A page changes when anything rendered on it changes, including a shared component or a referenced asset.
// lib/effective-lastmod.js
export async function effectiveLastmod(entry) {
const own = await store.get(`lastmod:${entry.id}`);
const refs = await Promise.all(
collectReferences(entry).map((r) => store.get(`lastmod:${r.id}`)),
);
return [own, ...refs.filter(Boolean)].sort().at(-1);
}
SEO impact: Replacing an image used on forty product pages correctly advances all forty timestamps, which a field-level check on the product record alone would miss.
Validation: Update a shared asset and confirm every page that renders it moves.
Step 5: Emit the correct format
// app/sitemap.js
export default async function sitemap() {
const entries = await listIndexable();
return Promise.all(entries.map(async (e) => ({
url: `https://example.com${e.path}`,
lastModified: new Date(await effectiveLastmod(e)), // W3C datetime with offset
})));
}
<!-- The emitted form -->
<url>
<loc>https://example.com/blog/my-post/</loc>
<lastmod>2026-07-14T09:31:00+00:00</lastmod>
</url>
Validation: curl -s https://example.com/sitemap.xml | grep -oP '(?<=<lastmod>)[^<]+' | sort -u | wc -l should return a large number. A count of one means every URL shares a timestamp, which is the build-time failure.
Step 6: Audit accuracy against real recrawls
# Pair declared lastmod with the crawl date Search Console reports
node inspect-batch.js sample.txt \
| jq -r '.[] | [.url, .lastCrawlTime] | @tsv' > crawled.tsv
curl -s https://example.com/sitemap.xml \
| grep -oP '(?<=<loc>)[^<]+|(?<=<lastmod>)[^<]+' \
| paste - - > declared.tsv
join -t$'\t' <(sort declared.tsv) <(sort crawled.tsv)
Validation: Pages with a recent lastmod should show a more recent crawl than untouched ones. If there is no relationship, the signal is not being trusted yet.
Failure Modes and Their Signature
Each way of getting lastmod wrong has a recognisable shape in the sitemap itself, which makes diagnosis a one-command job.
SEO Impact Summary
The signal is a trust relationship: accuracy earns targeted recrawls, and inaccuracy costs the field entirely for far longer than it took to break.
| Signal | What improves | What breaks if misconfigured |
|---|---|---|
| Recrawl targeting | Changed pages are revisited sooner than unchanged ones | A build timestamp makes every page look equally stale |
| Crawl efficiency | Budget concentrates on genuinely updated content | Metadata-triggered updates spend budget on unchanged pages |
| Freshness in results | Updated content surfaces faster after a change | Static dates mean genuine updates go unnoticed for weeks |
| Signal credibility | A trusted field keeps working for future changes | Once ignored, accuracy alone does not immediately restore trust |
Measurable signals to watch:
- Distinct
lastmodvalues in the sitemap, which should approximate the number of distinct edit events. - Median time from publish to recrawl for changed pages versus unchanged ones.
- Correlation between declared
lastmodand reported last crawl time across a sample.
Edge Cases and Gotchas
Do not advance lastmod for a template change Redesigning the article template changes every page’s HTML but not its content. Advancing every timestamp asks for a full-site recrawl and consumes the budget you were trying to direct.
Timezone offsets are required, not optional A bare date is accepted but loses precision, and a datetime without an offset is ambiguous. Emit a full W3C datetime with an explicit offset.
A future lastmod is discarded Clock skew or a scheduled-publish timestamp written ahead of time can produce a value in the future, which is ignored. Clamp to the current time when writing.
The sitemap must regenerate when a timestamp changes An accurate stored value is useless if the sitemap is rebuilt nightly and the change happened at noon. Trigger regeneration from the publish webhook, as in automating sitemap regeneration on CMS publish.
Frequently Asked Questions
Does Google actually use lastmod? Yes, when it is consistently accurate — Google uses it to schedule recrawls and equally ignores it on sites where it is unreliable. A sitemap where every URL shares the deploy timestamp teaches Google to disregard the field, and that judgement persists after you fix it.
Should a typo fix update lastmod? Yes; it is a content change however small. The distinction is kind, not size: any edit to what a reader sees counts, while workflow transitions and internal notes do not. A content hash makes that judgement automatic.
What should lastmod be for a listing page? The most recent modification among the entries currently shown, not the generation time. A category whose top items have not changed has not changed, and advancing it every build is the fastest way to make the whole sitemap untrustworthy.
Is it better to omit lastmod than to guess it? Yes, unambiguously. An absent field is neutral; an inaccurate one is actively harmful because it trains crawlers to ignore your sitemap. If you cannot compute it accurately yet, leave it out until you can.
Part of: XML Sitemap Generation for Headless
Related
- Automating Sitemap Regeneration on CMS Publish — making the file reflect the timestamp the moment it changes
- Splitting Large Sitemaps With a Sitemap Index — the index-level
lastmodthat summarises each child file - Crawl Budget Impact in Headless — why accurate recrawl targeting is worth the implementation effort