Our sitemap has no priority and no changeFrequency, and 69 of its 72 dates come from a TypeScript file

Run this against the live site before reading any further:

curl -s https://pub-trivia.app/sitemap.xml | grep -o "<lastmod>[^<]*" | sort | uniq -c

At the time of writing it answers:

  69 <lastmod>2026-09-14T00:00:00.000Z
   3 <lastmod>2026-09-14T12:01:12.932Z

Seventy two URLs, two distinct values, and the interesting part is why there are two rather than one. There are also no <priority> or <changefreq> elements anywhere in the document, which took a deletion rather than a decision.

The sitemap is pub-trivia.app/sitemap.xml if you would rather read it than curl it.

Two fields that were eight invented numbers

The generator used to emit priority and changeFrequency for every entry, which in practice meant somebody sitting down and deciding that the pricing page was a 0.9 and the FAQ was a 0.7. Nobody could say what made one of those right. The homepage got 1.0 because it was the homepage. The guides got weekly because weekly sounded diligent.

Google documents both fields as ignored. Not deprecated, not weighted lightly: ignored. So the honest reading of that block of the file was eight numbers nobody could justify, carried in a document a crawler reads on every pass, asserting things we had no way of defending. They came out and nothing happened, which is the correct amount of consequence.

What is left is the one field a crawler does use, and the one it is most often lied to about.

A build date is a worse lie than no date

lastModified has had three implementations.

The first called new Date() separately inside each entry, so a 72 URL sitemap carried 72 very slightly different timestamps from the same build, which is a nice illustration of how little thought had gone into it.

The second hoisted that to one new Date() for the whole document. Tidier, and still wrong in exactly the same way: it says every page on the site was revised at the moment of the last deploy. Deploy a copy change to one marketing page and the crawler is told that all 72 changed. Do that for a few weeks and you have trained it to ignore the only field in the file it was willing to believe.

The third, which is what is live now, reads the date out of the content registry: one TypeScript module that holds a node per public page, and each node records the date its content actually changed.

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 is the whole route. Twelve lines, two imports, no list of URLs in it at all.

The three URLs that give the mechanism away

INDEXABLE_ROUTES is the registry's content paths plus the three legal documents, and the legal documents are not content-plan pages, so they have no registry node. nodeFor() returns undefined for them and they fall back to the build date.

That fallback is visible from outside, which is the part I like. A registry date is a calendar date, so it serialises as midnight UTC. The build date is a real instant, so it carries a time. You can pick the exceptions out of the live sitemap without seeing any of the code:

curl -s https://pub-trivia.app/sitemap.xml \
  | grep -E "<loc>|<lastmod>" | paste - - | grep -v "T00:00:00"

The three that come back are /legal/privacy, /legal/terms and /legal/cookies. Every other URL in the file, from /pricing to /guides/pub-quiz-format to /quiz-questions/christmas, is claiming a date somebody typed on purpose.

Whether that fallback is a bug depends on how much you care. A privacy policy that gets a new date on every deploy is making the same false claim the old sitemap made for everything, just three times instead of 72. The fix is either a node for each legal page or a hand-kept date constant in the page, and the reason it is not done yet is that three stale URLs out of 72 is a different size of problem from 72.

The sitemap is not allowed to disagree with the site

The thing the registry buys is not really the dates. It is that the list of URLs is derived rather than written.

Before this, the question "which pages are public" was answered independently in three places: the auth gate's allowlist, the sitemap, and robots.txt. They disagreed. /about shipped in the sitemap while the auth gate redirected to the login form, so every crawler that followed the sitemap entry got bounced. That is a page you have asked to be indexed and then refused to serve.

Now the sitemap maps over one list, that list comes from the registry, and the registry is also what builds each page's breadcrumb trail, its place in the footer, and its related links. A page cannot be in the sitemap and absent from the site, because the thing that puts it in the sitemap is the same thing that renders it. There is a test suite over that list whose only job is to fail when the derived lists stop agreeing.

The small version of the lesson: a sitemap field you cannot defend is worse than a missing one, and new Date() in a sitemap is a field you cannot defend.

If you want to see the rest of what comes off that one registry, the clusters it generates are at pub-trivia.app/guides, /features, /solutions, /compare, /quiz-questions and /tools. The app the whole site exists to sell is at pub-trivia.app, free tier, no card.

Story originally reported by Dev.to. View at Dev.to →
← Back to all news