als-sitemap
v2.1.0
Published
Sitemap XML builder and parser based on als-document
Maintainers
Readme
als-sitemap
ESM sitemap builder and parser built on top of als-document. Runs in Node and in the browser.
The package default export is SitemapIndex. It is both the index sitemap class and the library namespace for helpers such as SitemapIndex.Sitemap, SitemapIndex.urlset(), SitemapIndex.parse(), and SitemapIndex.buildSitemaps().
Sitemap and SitemapIndex both extend XmlDocument, which extends Document from als-document. XML is built as a DOM tree, including the XML declaration node.
Install
npm install als-sitemapImport
import SitemapIndex from 'als-sitemap'
const indexXml = new SitemapIndex(['/sitemaps/pages.xml'], { host: 'https://example.com' }).toString()
const urlsetXml = SitemapIndex.urlset(['/about'], { host: 'https://example.com' })Named imports are also available:
import { Sitemap, buildSitemaps, parse, validators } from 'als-sitemap'When working inside this repository, import from ./lib/index.mjs:
import SitemapIndex from './lib/index.mjs'Browser
The whole library (and als-document) runs in the browser. npm run build bundles everything into self-contained files in dist/ (no Node built-ins, ~27 KB minified):
als-sitemap.esm.js/.esm.min.js— ES moduleals-sitemap.iife.js/.iife.min.js— globalalsSitemapfor a<script>tag
<script src="als-sitemap.iife.min.js"></script>
<script>
const xml = alsSitemap.urlset(['/about'], { host: 'https://example.com' })
</script>import SitemapIndex, { parse } from './als-sitemap.esm.min.js'Bundlers (Vite, webpack, esbuild) pick the browser bundle automatically through the browser export condition. Everything works in the browser except gzip: toGzip() and buildSitemaps({ gzip: true }) need Node and throw a clear error in the browser build. byteLength falls back to TextEncoder when Buffer is unavailable.
Static Helpers
SitemapIndex.Sitemap
SitemapIndex.SitemapIndex
SitemapIndex.namespaces
SitemapIndex.validators
SitemapIndex.urlset(entries, options)
SitemapIndex.index(entries, options)
SitemapIndex.parse(xml, options)
SitemapIndex.generate(data, options)
SitemapIndex.buildSitemaps(entries, options)
SitemapIndex.robots(options)
SitemapIndex.sitemap(entries, options)
SitemapIndex.sitemapIndex(entries, options)Basic Urlset
import SitemapIndex from 'als-sitemap'
const sitemap = new SitemapIndex.Sitemap([
{
loc: 'https://example.com/',
lastmod: new Date('2026-06-09T00:00:00Z'),
changefreq: 'daily',
priority: 0.8
}
])
console.log(sitemap.toString())Output starts with:
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">You can also add entries one by one:
const sitemap = new SitemapIndex.Sitemap()
sitemap.add('https://example.com/')
sitemap.add({ loc: 'https://example.com/about', lastmod: '2026-06-09' })
const xml = sitemap.urlset()One Host For Many Paths
If every URL belongs to the same site, pass the host once and use paths in entries.
const sitemap = new SitemapIndex.Sitemap([
'/',
'/about',
{ loc: '/blog/post-1', lastmod: '2026-06-09' }
], { host: 'https://example.com' })
console.log(sitemap.toString())Generated <loc> values are absolute:
<loc>https://example.com/</loc>
<loc>https://example.com/about</loc>
<loc>https://example.com/blog/post-1</loc>host, origin, and baseUrl are accepted option names. The host is validated once when the document is created. Relative paths are resolved before entry validation.
Validation
Validation is enabled by default. Invalid entries are skipped and errors are collected.
const sitemap = new SitemapIndex.Sitemap()
sitemap.add({ loc: 'not a url' })
console.log(sitemap.entries) // []
console.log(sitemap.errors) // ['not a url is not a valid URL']Disable validation with validate: false:
const sitemap = new SitemapIndex.Sitemap([{ loc: 'not a url' }], { validate: false })entries and errors return copies, so external code cannot mutate internal state. Nested extension URLs (image.loc, image.license, video.thumbnail_loc, video.content_loc, video.player_loc, alternate.href) are validated too.
Image, Video And News
Namespaces are added only when the matching extension is used.
import SitemapIndex from 'als-sitemap'
const xml = new SitemapIndex.Sitemap([
{
loc: '/post',
images: [
{
loc: '/image.jpg',
caption: 'Preview',
title: 'Image title',
geo_location: 'Jerusalem, Israel',
license: '/license'
}
],
videos: [
{
thumbnail_loc: '/thumb.jpg',
title: 'Demo',
description: 'Demo video',
content_loc: '/video.mp4',
player_loc: {
loc: '/player',
allow_embed: 'yes',
autoplay: 'ap=1'
},
duration: 120,
tags: ['demo', 'product']
}
],
news: {
publication: { name: 'Example News', language: 'en' },
publication_date: '2026-06-09',
title: 'News title',
keywords: 'example, sitemap'
}
}
], { host: 'https://example.com' }).toString()The host is also applied to extension URL fields such as image.loc, image.license, video.thumbnail_loc, video.content_loc, video.player_loc, and video.uploader.info.
Videos support the full Google field set. Plain fields: duration, expiration_date, rating, view_count, publication_date, family_friendly, category, requires_subscription, live, tags. Fields that carry attributes use a { value, ...attributes } shape (a plain string is treated as value):
{
restriction: { relationship: 'allow', value: 'US CA' },
platform: { relationship: 'deny', value: 'tv' },
uploader: { info: 'https://example.com/users/bob', value: 'Bob' },
prices: [{ currency: 'EUR', type: 'rent', value: '1.99' }]
}price is accepted as an alias for a single prices entry. News entries also support genres, keywords, and stock_tickers.
Localized Versions (hreflang)
Add alternates to an entry to emit xhtml:link rel="alternate" tags for localized pages. The xmlns:xhtml namespace is added automatically.
import SitemapIndex from 'als-sitemap'
const xml = new SitemapIndex.Sitemap([
{
loc: '/en/page',
alternates: [
{ hreflang: 'en', href: '/en/page' },
{ hreflang: 'de', href: '/de/page' },
{ hreflang: 'x-default', href: '/' }
]
}
], { host: 'https://example.com' }).toString()lang/url are accepted as aliases for hreflang/href. href is resolved against host and validated like any other URL. Parsing returns the same alternates array on each url entry.
<xhtml:link rel="alternate" hreflang="de" href="https://example.com/de/page"/>XSL Stylesheet
Pass xslUrl (or stylesheet) to emit an <?xml-stylesheet?> processing instruction so the sitemap renders in a browser. The href is resolved against host.
const xml = new SitemapIndex.Sitemap(['/'], { host: 'https://example.com', xslUrl: '/sitemap.xsl' }).toString()<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="https://example.com/sitemap.xsl"?>
<urlset ...>Gzip And Size Limits
toGzip() returns a gzipped Buffer of the document (Node only). Available on both Sitemap and SitemapIndex.
const sitemap = new SitemapIndex.Sitemap(['/'], { host: 'https://example.com' })
const buffer = sitemap.toGzip()The sitemap spec caps a single file at 50,000 entries and 50 MB. Both classes expose:
byteLength: UTF-8 byte size of the rendered document.withinLimits:falsewhen entries exceedmaxEntries(50,000) or bytes exceedmaxBytes(50 MB).
Adding a 50,001st entry also pushes a message into errors. The limits are static (Sitemap.maxEntries, Sitemap.maxBytes) and can be overridden in a subclass. Use buildSitemaps to split large sets automatically.
Sitemap Index
import SitemapIndex from 'als-sitemap'
const indexXml = new SitemapIndex([
{ loc: '/pages.xml', lastmod: '2026-06-09' },
'/blog.xml'
], { host: 'https://example.com' }).toString()Or use the helper:
const indexXml = SitemapIndex.index(['/pages.xml'], { host: 'https://example.com' })Helpers
urlset(entries) is a shortcut for new Sitemap(entries).toString().
import SitemapIndex from 'als-sitemap'
const xml = SitemapIndex.urlset(['/'], { host: 'https://example.com' })generate(data) returns an object with XML strings for each provided sitemap type.
import SitemapIndex from 'als-sitemap'
const result = SitemapIndex.generate({
urlsSet: [{ loc: '/' }],
index: [{ loc: '/pages.xml' }],
image: [
{ page: '/post', loc: '/image.jpg' }
]
}, { host: 'https://example.com' })
console.log(result.urlsSet)
console.log(result.index)
console.log(result.image)Build Multiple Sitemap Files
buildSitemaps(entries, options) groups entries, splits large groups, and creates an index file.
import SitemapIndex from 'als-sitemap'
const files = SitemapIndex.buildSitemaps([
'/',
'/about',
'/blog/post-1',
'/blog/post-2'
], { host: 'https://example.com', limit: 50000 })
console.log(Object.keys(files))
// ['pages.xml', 'blog.xml', 'sitemap.xml']Options:
group(url, entry): returns the group name. Default groups top-level pages aspages, and nested paths by first path segment.limit: max entries per sitemap file. Default is50000.origin: origin used for links in the index. Default is inferred from the first entry.hostorbaseUrl: aliases fororigin; useful when entries are relative paths.dir: directory prefix for index links. Default is/.indexName: index file name. Default issitemap.xml.fileName(name): file name for a group that was not split.gzip: whentrue, every file (including the index) is gzipped. Keys gain a.gzsuffix, values areBuffers, and index links point to the.gzfiles. (Node only.)robots: when set, also emit arobots.txt. Entries withallow: falseare excluded from the sitemaps and added asDisallow:lines, and aSitemap:line points to the generated index. Passtruefor defaults or an object ({ userAgent, allow, disallow, crawlDelay, host, sitemaps, groups, fileName }).
Custom grouping:
const files = SitemapIndex.buildSitemaps(entries, {
host: 'https://example.com',
group: (url, entry) => url.pathname.startsWith('/docs/') ? 'docs' : 'pages',
dir: '/sitemaps/',
indexName: 'sitemaps.xml',
fileName: name => `${name}-sitemap.xml`
})robots.txt
robots(options) builds a robots.txt from path-pattern rules. It is path-based (not a per-URL list), so it scales: block a whole section with one line.
import SitemapIndex from 'als-sitemap'
const txt = SitemapIndex.robots({
host: 'https://example.com',
sitemaps: ['/sitemap.xml'],
groups: [
{ userAgent: '*', disallow: ['/admin', '/cart'], crawlDelay: 10 },
{ userAgent: ['Googlebot', 'Bingbot'], allow: ['/'] }
]
})User-agent: *
Disallow: /admin
Disallow: /cart
Crawl-delay: 10
User-agent: Googlebot
User-agent: Bingbot
Allow: /
Host: https://example.com
Sitemap: https://example.com/sitemap.xmlA group with no allow/disallow means "allow all" (Disallow:). userAgent accepts an array to share rules across agents. Relative sitemaps are resolved against host.
buildSitemaps integrates with it: mark entries allow: false (the default is true, so you never set it on normal pages) to keep them out of the sitemaps and turn them into Disallow: lines, then pass robots to emit the file.
const files = SitemapIndex.buildSitemaps([
'/',
'/about',
{ loc: '/admin', allow: false }
], { host: 'https://example.com', robots: { crawlDelay: 5 } })
console.log(files['robots.txt'])
// User-agent: *
// Disallow: /admin
// Crawl-delay: 5
//
// Sitemap: https://example.com/sitemap.xmlParse XML
import SitemapIndex from 'als-sitemap'
const xml = SitemapIndex.urlset([
{
loc: 'https://example.com/post',
images: [{ loc: 'https://example.com/image.jpg', caption: 'A & B' }]
}
])
const result = SitemapIndex.parse(xml)
console.log(result.urlsSet[0].loc)
console.log(result.image[0])Parsed result shape:
{
errors: [],
urlsSet: [
{
loc: 'https://example.com/post',
images: [{ loc: 'https://example.com/image.jpg', caption: 'A & B' }]
}
],
index: [],
image: [
{ page: 'https://example.com/post', loc: 'https://example.com/image.jpg', caption: 'A & B' }
],
video: [],
news: []
}Parse only selected parts:
SitemapIndex.parse(xml, { urlsSet: true })
SitemapIndex.parse(xml, { image: true })
SitemapIndex.parse(xml, { index: true })Static Values And Validators
import SitemapIndex from 'als-sitemap'
console.log(SitemapIndex.Sitemap.namespaces === SitemapIndex.namespaces) // true
console.log(SitemapIndex.validators.isValidURL('https://example.com/')) // true
console.log(SitemapIndex.validators.isValidDate('2026-06-09')) // true
console.log(SitemapIndex.validators.isValidPriority(0.5)) // trueAvailable namespaces:
SitemapIndex.namespaces.sitemap
SitemapIndex.namespaces.image
SitemapIndex.namespaces.video
SitemapIndex.namespaces.news
SitemapIndex.namespaces.xhtmlEntry Shape
Url entry:
{
loc: 'https://example.com/',
lastmod: '2026-06-09',
changefreq: 'daily',
priority: 0.8,
images: [],
videos: [],
news: {},
alternates: []
}Sitemap index entry:
{
loc: 'https://example.com/pages.xml',
lastmod: '2026-06-09'
}Scripts
npm test— run the test suite.npm run coverage— run tests with coverage.npm run build— build the browser bundles intodist/.
License
MIT
