sitemap.js is ESM-only and supports streaming sitemap generation
Sitemap-generating framework for node.js
At a glance
- What is it?
- The sitemap package generates sitemap XML for Node.js through a streaming API, a CLI, and a higher level helper that splits large URL lists into indexed files. Version 9 dropped the CommonJS build, which moved the minimum Node version from 22.0 to 22.12, and a patch release landed across three major versions on the same day.
- Who is it for?
- Adopt sitemap when you need streaming sitemap generation without holding the whole document in memory, because SitemapStream and streamToPromise cover both the one shot and the cached server case. Do not adopt it on Node below 22.12, since require('sitemap') only works where built-in require(esm) exists.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 3 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The ESM-only move and why it moved the floor to 22.12
Installation is one command:
npm install --save sitemapThe requirement line is the one to read carefully. The package requires Node.js 22.12 or newer and is ESM-only, with no CommonJS build shipped. The README gives the reason for the specific version rather than a round number: require('sitemap') works only on Node versions with built-in require(esm) support, which is why the minimum is 22.12 rather than 22.0.
That is a sharper break than a normal major version bump. Code written as const sitemap = require('sitemap') fails to load on older runtimes even if the API is unchanged, and code in a CommonJS project needs either an upgrade or a dynamic import.
The package layout follows. package.json declares type module, exports only the root entry and ./package.json, points bin at dist/cli.js, and ships dist plus schema. Types come from dist/index.d.ts. Source is TypeScript in lib/ and cli.ts, built with tsc against tsconfig.build.json.
The schema directory is shipped on purpose rather than omitted, which suggests consumers can validate their own XML against the same definitions the project validates against.
The CLI covers one time generation and existing files
For a plain list of URLs the README points at the CLI, and it also parses, updates and validates existing sitemaps:
npx sitemap < listofurls.txtThe help flag is `npx sitemap -h`, which the README describes as the place to find more examples and the full option list.
The programmatic equivalent for a one time generation is small enough to read in full. You build an array of link objects, construct a SitemapStream with a hostname, pipe a Readable through it, and resolve the XML as a string through streamToPromise:
import { SitemapStream, streamToPromise } from 'sitemap'
import { Readable } from 'stream'
const links = [{ url: '/page-1/', changefreq: 'daily', priority: 0.3 }]
const stream = new SitemapStream( { hostname: 'https://...' } )
return streamToPromise(Readable.from(links).pipe(stream)).then((data) =>
data.toString()
)The hostname option is what turns relative URLs into absolute ones, which is why every entry's url starts with a slash here. changefreq and priority are optional, as a later example in the README shows by writing only { url: '/page-3/'}.
An img field also appears on an entry, so image entries are supported through the same stream rather than a separate API.
Serving a cached sitemap from Express
The README's server example is the pattern most people copy, and it is deliberately a cache plus a stream rather than a regeneration per request. It applies to fewer than 50 thousand URLs; the README points to SitemapAndIndexStream beyond that.
The route sets Content-Type to application/xml and Content-Encoding to gzip, returns the cached buffer if one exists, and otherwise builds a SitemapStream, pipes it into createGzip, writes entries, attaches streamToPromise to cache the result, and ends the sitemap stream. The comment on that line is the part people get wrong: a write stream such as streamToPromise has to be attached before ending, otherwise the cached value is never populated and every request regenerates the file.
Writes can go one at a time with smStream.write({ url: '/page-1/', changefreq: 'daily', priority: 0.3 }) or in bulk by piping a Readable of entries, which the README notes avoids writing your own loop.
The gzip pipeline is then piped to the response, with errors thrown, and the surrounding try/catch responds 500. Whether you need that catch depends on your Node version and error handling, but the structure is what the documentation models.
SitemapAndIndexStream and the 45,000 entry split
Past 50,000 URLs, sitemaps must be split and referenced from a sitemap index, and that is the job of the more complex interface. The convenience wrapper is simpleSitemapAndIndex, which writes both the sitemaps and the index to a directory you name:
import { simpleSitemapAndIndex, lineSeparatedURLsToSitemapOptions } from 'sitemap'
simpleSitemapAndIndex({
hostname: 'https://example.com',
destinationDir: './',
sourceData: lineSeparatedURLsToSitemapOptions(
createReadStream('./your-data.json.txt')
),
limit: 45000,
gzip: true,
publicBasePath: '/sitemaps/',
xslUrl: 'https://example.com/sitemap.xsl',
}).then(() => {
// Do follow up actions
})sourceData is flexible: a line separated URLs file wrapped in lineSeparatedURLsToSitemapOptions, a plain array of entries, or a path string. limit defaults to 50000, gzip defaults to true, and publicBasePath defaults to './'. xslUrl is optional and attaches an XSL stylesheet, which is how you get a human readable sitemap page.
When you need control, drop to SitemapAndIndexStream and supply a getSitemapStream callback that is invoked each time a new file is needed. You return three things: the URL where the sitemap will be hosted, the SitemapStream, and the write stream it should go to. That is how you direct output to a CDN path or a different filesystem.
The callback's compression handling has a documented subtlety: if your server automatically serves sitemap.xml.gz in response to a request for sitemap.xml, leave the index path without the extension, otherwise add .gz to both the filename and the URL so the index and the file agree. Getting this wrong produces an index that points at files the server does not serve, which is the classic broken sitemap.
Testing setup and the deploy gate
The scripts section shows a serious test setup for a small library. The test script builds first and then runs node with --experimental-strip-types over tests/*.test.ts, so the suite executes TypeScript directly rather than a compiled copy. test:coverage adds thresholds that are enforced by the flag itself: 80 percent branch, 90 percent function, 90 percent line.
test:full chains lint, coverage and an XML validation step, and there are separate targets for schema validation and for performance, test:perf running node ./tests/perf.mjs. prepublishOnly wipes dist, rebuilds and runs the tests, so a publish cannot ship an unbuilt or failing package.
The Makefile adds a check the npm scripts do not. It greps version strings out of package.json and index.js, counts the unique semantic versions, and fails if there is more than one, printing that versions in the files differ. deploy runs test, check-versions, deploy-npm and deploy-github in that order. That guard exists because the version has historically been duplicated across files, and a mismatch between what you publish and what you test is invisible otherwise.
Repository layout is conventional TypeScript: index.ts at the root, lib/ for source, tests/, docs/, examples/ with nine runnable scripts covering filtering, parsing existing XML, streamjson input, writing to console and to file, updating a sitemap and the index case. api.md documents the surface, CLAUDE.md is present, and husky plus .prettierrc and eslint.config.mjs handle hooks and formatting.
Three maintained majors, all patched together
The release list is the part to plan around: 7.1.3, 8.0.3 and 9.0.1 all shipped on 2026-02-28, each labelled a security patch, within about twenty minutes of each other. Three major lines are being maintained in parallel rather than one line at a time.
That has two consequences. If you are pinned to 7.x or 8.x you are still receiving security fixes, so staying behind is not the same as being unpatched. It also means the ESM-only constraint applies differently per line, and the package you must write against differs by major version, so an upgrade is a real migration rather than a version bump.
There is a CHANGELOG.md at the root, a CODE_OF_CONDUCT.md, a dependabot configuration for dependency updates, and a Makefile env target that builds an isolated nodeenv for the Python tooling the XML checks use.
Licensing is MIT, and the CLI ships inside the same package under the bin entry rather than as a separate install, so installing sitemap gives you both the library and the npx entry point.
Editorial conclusion
Adopt sitemap when you need streaming sitemap generation without holding the whole document in memory, because SitemapStream and streamToPromise cover both the one shot and the cached server case. Do not adopt it on Node below 22.12, since require('sitemap') only works where built-in require(esm) exists. Verify first that npm run test:full passes on your machine, since it chains lint, coverage thresholds of 80 percent branches and 90 percent functions and lines, and the XML validation step.
Frequently asked questions
What Node.js version does the sitemap package require?
Node 22.12 or newer. The package is ESM-only and no longer ships a CommonJS build, so require('sitemap') works only where Node has built-in require(esm) support, which is why the floor is 22.12 rather than 22.0.
How do I generate a sitemap from a list of URLs?
Pipe the list into the CLI with npx sitemap < listofurls.txt. Programmatically, build a SitemapStream with a hostname, pipe a Readable of entry objects through it, and collect the XML with streamToPromise.
How do I split a sitemap larger than 50,000 URLs?
Use SitemapAndIndexStream, or the simpleSitemapAndIndex wrapper. It writes a new sitemap every 45,000 entries by default and generates a sitemap index referencing them. The limit, gzip and publicBasePath options all have documented defaults.
Why is my sitemap index pointing at files that do not load?
It is usually the .gz extension mismatch. If your server serves sitemap.xml.gz when asked for sitemap.xml, keep the index entry without .gz; otherwise add .gz to both the written filename and the URL so the index and the file agree.
Can the sitemap package parse and validate an existing sitemap file?
Yes. The README states the CLI can parse, update and validate existing sitemaps, and the examples directory includes parse-existing-xml.js and update-sitemap.js as runnable starting points.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/ekalinin-sitemap-js)Was this useful?