A sitemap file caps at 50,000 URLs or 50MB uncompressed, whichever comes first; past either limit you need a sitemap index file pointing at shards. The three decisions that matter: shard by template rather than by number, report lastmod honestly or not at all, and skip priority and changefreq entirely because Google ignores both. Everything else is plumbing, and the plumbing is about 60 lines of build script.

The limits, precisely

Per the sitemaps protocol: 50,000 URLs or 50MB uncompressed per sitemap file. You can gzip on the wire, but the 50MB limit applies to the uncompressed document. A sitemap index can reference up to 50,000 sitemap files — 2.5 billion URLs of headroom — but index files cannot nest: no indexes of indexes. The index itself is trivial:

<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <sitemap>
    <loc>https://selfhostindex.com/sitemap-apps.xml</loc>
    <lastmod>2026-06-14</lastmod>
  </sitemap>
  <sitemap>
    <loc>https://selfhostindex.com/sitemap-comparisons-1.xml</loc>
    <lastmod>2026-06-14</lastmod>
  </sitemap>
</sitemapindex>

The lastmod on an index entry should be the newest lastmod inside that shard — it tells crawlers which shards to re-fetch.

Shard by template and sitemaps become a dashboard

The 50k limit forces sharding; how you shard is a free choice, and most sites throw the choice away by cutting numerically (sitemap-00001.xml, sitemap-00002.xml). Shard by template instead. This site ships sitemap-apps.xml (3,550 URLs), sitemap-comparisons-{1,2}.xml (~44,000), sitemap-categories.xml, and sitemap-blog.xml.

The payoff: Search Console's Pages report filters by sitemap, so per-template indexation — the single most useful number a programmatic site can watch — falls out for free. When the comparisons shard shows 42% indexed and the apps shard shows 91%, you know exactly which template to work on, and diagnosing thin patterns starts from evidence instead of vibes. Numeric shards destroy that signal permanently.

lastmod is a trust contract

Google's sitemap documentation says it uses lastmod when it is "consistently and verifiably accurate." Read that as a threat: crawlers spot-check. If pages with fresh lastmod keep diffing identical to the last fetch, your dates stop being believed — site-wide, not per-URL — and you lose the one mechanism that gets changed pages recrawled quickly.

The honest implementation is to derive lastmod from a content hash, not from the build timestamp: hash the fields that render, bump the date only when the hash changes. The full pipeline is in what dateModified actually does. Done right, the effect is measurable — on this site, app pages whose data changed get recrawled within about a week of the lastmod bump, versus multi-week ambient recrawl for untouched pages.

priority and changefreq, meanwhile, are dead weight: Google has said for years it ignores both. On 50,000 URLs the two tags add roughly 2MB of XML that no crawler reads. Delete them. One formatting note while you're in there: date-only lastmod values (2026-06-14) are valid W3C datetime and entirely sufficient — faking precision with a timestamp adds bytes, not trust.

What belongs in a sitemap (only one thing)

Canonical, indexable, 200-status URLs. Nothing else. Every redirect, 404, or noindexed URL in a sitemap wastes a fetch and erodes the same trust lastmod depends on. The mirror-image mistake also matters: a page in the sitemap that no other page links to is an orphan wearing a disguise — sitemaps get URLs discovered, but discovery without inbound links mostly ends at "Discovered — currently not indexed."

Two assertions worth putting in CI: every sitemap URL returns 200 and self-canonicalises, and no shard exceeds 45,000 URLs (headroom so growth never silently truncates).

On size, for calibration: a 45,000-URL shard with loc and lastmod runs about 6–7MB uncompressed and under 1MB gzipped, and generating the full set for 54,000 URLs takes this site's build a few hundred milliseconds. The 50MB ceiling only threatens sitemaps carrying image or video extensions with long captions — plain URL sitemaps hit the 50,000 count limit first, every time. If you're anywhere near 50MB with plain URLs, something upstream is emitting garbage.

Submission: robots.txt and Search Console, not pings

The sitemap ping endpoint was deprecated in 2023 and removed in January 2024 — guides still telling you to curl google.com/ping are out of date. Two submission paths remain, and you want both:

# robots.txt
Sitemap: https://selfhostindex.com/sitemap-index.xml

plus a one-time submission of the index file in Search Console (which is what enables the per-shard reporting above). Submit only the index; listing individual shards as well just clutters the report. Bing accepts the same robots.txt line and adds IndexNow if you want push semantics — useful, but optional.

What sitemaps will not do

A sitemap is a discovery and recrawl hint, not an indexing command. It will not make Google index pages it has judged thin, and it will not substitute for internal links — a URL's presence in a shard carries no authority. If a section is stuck at "Crawled — currently not indexed," the sitemap has done its job and the template is the problem. Treat sitemaps as instrumentation: they are how you see the problem, per the programmatic SEO playbook, not how you fix it.

What I'd do

Generate shards by template at build time; assert size, status, and canonical consistency in CI; derive every lastmod from a content hash; submit one index via robots.txt and Search Console; then check per-shard indexation monthly and treat any shard trending down as a template regression to investigate that week. Total ongoing cost: one glance a month. Total value: the earliest warning system a large site gets.