D4 is worth four points and asks one question three ways: has anything on this site changed lately, and can a machine tell? One point for a sitemap that carries any lastmod at all, two for at least a fifth of its URLs having changed in the last 90 days, and one for a machine-readable date somewhere in the pages the crawler read. The rubric rates the effort Medium, which is fair. None of the three is hard to produce, all three are easy to produce falsely, and a false one is worth less than none. This post goes through how the check computes each part, what a sitemap stamped at build time looks like from outside, and how our own dates are kept true.
What the check reads
Two inputs, both already in hand before the check runs. The first is the list of URLs parsed from the site's sitemap, each with whatever lastmod it carried. Which file that is, and why the first Sitemap: line in robots.txt decides it, is covered in the sitemap post and not repeated here. The second is the raw HTML of every crawled page that returned a 200 with a body, joined into one string. Nothing is rendered and nothing extra is fetched.
Point one, a lastmod exists
The check counts the URLs whose lastmod is non-empty and records the count as withLastmod. The point is awarded when the count is at least one. That is the whole test. A single dated URL in a file of ten thousand undated ones scores it, which is why it is worth one point and not three: it establishes that the site knows the element exists, nothing more.
Points two and three, the 90-day window
The check takes the moment of the scan, subtracts 90 days, and counts the dated URLs whose lastmod parses to a time at or after that moment. That count is modifiedLast90Days. The ratio the two points turn on is that count divided by the number of URLs in the file, all of them, dated or not, and the points are awarded when the ratio is at least 0.2. When the ratio falls short, the recommendation on the report states it as a percentage: only 4% of sitemap URLs modified in the last 90 days.
The date goes through JavaScript's Date.parse. A date-only value such as 2026-09-04 is read as midnight UTC on that day, which is fine at a 90-day granularity. A full timestamp with an offset, which is what the sitemaps protocol specifies as the W3C Datetime format, parses too. The parser is lenient in one way worth knowing: 04/09/2026 is read as 9 April, not 4 September. A value that does not parse at all still counts towards withLastmod, because it is present, but never towards the recent count. Use the protocol's YYYY-MM-DD and neither problem arises.
What 20% means depends on the shape of the file. A thirty-URL marketing site needs six URLs with a genuine change in any 90-day window, which is an edit every fortnight. A site that publishes weekly gets there on new URLs alone, since thirteen posts a quarter clear the bar in any file of up to 65 URLs. A catalogue with thousands of product pages that were last edited when they were created will not get there by editing, and the sitemap post covers the ordering fix for that case.
Point four, a visible date
The last point is a string test against the joined HTML. It passes if any crawled page contains a time element with a datetime=" attribute, matched without regard to case, or the literal text "datePublished" or "dateModified", quotes included, which is what those keys look like inside a JSON-LD block. The evidence field is visibleDates, true or false.
Three consequences follow from it being a string test. A date written as prose, "Last updated March 2026" in a paragraph, scores nothing, because the check does not read prose. A time element with a single-quoted attribute does not match, because the pattern wants the double quote. And a JSON-LD block that carries dateModified passes even if the rest of the block would fail B1, because the check looks for the key, not for a parseable document.
The other constraint is which pages are searched. The crawler fetches the homepage and up to five more, preferring paths under pricing, product, docs, about, contact and checkout, so a date that lives only on blog posts helps only if a post was among the six. The reliable version is a date on every page, and that is the one we use.
The stamp at build time
Several sitemap generators, given no better information, write the build time into every URL's lastmod. On a site that deploys daily, every URL then claims to have changed today.
Seen through the check, that file produces three equal numbers: sitemapUrls, withLastmod and modifiedLast90Days all the same, and a ratio of 100%. The check as written in rubric v1.0.1 awards the points, because a scan is a single observation and cannot tell a stamp from a genuinely busy site. Anyone holding a second observation can. Fetch the file twice a week apart: if every date has moved by exactly the interval between deploys, the dates are deploy dates. A consumer that has a cached copy of a page and sees its lastmod advance while its content stays the same learns the same thing, and what it learns is to ignore the field. That is the sense in which the stamp is worse than nothing: an absent lastmod says the site has not said, while a false one says the site cannot be believed on this point, and it is a short step from there to the others.
The protocol defines lastmod as the date the page was last modified. We keep to that definition on our own site for the same reason we do not publish a score without a scan behind it.
How ours stays true
Our sitemap is a route handler that regenerates hourly rather than a file written at build. It builds its urlset from the route registry, and the date on each entry comes from one of two places.
Static pages take a content date held as a single constant in the registry. The comment above it in the source says what it is for:
/**
* 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.
*/
const CONTENT_UPDATED = "2026-09-04";
It is edited by hand when copy changes and left alone when it does not, so a deploy that touches only code moves nothing. Blog posts take their own date from the post's frontmatter: the revision date if one has been set, otherwise the publish date. The sitemap route's own comment puts the rule in one line: static pages carry the registry's content date, blog posts carry their own publish or revision date.
This means our ratio on D4 is whatever the calendar says. While the blog schedule runs, new posts keep it well above a fifth. When the schedule ends, it will drift down, and the fix will be to write something, not to stamp the file.
The visible-date point is handled so that it does not depend on which six pages a crawl happens to pick. The WebSite block in the JSON-LD on every page carries a dateModified taken from the same constant as the sitemap, so the two never disagree. Blog posts add a Published line in a real time element with a datetime attribute, an Updated line beside it when a revision date exists, and datePublished and dateModified in their Article block. The privacy, terms and DPA pages carry a Last updated line in a time element, and the rubric changelog dates every entry the same way.
Check your own site
Fetch your sitemap and look at the lastmod values: whether they exist, whether they differ from one another, and whether they match anything you remember changing. The free scan records sitemapUrls, withLastmod, modifiedLast90Days and visibleDates under D4, and the check's definition is on the methodology page under trust and freshness.