The lastmod field tells Google when content actually changed — it's the strongest re-crawl priority signal you have. Done right (reflecting real content modification timestamps), Google re-crawls updated pages within hours. Done wrong (every URL showing today because the file regenerates daily, or stuck in the past), Google learns to ignore your sitemap dates entirely. This guide covers correct ISO 8601 format, sourcing real modification dates, and why priority and changefreq are dead weight.
curl -s https://example.com/sitemap.xml | \ grep -E "<loc>|<lastmod>" | \ head -40 # Spot-check 5-10 URLs. Open each URL in a browser. # Does lastmod match when you last actually edited that content?
# Count unique lastmod values curl -s https://example.com/sitemap.xml | \ grep -oE '<lastmod>[^<]+</lastmod>' | \ sort -u | head -10 # If you see one or two unique dates and they're recent — the file regenerates with current time on every URL
Required format. Other formats parse incorrectly or get rejected:
<!-- Date only (recommended for most cases) --> <lastmod>2024-01-15</lastmod> <!-- Date with time and timezone (for high-frequency updates) --> <lastmod>2024-01-15T14:30:00+00:00</lastmod> <lastmod>2024-01-15T14:30:00Z</lastmod>
<!-- BAD: US format --> <lastmod>01/15/2024</lastmod> <!-- BAD: Unix timestamp --> <lastmod>1705325400</lastmod> <!-- BAD: no timezone with time component --> <lastmod>2024-01-15T14:30:00</lastmod> <!-- BAD: human format --> <lastmod>January 15, 2024</lastmod>
Use post_modified (last edit date) not post_date (publish date):
// In a custom sitemap generator
$args = [
'post_type' => ['page', 'post'],
'post_status' => 'publish',
];
$query = new WP_Query($args);
while ($query->have_posts()) {
$query->the_post();
echo "<url>";
echo "<loc>" . get_permalink() . "</loc>";
echo "<lastmod>" . get_the_modified_date('Y-m-d') . "</lastmod>";
echo "</url>";
}
// Yoast / Rank Math handle this correctly by default
-- Source from updated_at column SELECT slug, updated_at -- NOT created_at FROM pages WHERE status = 'published' ORDER BY updated_at DESC;
# Hugo
{{ range .Site.RegularPages }}
<url>
<loc>{{ .Permalink }}</loc>
<lastmod>{{ .Lastmod.Format "2006-01-02" }}</lastmod>
</url>
{{ end }}
# Hugo .Lastmod sources from front-matter or git history
# Next.js
export default async function sitemap() {
const posts = await getPosts();
return posts.map(post => ({
url: `https://example.com/${post.slug}`,
lastModified: post.updatedAt, // Date object or ISO string
}));
}
// @astrojs/sitemap reads from page front-matter
// Set updatedDate in content collections schema
import { defineCollection, z } from 'astro:content';
const blog = defineCollection({
schema: z.object({
title: z.string(),
publishDate: z.date(),
updatedDate: z.date().optional(),
})
});
// Sitemap auto-uses updatedDate || publishDate
// BAD: every URL gets the current timestamp
const sitemap = urls.map(url => ({
loc: url,
lastmod: new Date().toISOString() // ← every URL gets "now"
}));
// RIGHT: lastmod from each URL's actual modification record
const sitemap = posts.map(post => ({
loc: `https://example.com/${post.slug}`,
lastmod: post.updatedAt.toISOString() // ← from database
}));
Google publicly stated (2017) it ignores both. Bing has similar policy. The fields were over-abused:
priority=1.0 on every URL hoping for ranking boostchangefreq=daily on URLs that change yearly<!-- BEFORE --> <url> <loc>https://example.com/about</loc> <lastmod>2024-01-15</lastmod> <changefreq>monthly</changefreq> <priority>0.8</priority> </url> <!-- AFTER --> <url> <loc>https://example.com/about</loc> <lastmod>2024-01-15</lastmod> </url>
Smaller files, less crawl bandwidth, same effect.
<url>
<loc>https://example.com/products/widget</loc>
<lastmod>2024-01-15</lastmod>
<image:image xmlns:image="http://www.google.com/schemas/sitemap-image/1.1">
<image:loc>https://example.com/images/widget.jpg</image:loc>
<image:caption>Blue ceramic widget</image:caption>
</image:image>
</url>
<url xmlns:news="http://www.google.com/schemas/sitemap-news/0.9">
<loc>https://example.com/news/article-1</loc>
<news:news>
<news:publication>
<news:name>Example News</news:name>
<news:language>en</news:language>
</news:publication>
<news:publication_date>2024-01-15T14:30:00+00:00</news:publication_date>
<news:title>Article title</news:title>
</news:news>
</url>
<!-- News sitemap entries auto-removed after 2 days -->
Yoast caches sitemap output. If posts edit but cache hasn't expired, lastmod stays old. Force regeneration:
// functions.php
add_action('save_post', function($post_id) {
// Trigger Yoast cache flush on every save
if (function_exists('wpseo_xml_sitemaps_init')) {
do_action('wpseo_ping_search_engines');
}
});
Default WordPress sitemap (since 5.5) uses post_modified which is correct. But if you've manually touched post_modified (via filter or update_post_meta) without actual content change, dates lie. Stick to letting WordPress manage it.
# Hugo example - WRONG
{{ now.Format "2006-01-02" }} <!-- always current build time -->
# RIGHT - source from front-matter
{{ .Lastmod.Format "2006-01-02" }} <!-- from content file -->