Sitemaps

A sitemap is an XML list of a site's public URLs. Search engines use it to discover pages. It is not a navigation page and does not decide whether a page is publicly reachable.

Smallest configuration

Sitemap generation is opt-in. A site enables it with an empty [sitemap] table and supplies its deployed origin separately from its deployment path:

[site]
origin = "https://example.org"
base_path = "/guide/"
time_zone = "Europe/Berlin"

[sitemap]

site.origin supplies the scheme and authority: https://example.org in this example. It is an absolute HTTP or HTTPS origin with no credentials, query, fragment, or path other than /. site.base_path supplies the path below that origin where the site is deployed. Together they produce canonical URLs such as https://example.org/guide/blog/first-post.html.

Sitemap locations use Heine's literal generated-file URLs. For example, public/blog/index.html becomes https://example.org/guide/blog/index.html; Heine does not remove index.html or assume that a production host resolves directory requests. heine serve may resolve /blog/ locally as a browsing convenience, but that does not alter generated links or sitemap locations.

Keeping these settings separate makes a deployment below a subdirectory unambiguous. A site at the root uses the default base_path = "/". site.time_zone is an IANA time-zone identifier used to resolve authored publication timestamps. It is required when any page declares one, but not for sitemap generation itself. Without it, pages may not declare published or updated, and their sitemap entries simply omit <lastmod>. Heine has no implicit UTC or machine-local default: Z appears only when Heine serializes an already resolved authored instant as UTC.

The timezone is site-wide. Heine does not infer one from a locale, languages are not geographic regions, and pages do not override it in this version. A site therefore chooses one editorial timezone for its publication chronology.

[sitemap] without site.origin is an error. A sitemap must contain absolute URLs, and guessing an eventual deployment host would make generated output unreliable.

The names have separate scopes: the site-level [sitemap] table enables generation and may gain sitemap-wide settings later. The sitemap field in a page or [pages] descriptor only removes authored output from that generated document; it never enables the feature or adds a page to it.

Generated file

A full build writes sitemap.xml at the root of managed output:

public/sitemap.xml

The file is a Heine-generated resource. It participates in output collision checking and is published as part of the same complete, atomic output tree as the rest of the site. A conflicting copied asset, deployment file, license text, page, or other generated resource is a build error.

The sitemap is served unchanged by heine serve. In particular, development does not rewrite it to localhost or to the request host. The generated file therefore remains deployable and always describes the configured canonical origin.

base_path changes URLs, not the output-directory layout. A site deployed at /guide/ normally configures an output directory that the host mounts there:

[site]
base_path = "/guide/"

[build]
output_dir = "public/guide"

That build writes public/guide/sitemap.xml, which the host serves as /guide/sitemap.xml. As with every generated file, build.output_dir is the only setting that determines its filesystem location.

Quick builds do not update the sitemap. They intentionally do not resolve the complete output topology, so regenerating it could make the sitemap disagree with stale generated output. Run a full build after changes that affect site structure.

Sitemap size

The Sitemap Protocol limits one sitemap to 50,000 URLs and 50 MB of uncompressed XML. Heine must never publish a sitemap beyond either limit.

When the resolved inventory fits within Heine's conservative limit of 45,000 entries and 45,000,000 serialized UTF-8 bytes, sitemap.xml is a normal URL-set document. When it does not, Heine generates sitemap.xml as a sitemap-index document and writes deterministically numbered child URL-set documents below sitemaps/. The index refers to those children with canonical URLs. Child boundaries follow deterministic URL order and account for both entry count and final UTF-8 XML size.

Every child is a generated-output claim and participates in the same collision checks and atomic publication as sitemap.xml. If an individual canonical URL would reach the Sitemap Protocol's 2,048-character URL limit, Heine reports the responsible rendered route instead of emitting an invalid entry. The same limits apply to the sitemap index; an inventory too large for one index is a build error rather than invalid XML. Recursive index trees are not part of the initial feature.

Publication chronology

Sitemap timestamps and Atom feeds need a statement about publication, not a generic calendar date. Heine therefore replaces the existing date page field with these optional fields:

# content/en/blog/first.page
published = 2026-08-14T09:30:00
updated = 2026-08-27T14:35:00 # optional

published is when the page first became publicly available. updated is the most recent change that the author considers meaningful to readers and search engines. It is not changed for a rebuild, a filesystem timestamp, or an insignificant site-wide edit. updated must not precede published.

Both values are local TOML date-times without an offset. Heine resolves them with the configured site.time_zone, then retains the resulting instant. The generated sitemap uses updated as <lastmod> when it exists and otherwise uses published; it emits canonical RFC 3339 UTC timestamps. A page with neither field remains in the sitemap without <lastmod>.

Templates receive the same resolved values as page.published and page.updated: UTC RFC 3339 text such as 2026-08-14T07:30:00Z. This makes them directly usable with format_date() and avoids exposing an ambiguous local time at the template boundary.

Heine never uses the build machine's time zone. A named IANA zone makes the source's intended interpretation clear and accounts for daylight-saving changes. Heine resolves it using a bundled, versioned IANA timezone database, not the host's zoneinfo files, so the same Heine version produces the same instant on every supported platform.

A local date-time that never occurred during a clock change is an error. A local date-time that occurred twice is also an error. In both cases, the diagnostic identifies the authored timestamp and configured zone and explains the transition. The repeated-hour restriction is deliberate: without an offset or another disambiguator, the source cannot name which of the two real instants it means. Heine does not accept offset-bearing timestamps as an escape hatch in this version.

published and updated are page chronology, not general-purpose historical dates. A page needing another date-only fact may place it in extra. The renamed collection orders are published-asc and published-desc.

When chronology is required

Chronology is optional for ordinary rendered pages. A contact page, legal notice, or long-lived landing page may have no trustworthy publication instant; requiring one would encourage invented metadata. A sitemap may therefore omit <lastmod> for such a page.

Features that depend on publication time require it at their own boundary. A publication-ordered collection requires published for every member, just as its current ordering rules require their ordering metadata. A directory can apply the same policy to its descendant pages:

# content/en/blog/_directory.toml
[pages.metadata]
required = ["published"]

An Atom feed likewise requires a real publication timestamp for every included entry. updated remains optional everywhere: a page that has not changed significantly since publication uses published as its initial update instant. Requiring both fields would merely duplicate that same fact.

Included URLs

Heine derives sitemap entries from its resolved rendered-output inventory, not by scanning files below public/. This includes every rendered authored page and every generated pagination, taxonomy-index, taxonomy-term, Series-index, and Series-listing page across all configured locales.

It excludes copied assets, declared content files, deployment files, published license texts, non-rendered pages, sitemap.xml itself, and the reserved __heine/ development-server namespace. Each route appears once, in deterministic URL-path order.

Generated pagination, taxonomy, and Series pages have no authored chronology of their own. Their initial sitemap entries therefore omit <lastmod>.

For a multilingual site, the default locale's routes remain unprefixed and other locales retain their normal locale prefixes. The initial sitemap lists all of those canonical URLs. It does not infer translation relationships or emit hreflang alternates: translation groups may be partial, and that extension needs its own explicit contract.

Sitemap membership

Rendered authored pages are included by default. A page may stay public while opting out of discovery through the sitemap:

# content/en/private-preview.page
template = "page.tera"
title = "Private preview"
sitemap = false

This affects only sitemap membership. It does not suppress rendering, linking, collections, taxonomies, or other page behavior. Use template = false when a page should not produce output at all. It also does not add an HTML noindex directive, restrict access, or change robots.txt; those are separate web publishing concerns.

When the opted-out page is a paginated listing, the setting excludes both its authored first page and every pager generated from it. They are one listing family with one authored owner. Taxonomy index and term pages have no such owner, so the first version has no taxonomy-wide or individual taxonomy-page opt-out.

Page sitemap settings require sitemap generation to be enabled. If a page has sitemap = false but heine.toml has no [sitemap] table, Heine reports an error at the page setting. Remove the now-inapplicable field or enable the feature. This prevents inactive settings from silently changing behavior if a sitemap is enabled later.

The page field accepts only false; omit it to use ordinary membership. sitemap = true is rejected rather than accepted as an inert restatement of the default. Sitemap settings on a non-rendered page are rejected for the same reason.

Directory boundaries

A directory descriptor can exclude an entire subtree:

# content/en/drafts/_directory.toml
[pages]
sitemap = false

This boundary applies recursively to every descendant rendered page and to every pager derived from those pages. It is not a default: a nested descriptor or page cannot re-include a descendant. A page setting of sitemap = true is always invalid; below this boundary its diagnostic also points to the descriptor that established the exclusion. This makes a directory exclusion a reliable policy for drafts and previews rather than a preference a nested page can silently undo.

As with page opt-outs, [pages] sitemap = false requires global sitemap generation. A redundant page opt-out below a directory boundary is allowed: it has the same effect and can make an individual page's intent clear without weakening the boundary.

Generated pagers cannot have an independent opt-out. Taxonomy pages have no authored owner in the first version, so they likewise have no individual opt-out.

Intentional first-version boundary

The initial sitemap contains required <loc> entries and <lastmod> only when page chronology supplies it. It does not provide:

  • arbitrary externally supplied URLs;
  • changefreq or priority fields;
  • hreflang alternate links;
  • taxonomy-wide or individual taxonomy-page opt-outs;
  • XML template overrides or template-side sitemap generation; or
  • a development-only sitemap with local-host URLs.

These omissions keep the sitemap an exact, checked view of output that Heine knows it rendered. Later extensions must retain that property and add authored metadata only when its semantics are clear. Atom feeds use the same page chronology while separately defining feed-level metadata and timestamps; see the Atom-feed guide.