[BREAKING CHANGES]
Dropped CommonJS build — the package is now ESM-only
require('sitemap')is no longer supported on Node versions without
built-inrequire(esm). Thedist/cjsbuild, therequireexport condition,
andtsconfig.cjs.jsonare all gone;exportsnow resolves todist/index.js
for every consumer. Migrate toimport { SitemapStream } from 'sitemap'.- Node.js >=22.22.1 now required (previously >=20.19.5). CommonJS
consumers can still load this package throughrequire(esm), which landed
unflagged in 22.12; the floor is 22.22.1 to match the development toolchain. - Build output flattened from
dist/esm/*anddist/cjs/*todist/*. The
binentry is nowdist/cli.js.
simpleSitemapAndIndex defaults to 45,000 URLs per sitemap
- The default
limitdrops from 50,000 to 45,000, the same default
SitemapAndIndexStreamand the CLI already use (DEFAULT_SITEMAP_ITEM_LIMIT).
The headroom keeps rich entries (images, video, news) under the 50MB
uncompressed file-size limit. A 50,000-URL input now produces two sitemap
files instead of one; passlimit: 50000to keep the old output.
SitemapStream writes an empty sitemap instead of erroring
- Ending a
SitemapStreamwithout writing any items now produces a
well-formed empty<urlset>instead of failing withEmptySitemap
(#394, #421). This matchesSitemapIndexStream, which already wrote an empty
<sitemapindex>.EmptySitemapis still exported but deprecated and no
longer thrown. An empty<urlset>does not pass strictsitemap.xsd
validation, which requires at least one<url>; search engines accept it.
One logger option across the library; xmlNS renamed to xmlns
SitemapStream,SitemapIndexStream,SitemapAndIndexStreamand
simpleSitemapAndIndexaccept the sameloggeroption the parsers already
had (#348). Validation warnings that went straight toconsole.warnnow go
through it. Passfalseto silence. TheLoggertype is now exported.- Invalid news and video attribute values now go through
errorHandler(and
so the logger) like every other validation error, instead of a hardcoded
console.warn. The warning text is now the error's name and message, which starts with
the item's URL. UnderErrorLevel.THROWa customerrorHandlerreceives
these errors too, like every other validation error; the default handler
still throws. - The
SitemapStreampropertyxmlNSis nowxmlns, matching the option
name (#300).xmlNSstill works as a deprecated alias.SitemapStream.errorHandleris always set now; it defaults to
a handler that warns throughlogger.
Both XML parsers share one error store and entry cap
XMLToSitemapItemStream and XMLToSitemapIndexStream now extend one internal
base class, so they report errors and cap entries the same way.
XMLToSitemapIndexStream.erroris a read-only getter for the first error
(errors[0], ornull). Assigning to it now throws aTypeErrorin strict
mode. The index parser also gains theerrorsarray (capped at
LIMITS.MAX_PARSER_ERRORS) anderrorCount, where it previously kept only
the first error.XMLToSitemapItemStreamreports a sitemap with more than 50,000<url>
entries once, instead of logging and recording an error for every dropped
entry, soerrorCountand the log no longer grow with the overflow. It
gains the index parser'smaxEntriesoption,entryCountproperty and
'maxEntriesExceeded'event, and anerrorgetter.urlCountstill works
as a deprecated alias forentryCount.parseSitemapstill resolves with
the first 50,000 items, but it now stops reading once the cap is crossed
and destroys the source stream, asparseSitemapIndexalready did.- Both parsers now fail on a truncated document (an element left unclosed
when the input ends), like any other malformed XML. Before, the
unfinished entry was dropped silently.parseSitemapand
parseSitemapIndexreject in that case, andparseSitemapnow also
rejects when the source stream fails instead of never settling. - The index parser's warnings for an invalid
<loc>or<lastmod>are now
logged as one message, the same text recorded inerrors. The recorded
lastmod error now readsInvalid lastmod date format in sitemap index: ….
SitemapAndIndexStream destroys itself on error
- When a sitemap stream errors, or
getSitemapStreamthrows or returns an
invalid tuple,SitemapAndIndexStreamnow destroys itself and emits the
error once. It used to emit the error without stopping, so the same failure
could be emitted twice, and agetSitemapStreamfailure was followed by a
genericNo sitemap stream availableerror. Code that listened forerror
and kept writing now gets write-after-destroy errors instead.
sitemap --index --gzip names sitemaps sitemap-N.xml.gz
- With
--gzip,sitemap --indexnow writes each sitemap tositemap-N.xml.gz
and lists that URL in the index, assimpleSitemapAndIndexalready did. It
used to write gzipped data tositemap-N.xmland index it under that name,
so crawlers fetched gzip bytes from a.xmlURL. Update scripts that upload
or servesitemap-*.xmlafter a gzipped run.
SitemapStream reports item errors as stream errors
SitemapStreamandSitemapItemStreamnow report a failure on an item
through the stream:write()no longer throws, and the stream emits
errorand is destroyed. This covers an invalid item at
ErrorLevel.THROW, an itemnormalizeURLrejects (such as an invalid
lastmod), and an item that can't be serialized (such as a non-string
video attribute atErrorLevel.SILENT). Before, the error was thrown out
ofwrite(), so a piped source crashed the process with an uncaught
exception. Code that wrappedwrite()intry/catchmust listen for
errorinstead, or usepipeline().SitemapStreamchecksxmlns.customin its constructor, so an invalid
custom namespace now throws fromnew SitemapStream()instead of from the
firstwrite().SitemapStreamserializes each item itself instead of piping it through an
internalSitemapItemStream, so it now outputs one chunk per item. The XML
is unchanged.
Stricter priority and alternate-link validation
- A
priorityofNaNis now reported as aPriorityInvalidError: it
throws atErrorLevel.THROWand warns atWARN. It used to skip
validation and write<priority>NaN</priority>at every level. - An alternate link with no
langorhreflangis now reported as the new
InvalidLinkHreflangerror atWARNandTHROW, and dropped from the
output atWARNandSILENT.SitemapItemStream, which doesn't
validate, drops it too, with no warning. Every level used to fail with a
TypeErrorfrom the XML writer. Writing the link withouthreflangwould
produce theandroidLinkmarkup, which parses back asandroidLink.
Error classes are exported by name; UndefinedTargetFolder removed
index.tslists the error classes explicitly instead of
export * from './lib/errors.js', like the public types, so a new error
class is public only once it is added there.UndefinedTargetFolderis removed. Nothing has thrown it since 7.0.0,
which removedcreateSitemapsAndIndex, its only user.
Changed
- The CLI parses arguments with Node's built-in
util.parseArgsinstead of
theargpackage, which is no longer a dependency (#471). Unknown options
and a non-integer--limitnow print a message and exit with code 2 instead
of throwing an uncaught error. - Test suite migrated from Jest to the built-in Node.js test runner. Coverage
thresholds moved fromjest.config.cjsinto thetest:coveragescript and are
enforced in CI by a dedicated job. - CI now runs on Linux, Windows and macOS across Node 22/24/26. Lint, formatting
and typechecking run once rather than on every matrix leg. - Stricter
<loc>validation in the sitemap parser:XMLToSitemapItemStream
now validates<loc>values with the samevalidateURLused by the sitemap
index parser, adding a URL-parseability check on top of the existing length
and protocol checks. A malformed URL that passed the old checks but fails
new URL()is now dropped with a warning (or throws under
ErrorLevel.THROW). Warn/error messages for invalid<loc>values changed to
theInvalid URL in sitemap: …form. validateURLthrowsInvalidURLError, a new subclass of
InvalidHostnameError(soinstanceof InvalidHostnameErrorstill matches).
Its message readsInvalid URL "…": …instead ofInvalid hostname "…": …,
and it shows at most the first 100 characters of the offending value.- Stricter video validation in
validateSMIOptions: a videoduration,
ratingorview_countthat isNaN,Infinityor otherwise non-numeric
is now reported as invalid (and throws underErrorLevel.THROW). Previously
only out-of-range finite numbers were caught, soNaNslipped through. XMLToSitemapItemStreamnow buffers each element's character data and
handles it once at the next tag boundary: text and CDATA chunks within one
element are joined, surrounding whitespace is trimmed for CDATA as it already
was for text, whitespace between chunks is kept (Episode <![CDATA[One]]>
now parses asEpisode One, notEpisodeOne), and stray character data
between elements is reported against
the enclosing element instead of overwriting the previous one. Character data
longer thanLIMITS.MAX_PARSER_CHAR_DATA_LENGTH(64 KiB) for one element is
dropped with a warning.- Internal: the parser's duplicated
text/cdataSAX handlers were collapsed
into one, and its inline bounds checks (priority, video duration/rating/view
count, ISO dates) now delegate to shared guards inlib/validation.ts, with
the bounds defined once inLIMITS. XMLToSitemapIndexStreamnow handles character data the same way: text and
CDATA within one<loc>or<lastmod>are joined and trimmed, stray
character data is reported against the enclosing element, and values longer
thanLIMITS.MAX_PARSER_CHAR_DATA_LENGTHare dropped with a warning.
Unhandled CDATA is now logged asunhandled text for tag:instead of
unhandled cdata for tag:. Both parsers share one buffer,
createCharDataBufferinlib/utils.ts.XMLToSitemapIndexStreamenforces the 50,000-entry spec limit itself.
Entries pastLIMITS.MAX_SITEMAP_ITEM_LIMITare dropped with a single error
(which throws underErrorLevel.THROW), and a newentryCountproperty counts
every<sitemap>seen. Previously only theparseSitemapIndexwrapper capped
entries, so piping the stream directly had no limit.parseSitemapIndexstill
rejects an over-limit index and stops reading as soon as the limit is crossed,
but amaxEntriesabove 50,000 no longer raises the limit. A new
maxEntriesstream option lowers the cap, or raises it (Infinityturns it
off) for callers that knowingly parse oversized indexes. When the cap is
crossed the stream emits'maxEntriesExceeded', and logs the overflow only
if nothing listens for that event, soparseSitemapIndexrejects without
also logging.maxEntries: 0is allowed (nothing is emitted, and
parseSitemapIndexrejects on the first entry); a negative, fractional or
NaNvalue is rejected, andparseSitemapIndexdestroys the source stream
before rejecting.- Errors that
simpleSitemapAndIndexandSitemapAndIndexStreamwrap, such
asFailed to write sitemap files: …or agetSitemapStreamfailure, now
carry the original error ascause. SaxParserStreamOptions, the shared base ofXMLToSitemapItemStreamOptions
andXMLToSitemapIndexItemStreamOptions, is exported.LIMITSgains the bounds the validators use:MIN_PRIORITY,
MAX_PRIORITY,PRIORITY_STRING_REGEX,MIN_VIDEO_DURATION,
MAX_VIDEO_DURATION,MIN_VIDEO_RATING,MAX_VIDEO_RATINGand
MIN_VIDEO_VIEW_COUNT.LIMITS.MAX_URL_ENTRIESis deprecated. Nothing reads it; the parsers cap
entries atLIMITS.MAX_SITEMAP_ITEM_LIMIT(the same value), which their
maxEntriesoption overrides.
Fixed
- A string
priorityno longer crashesSitemapStreamwith
item.priority.toFixed is not a function(#393). A plain decimal string
such as'0.5'or'.5'is converted to a number (hex, binary and
exponent forms such as'0x1'or'1e-1'are not), and
SitemapItemLoose.prioritynow acceptsnumber | string. Any other value,
including an empty string, is reported as aPriorityInvalidErrorat
WARNandTHROW, and written as given atWARNandSILENT, like other
invalid values.isValidPriorityrejects non-numbers instead of coercing
them. isValidYesNoaccepts only a wholeyes,Yes,YES,no,NoorNO.
Its unanchored alternation used to accept any value that started withYES,
containedNOoryes, or ended inno, such asNOPEormaybe-yes, so
the parsers andvalidateSMIOptionslet those values through.XMLToSitemapItemStreamreports an invalidvideo:requires_subscription,
video:family_friendly,video:liveorplayer_locallow_embedvalue as a
warning and a parse error. It used to drop the value without a word. It also
trims whitespace aroundallow_embed, as it already does for element text.validateSMIOptionschecksrequires_subscription,liveand
player_loc:allow_embedas well asfamily_friendly, and reports an invalid
value with the newInvalidVideoYesNoerror.SitemapAndIndexStreamerrors withERR_STREAM_PREMATURE_CLOSEinstead of
hanging when a sitemap's write stream is destroyed before it finishes, at any
point in the sitemap. ASitemapAndIndexStreamdestroyed during rotation no
longer opens another sitemap file once the current one has finished.SitemapAndIndexStreamdestroys itself with the error when a sitemap's write
stream fails while the sitemap is still open, such asEACCESorEISDIR
when opening the file. It used to crash the process with an unhandled
errorevent.simpleSitemapAndIndexnow rejects in that case.- Destroying a
SitemapAndIndexStreamalso destroys the open sitemap and its
write stream, so the file descriptor is released. SitemapAndIndexStreamdestroys the streamsgetSitemapStreamreturned when
it rejects the tuple, instead of leaving their file open.- At
ErrorLevel.THROW,SitemapAndIndexStreamfails when the index rejects a
sitemap's index item, such as a non-http(s) URL. It used to drop the error,
writing the sitemap file without listing it in the index. simpleSitemapAndIndexand the CLI connect each sitemap to its gzip and file
streams withpipeline(), so a gzip failure fails the sitemap instead of
leaving the write stream open.SitemapAndIndexStreamfails with the error, instead of leaving an unhandled
promise rejection, when writing the first item of a sitemap after rotation
throws.- At
ErrorLevel.THROW,SitemapIndexStreamno longer outputs a partial
<sitemap><loc>element before it fails on an invalidlastmod. simpleSitemapAndIndexand the CLI validate each sitemap URL before they
create its file, so a URL the index rejects no longer leaves an empty
sitemap file behind. They now fail on such a URL at every error level.sitemap --indexprints a stream error and exits with code 1 instead of
crashing with an unhandlederrorevent.sitemap --validateexits with code 1 when the sitemap is invalid and 2 when
validation cannot run (xmllint missing or not executable, or schema not
found). It used to exit 0 in every case, so scripts and CI could not detect a
failed validation. An xmllint that fails to start for a reason other than
ENOENT, such asEACCES, now rejectsxmlLint()with[error]alone
instead of[error, ''], so it is no longer reported as an invalid sitemap.sitemap --prependwrites every entry of the prepended sitemap, then the new
URLs. Both sources were piped into one stream, so whichever ended first
ended the sitemap: a large prepended file was cut off after its first entries
and failed with a write-after-end error.
A prepended file with more than 50,000 URLs, the most a sitemap may hold,
now fails with exit code 1 instead of being silently truncated.sitemap --parse,--prependand the default mode print a stream error,
such as a parse error or a missing input file, and exit with code 1 instead
of crashing with an unhandlederrorevent. In those modes a reader that
closes stdout early, such as| head, is not treated as an error. In
--indexmode it still is (exit code 1), because the sitemap files written
so far are left incomplete.xmlLint()andsitemap --validatefind the bundledschema/directory
when the package is installed as a dependency. The lookup searched relative
toprocess.cwd(), so outside this repository it threw a synchronous
"Schema directory not found" error that crashed the CLI. It now walks up from
the module's own directory, and a missing schema rejects the promise instead
of throwing.xmlLint()destroys aReadableinput if xmllint exits or fails to start
before reading all of it, so the source no longer stays paused with its file
descriptor open.SitemapStreamandSitemapIndexStreamcheck their publiclevelfield
when they warn, so changinglevelafter construction takes effect again,
as it did in 9.0.1.@types/nodeis a runtime dependency again, as it was in 9.0.1, because the
published type definitions import Node's stream types. The range now covers
every supported Node major (^22 || ^24 || ^26), so npm can share a
consumer's own copy.XMLToSitemapItemStreamnow treats CDATA sections as ordinary character data
in every tag. Previously only the 8 tags from issue #445 (loc,image:loc,
video:title,video:description,news:name,news:title,
image:caption,image:title) accepted CDATA; content in any other tag was
logged as unhandled. Seedocs/adr/0001-cdata-is-character-data.md.xmlLint()no longer probes for the binary withwhich, which is not a
command on Windows and made validation reportXMLLintUnavailablethere even
when xmllint was installed. A missing binary is now detected viaENOENTfrom
the spawn itself, which is correct on every platform.- The published tarball now contains only
dist/andschema/(via afiles
allowlist), so maintainer files such asCLAUDE.mdno longer ship to
consumers (#480). - The CLI locates
package.jsonby walking up from its own directory instead of
a hardcoded relative depth, so--versionno longer breaks when the build
output layout changes. sitemap --indexreports an invalid--limitwith the same message as
simpleSitemapAndIndex'sInvalidLimitError, prefixed with--limit:, and
accepts only decimal digits: forms such as1e3,0x10or250are
rejected rather than converted.InvalidLimitErrorsays the limit must be an integer between 1 and 50000,
rather than a number, since fractional limits are rejected too.XMLToSitemapItemStreamno longer adds an empty{ url: '', lang: '' }
entry tolinksfor anxhtml:linkthat lacksrelorhrefor has an
unsupportedrel. It still warns and records a parse error.XMLToSitemapItemStreamandXMLToSitemapIndexStreamcopy their options
instead of settingobjectModeon the object the caller passed in.
Documentation
api.mdnow documentsXMLToSitemapIndexStreamandparseSitemapIndex, and
the 50,000-entry and error-array caps the parsers enforce.api.mdno longer claimssimpleSitemapAndIndexaccepts an absolute
destinationDir; absolute paths have been rejected since 9.0.1 (#478).- JSDoc examples for
parseSitemapandparseSitemapIndexuseimportand
current APIs instead ofrequire()and the removedcreateSitemap.