Munchable's sitemap is 73 lines of app/sitemap.ts, the Next.js file convention. It returns an array, the framework prerenders it at build time and owns the escaping and the content type. You can read the output right now: munchable.app/sitemap.xml.
The file opens with a doc comment that explains what it replaced, and ends it with a number:
With the product pages gone, every URL here comes from code, the whole set is about 190 entries, and the framework can prerender it at build time.
Here is the whole set, counted from production:
curl -s https://munchable.app/sitemap.xml | grep -c '<loc>'
# 516
The number was true
I went back to the commit that wrote it. At that point the same file carried a second count, inline, above the block that emits the question pages:
the index exists so a crawler reaches all 173 without relying on this file alone.
173 question pages, plus their index, plus a conditions index, plus one page per condition, plus three legal pages, plus the homepage. About 190. The comment was accurate on the day somebody typed it.
Then four things happened, none of which touched sitemap.ts:
- 38 recipe pages arrived, each one a route under /recipes.
- A page count that is a product, not a sum, gained a factor. The question pages are generated from the ingredient index crossed with the conditions, so adding one condition does not add one page. /conditions lists eight now, and the newest one is responsible for 88 of the URLs in that file by itself.
- One reference page got added at the site root.
- An About page.
Today's breakdown, straight out of the XML: 461 question pages at the site root, 38 recipes, 8 conditions, 9 fixed pages. The 190 is off by 326, which is more than the whole site was when the comment was written.
The file it imports refuses to do this
sitemap.ts imports ANSWER_PAGES from the module that builds the question pages. That module's header says, in the third sentence:
The count is not written down here on purpose: it moves every time a rule map grows, and a number in a comment goes stale silently.
So the policy exists, it is written down, it is correct, and it is one import below a comment that violates it. The inline "all 173" was eventually fixed, by the same technique: it now reads "so a crawler reaches every one of them". Whoever did that edited the sentence they were standing in and left the one at the top of the file alone, which is exactly what I would have done.
It is in a third file as well
The IndexNow submitter builds its own list of every URL the site publishes, because search engines want to be told what changed rather than asked to re-crawl. Two of its comments:
/** Protocol ceiling for one request. The whole site is ~190 URLs, but batching
* is what keeps a future product-page sitemap from silently overflowing. */
const MAX_URLS_PER_REQUEST = 10_000;
// A fingerprint is a hash of what a page actually says, so "has this changed?"
// is answered without fetching 190 pages and diffing HTML.
There is a test over this file, and it is a good test: it fails if the submitter's URL set and the sitemap's URL set ever disagree. Two lists, one assertion, no drift. What it cannot catch is that both lists are now 2.7 times the size that three comments in the repo claim, because no comment is in the assertion. The batching ceiling is fine either way: 516 is a long way under 10,000, and that was the point of writing it as a constant.
The one lastmod in the file
515 of the 516 entries have no <lastmod>. The homepage has one:
<url>
<loc>https://munchable.app/</loc>
<lastmod>2026-10-21T17:20:41.891Z</lastmod>
<changefreq>weekly</changefreq>
<priority>1</priority>
</url>
That is new Date() evaluated during the build. The commit that produced the current deploy is timestamped 17:20:03 UTC, so the one freshness signal in the file is 38 seconds after a git commit and has nothing to do with when the homepage's copy last changed. It will be identical on the next deploy that changes nothing, and that is the behaviour lastmod exists to avoid.
I am not in a hurry to fix it. A wrong lastmod on one URL is a weaker lie than a missing one on 515, and the content pages genuinely do not have a modification date to report: they are derived at build time from data modules, and "when did this file change" is a git question, not a runtime one. The honest fix is to stop emitting lastmod at all, which costs nothing, because priority and changefreq are already decoration. The shape of the priorities in that file (1.0 for home, 0.9 for conditions, 0.8 for the indexes, 0.7 for the leaves, 0.3 for legal) is a statement of intent to a crawler that has been ignoring the field for years.
What the file does that is worth copying
Two things, neither of them the numbers.
First, every URL in it comes from a data module, not from a string literal. Adding a condition page adds its URL, its canonical tag, its entry in the IndexNow list and its row in the sitemap, with no sitemap edit. That is why nobody opened the file for four feature commits, which is also how the comment got to be this wrong.
Second, the first line of the function:
if (!IS_INDEXABLE) return [];
Every preview deploy on Vercel gets its own hostname. robots.ts already disallows everything on those hosts, and a preview sitemap that advertises 516 crawlable URLs under a hostname whose robots.txt forbids them is two files disagreeing in public. An empty array keeps them in step.
If you want to see the per-condition arithmetic that moved the count, the index is at /answers, and the newest condition's reference page is /hidden-gluten-ingredients. Count the <loc> tags yourself. I did, and the comment at the top of my own sitemap was wrong by more than its own value.
Top comments (0)