Build a sitemap
Sitemap is the builder for any site under the protocol limit of 50,000 URLs
or 50MB. It keeps every entry in memory, so you can inspect it, run
diagnostics on it, and serialise it more than once. Above that limit, use
SitemapIndex.
import { Sitemap } from "@warlock.js/sitemap";
const sitemap = new Sitemap({ baseUrl: "https://example.com", changefreq: "weekly", priority: 0.5,});
sitemap.add({ path: "/" });
for (const post of await Post.all()) { sitemap.add({ name: "post-details", route: "/posts/:id", path: `/posts/${post.slug}`, lastmod: post.updatedAt, priority: 0.8, });}
const xml = sitemap.toXML();
await sitemap.saveTo("public/sitemap.xml");new Sitemap(options)
Section titled “new Sitemap(options)”| Option | Meaning |
| --- | --- |
| baseUrl | Required. An absolute origin, such as https://example.com or https://example.com/docs. |
| changefreq | The default for any entry that doesn’t set its own. |
| priority | The same. |
| lastmod | The same. |
The constructor validates baseUrl. A value that isn’t an absolute http(s)
URL throws InvalidBaseUrlError. That includes a missing value, a relative
path, mailto:, and file:. The error points at the line that set the value,
so you don’t find out at the first request.
Entries — add(entry) / addMany(entries)
Section titled “Entries — add(entry) / addMany(entries)”Only path is required. It must be a concrete path, such as /posts/123.
A pattern like /posts/:id is not a URL.
| Field | Meaning |
| --- | --- |
| path | Required. Resolved against baseUrl. An absolute URL is used as given. |
| name | An optional label for the route, for your own diagnostics. It is never written to the XML. |
| route | The optional pattern this URL came from, such as /posts/:id. routes() and duplicates() use it. |
| lastmod | A Date, written as a W3C datetime (toISOString()), or a string, written exactly as given. |
| changefreq | always, hourly, daily, weekly, monthly, yearly, or never. |
| priority | 0.0–1.0. |
| alternates | Language versions of this page. See Alternates below. |
Images
Section titled “Images”Set images to absolute HTTP(S) URLs for Google’s image sitemap extension:
sitemap.add({ path: "/products/widget", images: [{ loc: "https://cdn.example.com/widget.jpg" }],});The image namespace is emitted only when an entry uses it. At most 1,000
images are emitted for one URL. Extra images are dropped; configure
onImageLimitExceeded in SitemapOptions to receive a route diagnostic.
Both methods return this, so you can chain them. An entry the protocol can’t
represent throws InvalidSitemapEntryError from add(): a missing path, a
priority outside 0.0–1.0, an unknown changefreq, an invalid Date, or
an alternate with no hreflang. Paths are normalised with a leading slash,
so a and /a are the same entry.
size gives the number of URLs that will be emitted. entries() returns a
copy of them, with the defaults already applied.
Duplicates are silent, but not hidden
Section titled “Duplicates are silent, but not hidden”Entries are stored by path. If you add the same path twice, the later entry
replaces the earlier one without an error. A duplicate <loc> would make the
document invalid, and two loops that cover overlapping URLs are common.
duplicates() lists every collision with its count and the route of each
add(). That lets you name the two sources that collided:
sitemap.duplicates();// [ { path: "/posts/1", count: 2, routes: ["/posts/:id", "/:slug"] } ]routes() and declareRoute()
Section titled “routes() and declareRoute()”routes() counts the URLs each route produced. Watch for a count of
zero. It means a route you expected to add URLs added none. A whole section
of the site is then missing from a document that otherwise looks correct.
A route only appears in routes() when an entry uses it, so its count is
never zero on its own. declareRoute() registers the pattern in advance, and
then an empty result shows up:
sitemap.declareRoute("/products/:slug");
sitemap.routes();// [ { route: "/posts/:id", count: 400 }, { route: "/products/:slug", count: 0 } ]
const empty = sitemap.routes().filter((route) => route.count === 0);
if (empty.length > 0) { throw new Error(`empty sitemap routes: ${empty.map((route) => route.route).join(", ")}`);}The package only reports problems. It never prints anything, and it never throws over a duplicate or an empty route. You decide whether a zero is a warning or a failed build.
Language alternates
Section titled “Language alternates”alternates writes <xhtml:link rel="alternate" hreflang="…"> inside each
<url>:
const alternates = [ { hreflang: "en", path: "/en/about" }, { hreflang: "ar", path: "/ar/about-us" }, { hreflang: "x-default", path: "/en/about" },];
sitemap.add({ path: "/en/about", alternates });sitemap.add({ path: "/ar/about-us", alternates });The package has no concept of a locale. hreflang accepts any string (en,
en-GB, x-default), and the package never builds a /{locale}/… path for
you. You supply each path yourself, which also works when slugs differ between
languages. The xhtml namespace is declared only when an entry uses it.
Output
Section titled “Output”toXML()
Section titled “toXML()”toXML() is pure and synchronous. Two calls return the same string, and it
changes nothing. Sitemap satisfies core’s XMLable contract
(toXML(): string) by shape alone, so a Warlock controller can return it
directly:
import type { RequestHandler } from "@warlock.js/core";import { Sitemap } from "@warlock.js/sitemap";
export const sitemapController: RequestHandler = async ({ response }) => { const sitemap = new Sitemap({ baseUrl: "https://example.com" });
sitemap.add({ path: "/" });
return response.xml(sitemap); // Content-Type: application/xml};See response.xml(). A
Warlock web app doesn’t need this
route, because web serves /sitemap.xml itself.
saveTo(filePath)
Section titled “saveTo(filePath)”Writes the document atomically and creates any missing parent directories.
If the write fails or is interrupted, the existing file at filePath stays
as it was. It is never left half-written.
publishTo(outDir, fileName = "sitemap.xml")
Section titled “publishTo(outDir, fileName = "sitemap.xml")”Publishes the document as the entire contents of outDir, in the same way
SitemapIndex.saveTo()
publishes a set. The directory is swapped in atomically and marked as owned.
Use it when the site may later need an index. The index can then be published
to the same directory, and a later single-file publish removes old shards.
It returns the path of the published file.
outDir must be used only for the sitemap. A non-empty directory without the
ownership marker throws UnownedOutputDirectoryError, and nothing in it is
changed.
Also exported
Section titled “Also exported”buildSitemapXml(entries, baseUrl), renderUrlBlock(entry, baseUrl),
escapeXml(value), and joinOrigin(origin, path) are the functions Sitemap
is built from. Use them if you want the serialiser without the builder. See the
API reference.