A static site generator builds every page at once, and a sitemap plugin with no better information writes the moment of the build into every lastmod. Deploy daily and every URL on the site claims to have changed today, forever. The sitemaps protocol defines lastmod as the date the page was last modified, so that file is false on every line, and the falseness has a shape the scan evidence shows. This post is about where a real date comes from when there is no server to ask, and how our own static pages and articles get theirs.
What the two checks want
A3, three points, wants the sitemap to exist, to parse to at least one URL, and to be referenced from robots.txt. D4, four points, reads the lastmod values from the first sitemap robots.txt declares: one point if any URL carries one, two points if at least 20% of the URLs in the file were modified in the last 90 days, one point for a machine-readable date on a crawled page. XML sitemaps for agents covers A3 and why the first Sitemap: line is the one that counts; Freshness signals goes through the D4 arithmetic in detail. This post takes both as read and stays on the generator.
The shape of a stamp
The D4 evidence carries three counts: sitemapUrls, withLastmod and modifiedLast90Days. A sitemap stamped at build time produces three identical numbers, because every URL has a date and every date is today. A single scan awards the points, since one observation cannot tell a busy site from a stamped one. Two can. Scan again after the next deploy and every date has moved by exactly the interval between deploys, on pages whose content did not change. A consumer holding a cached copy of a page learns the same thing from a lastmod that advances while the bytes stay the same, and what it learns is that this site's dates carry no information.
An absent lastmod says the site has not said. A stamped one says the site cannot be believed on this point. The second is the worse position to be in, and it is the default output of several tools.
Where a real date comes from in a static build
There is no request time in a static build; there is only build time, which is the wrong date, and whatever the build can read. Three sources are real.
Front-matter dates
A date field in the content file, and an updated field beside it when the piece is revised, is the editorial record: someone wrote a date because something happened. It survives rebuilds, moves and reformatting, and it is wrong only when a person forgets to bump updated, which is a failure that leaves the date stale rather than false. Stale is recoverable. Most generators expose front matter to the sitemap step; the work is to make the sitemap read it rather than the clock.
Git commit times
The last commit that touched a content file is a real event with a real timestamp, and one command retrieves it.
git log -1 --format=%cI -- src/content/docs/getting-started.md
It is worth a warning per line. A CI checkout with a depth of one has no history, so every file's last commit is the clone, which recreates the stamp. A formatting pass, a bulk find-and-replace or a directory move touches every file at once and dates the whole site to that afternoon. And the commit that touched a file is not always a change to its content; a corrected front-matter key is a commit. Git times are honest by default and easily made dishonest by routine housekeeping, so the sites that use them well pair them with a rule about what counts as a content commit.
A content registry
The third source is one list, kept by hand, of the site's pages and the date the content of each last changed. It costs one edit per real change and nothing per deploy, and because the sitemap, the llms.txt and any other index are generated from the same list, they cannot disagree. This is what we use for the static pages, and articles carry their own dates alongside it.
How ours carries one
The route registry in our web application holds every static page with a title, a one-line summary for machine readers, and a sitemap priority. Beneath it sits a single constant, and its comment is the rule:
/**
* When the marketing copy last meaningfully changed — bump on real content
* edits. Used for sitemap lastmod (check D4), so it must be true rather than
* "now" on every build. Report pages carry their own per-scan lastmod in
* /sitemaps/sites-{n}.xml.
*/
const CONTENT_UPDATED = "2026-09-04";
A deploy that changes code moves nothing. An edit to a page's copy is accompanied by a bump to that line, reviewed in the same pull request, and the date is true for the reason a signed statement is true: someone put their name to it.
Each route can carry a lastModified of its own that overrides the constant. Static pages leave it unset. Blog posts set it from their front matter: the post's updatedAt if a revision has been recorded, otherwise its publishedAt. The sitemap route then does one thing per entry:
const entries = [...indexableRoutes(), ...blogRoutes()].map((route) => ({
loc: absoluteUrl(route.path),
lastmod: route.lastModified ? new Date(route.lastModified) : modified,
}));
where modified is the registry's content date. The serialiser writes the date only, YYYY-MM-DD, the protocol's shortest form, so a scan never has to parse a timestamp. The route regenerates hourly rather than at build, because a post scheduled for a future day has to join the file on that day and a file written at deploy time would not know. The report pages, one per scanned site and each dated by the scan that produced it, live in separate files under /sitemaps/ and are listed on their own lines in robots.txt, so the first Sitemap: line, the one D4 reads, stays the registry's sitemap.
The effect on our score is whatever the calendar says. While the blog publishes daily, the recent share is high. When it stops, the share falls, and the fix will be to write something rather than to touch the constant.
Generator by generator
The specifics below are limited to what each project's documentation describes as of this writing; check it before relying on any of them.
- Astro. The sitemap integration's documentation describes a single
lastmodoption applied to every entry and aserializehook that lets you set a per-entry value, for instance from front matter or a git lookup. The single option set to the build time is the stamp; the hook is the fix. See the Astro documentation. - Docusaurus. The classic preset's sitemap plugin documents a
lastmodoption that is off by default and, when enabled, derives dates from the file's git history, with the caveats about shallow clones above. See the Docusaurus documentation. - Next.js. The sitemap is whatever your code returns, which is the best and worst case at once. We moved ours from the metadata file convention to a route handler because the convention prerendered once at build and ignored the revalidation settings; the sitemap route's comment records why.
The rule that applies to all of them: a URL's lastmod moves when its content moves, and if the build cannot know that for a URL, omit the element for that URL rather than invent it. Fewer dated URLs lower the ratio, and the way to raise it is to change something.
Check your own site
Fetch your own sitemap and look at whether the dates differ from one another and from today. The free scan records sitemapUrls, withLastmod and modifiedLast90Days under D4, and the checks are defined under discovery and access and trust and freshness on the methodology page.