Pagedeck

Sitemaps

build.sitemap is where a site asks the build to publish its URLs: one sitemap per locale, one index per output tree, and every entry annotated with the same hreflang links the page itself carries.

build: {
  outDir: "./site",
  origin: "https://example.com",
  xDefault: "en",
  sitemap: { pattern: "suffix" },
  // ...
}

A site that declares no sitemap gets exactly the build it got before the field existed. No file is written and no manifest row appears.

origin is required

Every <loc> in a sitemap is an absolute URL — the protocol has no relative form — so sitemap without origin is refused when the config loads rather than quietly writing nothing. The same rule xDefault follows, for the same reason: a field the build can never read is a field you can set and watch do nothing.

origin supplies the scheme and the port. A locale with its own domain overrides the host for that locale's tree, so a German locale on example.de is published on example.de while the default tree is published on the origin's host. This is the composition canonicals and hreflang already uses, called again rather than repeated.

The two patterns

pattern picks the address the per-locale files take. There is no default: which URLs your site publishes is not a choice this framework can make for you, and the addresses are ones you then have to keep serving.

pattern English sitemap German sitemap on its own domain
"suffix" /sitemap-en.xml /sitemap-de.xml
"directory" /en/sitemap.xml /de/sitemap.xml

Both write the tree's index at /sitemap.xml, which is the address to give a crawler or a robots.txt line. "directory" names the locale rather than the locale's URL prefix, which only matters on a tree with a single locale: such a tree is unprefixed, its pages sit at /about rather than /de/about, and its sitemap still goes to /de/sitemap.xml so that /sitemap.xml stays the index.

One index per output tree

A locale with a domain emits into that domain's own tree; locales without one share the default tree. Each tree gets its own index, and an index names only the sitemaps in its own tree — the default tree's index never mentions a URL on another host.

Every locale you declare gets a sitemap, including one that has no page yet. Its file is an empty <urlset>, which is valid and says what is true.

What an entry holds

<url>
<loc>https://example.com/en/pricing</loc>
<xhtml:link rel="alternate" hreflang="de" href="https://example.de/pricing"/>
<xhtml:link rel="alternate" hreflang="en" href="https://example.com/en/pricing"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://example.com/en/pricing"/>
</url>

The <loc> is the page's canonical and the xhtml:link set is the page's hreflang set — the same values, from the same call, as the <link> tags in that page's <head>. They cannot disagree, so there is nothing to keep in step: declare xDefault and it appears in both, translate a page into a fourth locale and both grow the entry.

Fallback pages are in the sitemap, because they are pages: a locale that takes a page from its fallback chain serves it at its own URL, and that URL is the one it publishes.

Experiment variants are not. An arm declared in build.routing.experiments is the primary page's bytes at a second address, it canonicalizes to the primary, and no sitemap names it.

The 404 page is not either. A page a build.routing.notFound rule names carries noindex, so no sitemap lists it and no entry names it as an alternate. See Routing.

What an entry does not hold

No <lastmod>, <changefreq> or <priority>.

<lastmod> states when the page last changed, and the only instant a build has is when the build ran — so a redeploy that changed nothing would tell every crawler that everything had changed. <changefreq> and <priority> are values the build would have to invent about your content, and the search engines that once read them have said they do not.

Incremental builds

pagedeck build --incremental regenerates a locale's sitemap only when a page moved: one added, one removed, one served from a new address, or one that became the 404 page or stopped being it. That counts pages in the locale itself, and pages in any other locale at a path this one also routes — the sitemap names those in the path's hreflang set, so they move it too. An edit to a page's content moves neither a URL nor an hreflang set, so the file is left exactly as it was — the same bytes, not rewritten with the bytes it already held.

A sitemap the run keeps is read back off the output tree and checked against the previous build's manifest first. Delete one, or edit one by hand, and the next incremental build writes it again rather than refusing: a sitemap is composed from the route table, so there is nothing lost that the build cannot make again.

Each tree's index is written on every run. Its bytes depend on your locale set and on nothing a page can change, and the build compares no config between runs — so it composes the index rather than trust that you did not add a locale.

Change origin or xDefault and run a full build. Both go into every <loc> and every hreflang link, and an incremental build has no way to see that a config value moved. A locale whose pages did not move keeps the sitemap it had, which is the one the old origin wrote.

Known limit: 50,000 URLs per locale

The sitemaps protocol caps one file at 50,000 URLs and 50 MB, and the build writes one file per locale with no sharding. A locale past either cap ships a sitemap a crawler will refuse. If you are near it, file an issue: the tree's index is already the indirection that makes sharding invisible to a crawler.