npm sitemap 10.0.0

3 hours ago

[BREAKING CHANGES]

Dropped CommonJS build — the package is now ESM-only

  • require('sitemap') is no longer supported on Node versions without
    built-in require(esm). The dist/cjs build, the require export condition,
    and tsconfig.cjs.json are all gone; exports now resolves to dist/index.js
    for every consumer. Migrate to import { SitemapStream } from 'sitemap'.
  • Node.js >=22.22.1 now required (previously >=20.19.5). CommonJS
    consumers can still load this package through require(esm), which landed
    unflagged in 22.12; the floor is 22.22.1 to match the development toolchain.
  • Build output flattened from dist/esm/* and dist/cjs/* to dist/*. The
    bin entry is now dist/cli.js.

simpleSitemapAndIndex defaults to 45,000 URLs per sitemap

  • The default limit drops from 50,000 to 45,000, the same default
    SitemapAndIndexStream and 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; pass limit: 50000 to keep the old output.

SitemapStream writes an empty sitemap instead of erroring

  • Ending a SitemapStream without writing any items now produces a
    well-formed empty <urlset> instead of failing with EmptySitemap
    (#394, #421). This matches SitemapIndexStream, which already wrote an empty
    <sitemapindex>. EmptySitemap is still exported but deprecated and no
    longer thrown. An empty <urlset> does not pass strict sitemap.xsd
    validation, which requires at least one <url>; search engines accept it.

One logger option across the library; xmlNS renamed to xmlns

  • SitemapStream, SitemapIndexStream, SitemapAndIndexStream and
    simpleSitemapAndIndex accept the same logger option the parsers already
    had (#348). Validation warnings that went straight to console.warn now go
    through it. Pass false to silence. The Logger type 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. Under ErrorLevel.THROW a custom errorHandler receives
    these errors too, like every other validation error; the default handler
    still throws.
  • The SitemapStream property xmlNS is now xmlns, matching the option
    name (#300). xmlNS still works as a deprecated alias. SitemapStream.errorHandler is always set now; it defaults to
    a handler that warns through logger.

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.error is a read-only getter for the first error
    (errors[0], or null). Assigning to it now throws a TypeError in strict
    mode. The index parser also gains the errors array (capped at
    LIMITS.MAX_PARSER_ERRORS) and errorCount, where it previously kept only
    the first error.
  • XMLToSitemapItemStream reports a sitemap with more than 50,000 <url>
    entries once, instead of logging and recording an error for every dropped
    entry, so errorCount and the log no longer grow with the overflow. It
    gains the index parser's maxEntries option, entryCount property and
    'maxEntriesExceeded' event, and an error getter. urlCount still works
    as a deprecated alias for entryCount. parseSitemap still resolves with
    the first 50,000 items, but it now stops reading once the cap is crossed
    and destroys the source stream, as parseSitemapIndex already 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. parseSitemap and
    parseSitemapIndex reject in that case, and parseSitemap now 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 in errors. The recorded
    lastmod error now reads Invalid lastmod date format in sitemap index: ….

SitemapAndIndexStream destroys itself on error

  • When a sitemap stream errors, or getSitemapStream throws or returns an
    invalid tuple, SitemapAndIndexStream now destroys itself and emits the
    error once. It used to emit the error without stopping, so the same failure
    could be emitted twice, and a getSitemapStream failure was followed by a
    generic No sitemap stream available error. Code that listened for error
    and kept writing now gets write-after-destroy errors instead.

sitemap --index --gzip names sitemaps sitemap-N.xml.gz

  • With --gzip, sitemap --index now writes each sitemap to sitemap-N.xml.gz
    and lists that URL in the index, as simpleSitemapAndIndex already did. It
    used to write gzipped data to sitemap-N.xml and index it under that name,
    so crawlers fetched gzip bytes from a .xml URL. Update scripts that upload
    or serve sitemap-*.xml after a gzipped run.

SitemapStream reports item errors as stream errors

  • SitemapStream and SitemapItemStream now report a failure on an item
    through the stream: write() no longer throws, and the stream emits
    error and is destroyed. This covers an invalid item at
    ErrorLevel.THROW, an item normalizeURL rejects (such as an invalid
    lastmod), and an item that can't be serialized (such as a non-string
    video attribute at ErrorLevel.SILENT). Before, the error was thrown out
    of write(), so a piped source crashed the process with an uncaught
    exception. Code that wrapped write() in try/catch must listen for
    error instead, or use pipeline().
  • SitemapStream checks xmlns.custom in its constructor, so an invalid
    custom namespace now throws from new SitemapStream() instead of from the
    first write().
  • SitemapStream serializes each item itself instead of piping it through an
    internal SitemapItemStream, so it now outputs one chunk per item. The XML
    is unchanged.

Stricter priority and alternate-link validation

  • A priority of NaN is now reported as a PriorityInvalidError: it
    throws at ErrorLevel.THROW and warns at WARN. It used to skip
    validation and write <priority>NaN</priority> at every level.
  • An alternate link with no lang or hreflang is now reported as the new
    InvalidLinkHreflang error at WARN and THROW, and dropped from the
    output at WARN and SILENT. SitemapItemStream, which doesn't
    validate, drops it too, with no warning. Every level used to fail with a
    TypeError from the XML writer. Writing the link without hreflang would
    produce the androidLink markup, which parses back as androidLink.

Error classes are exported by name; UndefinedTargetFolder removed

  • index.ts lists 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.
  • UndefinedTargetFolder is removed. Nothing has thrown it since 7.0.0,
    which removed createSitemapsAndIndex, its only user.

Changed

  • The CLI parses arguments with Node's built-in util.parseArgs instead of
    the arg package, which is no longer a dependency (#471). Unknown options
    and a non-integer --limit now 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 from jest.config.cjs into the test:coverage script 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 same validateURL used 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
    the Invalid URL in sitemap: … form.
  • validateURL throws InvalidURLError, a new subclass of
    InvalidHostnameError (so instanceof InvalidHostnameError still matches).
    Its message reads Invalid URL "…": … instead of Invalid hostname "…": …,
    and it shows at most the first 100 characters of the offending value.
  • Stricter video validation in validateSMIOptions: a video duration,
    rating or view_count that is NaN, Infinity or otherwise non-numeric
    is now reported as invalid (and throws under ErrorLevel.THROW). Previously
    only out-of-range finite numbers were caught, so NaN slipped through.
  • XMLToSitemapItemStream now 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 as Episode One, not EpisodeOne), and stray character data
    between elements is reported against
    the enclosing element instead of overwriting the previous one. Character data
    longer than LIMITS.MAX_PARSER_CHAR_DATA_LENGTH (64 KiB) for one element is
    dropped with a warning.
  • Internal: the parser's duplicated text/cdata SAX handlers were collapsed
    into one, and its inline bounds checks (priority, video duration/rating/view
    count, ISO dates) now delegate to shared guards in lib/validation.ts, with
    the bounds defined once in LIMITS.
  • XMLToSitemapIndexStream now 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
    than LIMITS.MAX_PARSER_CHAR_DATA_LENGTH are dropped with a warning.
    Unhandled CDATA is now logged as unhandled text for tag: instead of
    unhandled cdata for tag:. Both parsers share one buffer,
    createCharDataBuffer in lib/utils.ts.
  • XMLToSitemapIndexStream enforces the 50,000-entry spec limit itself.
    Entries past LIMITS.MAX_SITEMAP_ITEM_LIMIT are dropped with a single error
    (which throws under ErrorLevel.THROW), and a new entryCount property counts
    every <sitemap> seen. Previously only the parseSitemapIndex wrapper capped
    entries, so piping the stream directly had no limit. parseSitemapIndex still
    rejects an over-limit index and stops reading as soon as the limit is crossed,
    but a maxEntries above 50,000 no longer raises the limit. A new
    maxEntries stream option lowers the cap, or raises it (Infinity turns 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, so parseSitemapIndex rejects without
    also logging. maxEntries: 0 is allowed (nothing is emitted, and
    parseSitemapIndex rejects on the first entry); a negative, fractional or
    NaN value is rejected, and parseSitemapIndex destroys the source stream
    before rejecting.
  • Errors that simpleSitemapAndIndex and SitemapAndIndexStream wrap, such
    as Failed to write sitemap files: … or a getSitemapStream failure, now
    carry the original error as cause.
  • SaxParserStreamOptions, the shared base of XMLToSitemapItemStreamOptions
    and XMLToSitemapIndexItemStreamOptions, is exported.
  • LIMITS gains the bounds the validators use: MIN_PRIORITY,
    MAX_PRIORITY, PRIORITY_STRING_REGEX, MIN_VIDEO_DURATION,
    MAX_VIDEO_DURATION, MIN_VIDEO_RATING, MAX_VIDEO_RATING and
    MIN_VIDEO_VIEW_COUNT.
  • LIMITS.MAX_URL_ENTRIES is deprecated. Nothing reads it; the parsers cap
    entries at LIMITS.MAX_SITEMAP_ITEM_LIMIT (the same value), which their
    maxEntries option overrides.

Fixed

  • A string priority no longer crashes SitemapStream with
    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.priority now accepts number | string. Any other value,
    including an empty string, is reported as a PriorityInvalidError at
    WARN and THROW, and written as given at WARN and SILENT, like other
    invalid values. isValidPriority rejects non-numbers instead of coercing
    them.
  • isValidYesNo accepts only a whole yes, Yes, YES, no, No or NO.
    Its unanchored alternation used to accept any value that started with YES,
    contained NO or yes, or ended in no, such as NOPE or maybe-yes, so
    the parsers and validateSMIOptions let those values through.
  • XMLToSitemapItemStream reports an invalid video:requires_subscription,
    video:family_friendly, video:live or player_loc allow_embed value as a
    warning and a parse error. It used to drop the value without a word. It also
    trims whitespace around allow_embed, as it already does for element text.
  • validateSMIOptions checks requires_subscription, live and
    player_loc:allow_embed as well as family_friendly, and reports an invalid
    value with the new InvalidVideoYesNo error.
  • SitemapAndIndexStream errors with ERR_STREAM_PREMATURE_CLOSE instead of
    hanging when a sitemap's write stream is destroyed before it finishes, at any
    point in the sitemap. A SitemapAndIndexStream destroyed during rotation no
    longer opens another sitemap file once the current one has finished.
  • SitemapAndIndexStream destroys itself with the error when a sitemap's write
    stream fails while the sitemap is still open, such as EACCES or EISDIR
    when opening the file. It used to crash the process with an unhandled
    error event. simpleSitemapAndIndex now rejects in that case.
  • Destroying a SitemapAndIndexStream also destroys the open sitemap and its
    write stream, so the file descriptor is released.
  • SitemapAndIndexStream destroys the streams getSitemapStream returned when
    it rejects the tuple, instead of leaving their file open.
  • At ErrorLevel.THROW, SitemapAndIndexStream fails 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.
  • simpleSitemapAndIndex and the CLI connect each sitemap to its gzip and file
    streams with pipeline(), so a gzip failure fails the sitemap instead of
    leaving the write stream open.
  • SitemapAndIndexStream fails with the error, instead of leaving an unhandled
    promise rejection, when writing the first item of a sitemap after rotation
    throws.
  • At ErrorLevel.THROW, SitemapIndexStream no longer outputs a partial
    <sitemap><loc> element before it fails on an invalid lastmod.
  • simpleSitemapAndIndex and 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 --index prints a stream error and exits with code 1 instead of
    crashing with an unhandled error event.
  • sitemap --validate exits 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 as EACCES, now rejects xmlLint() with [error] alone
    instead of [error, ''], so it is no longer reported as an invalid sitemap.
  • sitemap --prepend writes 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, --prepend and 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 unhandled error event. In those modes a reader that
    closes stdout early, such as | head, is not treated as an error. In
    --index mode it still is (exit code 1), because the sitemap files written
    so far are left incomplete.
  • xmlLint() and sitemap --validate find the bundled schema/ directory
    when the package is installed as a dependency. The lookup searched relative
    to process.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 a Readable input if xmllint exits or fails to start
    before reading all of it, so the source no longer stays paused with its file
    descriptor open.
  • SitemapStream and SitemapIndexStream check their public level field
    when they warn, so changing level after construction takes effect again,
    as it did in 9.0.1.
  • @types/node is 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.
  • XMLToSitemapItemStream now 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. See docs/adr/0001-cdata-is-character-data.md.
  • xmlLint() no longer probes for the binary with which, which is not a
    command on Windows and made validation report XMLLintUnavailable there even
    when xmllint was installed. A missing binary is now detected via ENOENT from
    the spawn itself, which is correct on every platform.
  • The published tarball now contains only dist/ and schema/ (via a files
    allowlist), so maintainer files such as CLAUDE.md no longer ship to
    consumers (#480).
  • The CLI locates package.json by walking up from its own directory instead of
    a hardcoded relative depth, so --version no longer breaks when the build
    output layout changes.
  • sitemap --index reports an invalid --limit with the same message as
    simpleSitemapAndIndex's InvalidLimitError, prefixed with --limit:, and
    accepts only decimal digits: forms such as 1e3, 0x10 or 250 are
    rejected rather than converted.
  • InvalidLimitError says the limit must be an integer between 1 and 50000,
    rather than a number, since fractional limits are rejected too.
  • XMLToSitemapItemStream no longer adds an empty { url: '', lang: '' }
    entry to links for an xhtml:link that lacks rel or href or has an
    unsupported rel. It still warns and records a parse error.
  • XMLToSitemapItemStream and XMLToSitemapIndexStream copy their options
    instead of setting objectMode on the object the caller passed in.

Documentation

  • api.md now documents XMLToSitemapIndexStream and parseSitemapIndex, and
    the 50,000-entry and error-array caps the parsers enforce.
  • api.md no longer claims simpleSitemapAndIndex accepts an absolute
    destinationDir; absolute paths have been rejected since 9.0.1 (#478).
  • JSDoc examples for parseSitemap and parseSitemapIndex use import and
    current APIs instead of require() and the removed createSitemap.

Don't miss a new sitemap release

NewReleases is sending notifications on new releases.