Our sitemap does not use new Date(). Each entry's lastModified is read off the content registry, which is one TypeScript array describing every content page on the site:
export default function sitemap(): MetadataRoute.Sitemap {
const buildDate = new Date();
return INDEXABLE_ROUTES.map((path) => {
const updated = nodeFor(path)?.updated;
return {
url: `${SITE_URL}${path === "/" ? "/" : path}`,
lastModified: updated ? new Date(`${updated}T00:00:00Z`) : buildDate,
};
});
}
That updated is a string somebody types:
{
path: '/features/live-leaderboard',
title: 'Live Leaderboard',
description: 'Scores update the moment a question closes…',
cluster: 'features',
updated: '2026-09-14',
related: ['/features/question-timer', '/features/team-scoring', '/solutions/pubs'],
}
A date a human maintains is a date a human forgets. So I audited it: for each of the 69 content nodes, compare its updated against the date of the last commit that touched that page's own file.
The result
total 69 agree 69 registry older than git 0 registry newer than git 0
Sixty-nine out of sixty-nine. I was expecting a list of embarrassments and got nothing.
The live sitemap carries three distinct dates across 72 URLs:
| lastmod | URLs |
|---|---|
| 2026-09-14 | 50 |
| 2026-10-03 | 1 |
| 2026-10-15 | 21 |
The fifty are the content build-out, when most of these pages were written in one go. The eighteen of the twenty-one that are content pages are one commit: a copy change that swept through eighteen page files and bumped eighteen updated fields in the same diff. The one on its own is /about.
Why it works, which is not "discipline"
It is not that somebody is remembering. It is that the field is physically next to the thing that changes.
Here is /about being edited twice in two days, from the actual history:
description:
- 'PubTrivia is built by Sonacode Ltd. Who we are, how the quiz night works, …',
- updated: '2026-09-14',
+ 'PubTrivia is built by Daniel Pertu. Who we are, how the quiz night works, …',
+ updated: '2026-10-02',
description:
- 'PubTrivia is built by Daniel Pertu. Who we are, …',
- updated: '2026-10-02',
+ 'Who builds PubTrivia, how the quiz night works, …',
+ updated: '2026-10-03',
The updated line is three lines below the description line, in the same object literal, in the same hunk of the same diff. You cannot change the description without the date being on screen. That is the entire mechanism, and it is doing more work than any test could.
The eighteen-page copy change is the harder case, because those edits were in app/*/page.tsx files and the dates are in lib/content/registry.ts, a different file with nothing putting it in front of you. That one did depend on somebody remembering. It worked. It is also the one that will eventually not.
What the test suite actually checks
There are twenty-one tests on this registry. Here is everything they say about updated:
expect(node.updated, `${node.path} has a malformed date`).toMatch(/^\d{4}-\d{2}-\d{2}$/)
expect(Number.isNaN(Date.parse(node.updated)), `${node.path} date is not real`).toBe(false)
The date is shaped like a date, and it is a date. That is the whole assertion. '1999-01-01' passes. A page rewritten this morning and left at its 2026-09-14 value passes.
The test I would write compares each node's updated against git log -1 --format=%ad -- <its page file> and fails if git is newer. The reason it does not exist is boring and real: CI checkouts are shallow by default, so the test either needs full history fetched on every run or it silently passes because there is no history to read, and a test that silently passes is worse than no test. It is doable. It is on the list behind several things that are not.
Until then the honest description is: the registry's dates are currently correct, verified once, by hand, on the day of this post.
The three pages that are not in the registry
INDEXABLE_ROUTES is the 69 content paths plus the three legal documents, which have no registry node and therefore no updated. They take the buildDate fallback, which is the moment the sitemap was generated:
<loc>https://pub-trivia.app/legal/cookies</loc>
<lastmod>2026-10-15T12:56:04.220Z</lastmod>
That is not when the cookie policy changed. It is when the site was last deployed. Those three dates move on every build, whether or not a word in them has changed, which is exactly the signal lastmod is supposed to carry and exactly the one a crawler learns to ignore.
Right now this is invisible, because the last deploy happened on 2026-10-15 and so did the last content change, so the twenty-one URLs sharing that timestamp look like twenty-one pages that changed together. Eighteen of them are a fact and three of them are a clock.
And then the actual finding
Each legal page prints its own date for humans:
const LAST_UPDATED = "3 June 2025"
// …
<p className="text-sm text-muted-foreground mb-8">Last updated: {LAST_UPDATED}</p>
So the cookie policy currently holds three different dates:
- 3 June 2025, on the page, which is what a visitor reads
- 2026-09-06, the last time the file changed in git
- 2026-10-15, in the sitemap, which is what Google reads
Sixteen months between the first and the last, on the one page where the date is not decoration but part of what the document is for. The privacy policy and the terms are in better shape: their printed date, 3 October 2026, is also their last commit date. They still hand the crawler the build clock.
Which is the thing to take away, I think. The 69 pages with a hand-typed date in a shared array are the ones that are right, and the 3 pages where a date was supposed to be automatic are the ones where three different answers coexist. Putting the field next to the content beat putting it nowhere, and both beat deriving it from something that is not the content.
If you want to see the generated half working, the guides hub and everything under it carry dates that match their history, and the features hub is in the group of eighteen that moved together in October.
Top comments (0)