sitemap.js

repository·master·Indexed 23 days ago

https://github.com/ekalinin/sitemap.js

A high-level streaming sitemap-generating library and CLI for creating XML sitemaps. It supports generating single sitemaps, large sitemap indexes, and serving sitemaps dynamically via web servers. Key features include SitemapStream for XML generation, simpleSitemapAndIndex for handling large URL lists, and XMLToSitemapItemStream for parsing existing sitemaps.

Tokens
14K
Snippets
28
Records
57
Agent score
83%

What's inside sitemap

  1. Serve a sitemap from an Express server

    master

    For websites with fewer than 50,000 URLs, you can serve a sitemap dynamically using an Express route. It is recommended to cache the generated sitemap in memory to avoid re-generating it on every request.

    Note: When serving, ensure you set the Content-Type to application/xml and Content-Encoding to gzip if you are compressing the output.

    // ESM
    import express from 'express'
    import { SitemapStream, streamToPromise } from 'sitemap'
    import { createGzip } from 'zlib'
    import { Readable } from 'stream'
    
    const app = express()
    let sitemap
    
    app.get('/sitemap.xml', function(req, res) {
      res.header('Content-Type', 'application/xml');
      res.header('Content-Encoding', 'gzip');
      // if we have a cached entry send it
      if (sitemap) {
        res.send(sitemap)
        return
      }
    
      try {
        const smStream = new SitemapStream({ hostname: 'https://example.com/' })
        const pipeline = smStream.pipe(createGzip())
    
        // pipe your entries or directly write them.
        smStream.write({ url: '/page-1/',  changefreq: 'daily', priority: 0.3 })
        smStream.write({ url: '/page-2/',  changefreq: 'monthly',  priority: 0.7 })
        smStream.write({ url: '/page-3/'})    // changefreq: 'weekly',  priority: 0.5
        smStream.write({ url: '/page-4/',   img: "http://urlTest.com" })
        /* or use
        Readable.from([{url: '/page-1'}...]).pipe(smStream)
        if you are looking to avoid writing your own loop.
        */
    
        // cache the response
        streamToPromise(pipeline).then(sm => sitemap = sm)
        // make sure to attach a write stream such as streamToPromise before ending
        smStream.end()
        // stream write the response
        pipeline.pipe(res).on('error', (e) => {throw e})
      } catch (e) {
        console.error(e)
        res.status(500).end()
      }
    })
    
    app.listen(3000, () => {
      console.log('listening')
    });
  2. Filter sitemap entries during parsing

    master

    You can selectively process or delete URLs from an existing sitemap by piping an XMLToSitemapItemStream through a custom Transform stream. This is useful for extracting specific subsets of URLs (e.g., only URLs containing /blog/) from a large sitemap file.

    import { createReadStream } from 'fs'
    import { Transform } from 'stream'
    import { XMLToSitemapItemStream } from 'sitemap'
    
    // Create a filter that only keeps certain URLs
    const filterStream = new Transform({
      objectMode: true,
      transform(item, encoding, callback) {
        // Only keep URLs containing '/blog/'
        if (item.url.includes('/blog/')) {
          callback(undefined, item)  // Keep this item
        } else {
          callback()  // Skip this item (effectively "deleting" it)
        }
      }
    })
    
    // Parse and filter
    createReadStream('./sitemap.xml')
      .pipe(new XMLToSitemapItemStream())
      .pipe(filterStream)
      .on('data', (item) => {
        console.log('Filtered URL:', item.url)
      })
  3. Create sitemap and index files for large URL lists

    master

    If you have more than 50,000 URLs, you should use simpleSitemapAndIndex or SitemapAndIndexStream to split your URLs into multiple sitemap files and generate a sitemap index.

    Using simpleSitemapAndIndex

    This is a high-level function that handles the creation of multiple sitemaps and an index file automatically.

    Using SitemapAndIndexStream

    For more control, use SitemapAndIndexStream. You must provide a getSitemapStream function that returns an array containing: [url_of_the_sitemap, sitemap_stream, write_stream]. This function is called every time a new sitemap file needs to be created.

    // ESM
    import { createReadStream, createWriteStream } from 'fs'
    import { resolve } from 'path'
    import { createGzip } from 'zlib'
    import { simpleSitemapAndIndex, lineSeparatedURLsToSitemapOptions } from 'sitemap'
    
    // writes sitemaps and index out to the destination you provide.
    simpleSitemapAndIndex({
      hostname: 'https://example.com',
      destinationDir: './',
      sourceData: lineSeparatedURLsToSitemapOptions(
        createReadStream('./your-data.json.txt')
      ),
      // sourceData can also be:
      // sourceData: [{ url: '/page-1/', changefreq: 'daily'}, ...],
      // or
      // sourceData: './your-data.json.txt',
      limit: 45000, // optional, default: 50000
      gzip: true, // optional, default: true
      publicBasePath: '/sitemaps/', // optional, default: './'
      xslUrl: 'https://example.com/sitemap.xsl', // optional XSL stylesheet
    }).then(() => {
      // Do follow up actions
    })
  4. Implement the getSitemapStream callback

    master

    The getSitemapStream callback is mandatory for SitemapAndIndexStream. It is called whenever the current sitemap reaches the configured limit.

    It must return a 3-element tuple:

    1. IndexItem | string: The entry that will be added to the sitemap index (usually the URL of the sitemap file being created).
    2. SitemapStream: The stream instance used to write the actual sitemap items.
    3. WriteStream: The destination stream (e.g., a file write stream) that the SitemapStream should be piped to. The SitemapAndIndexStream will wait for this write stream to finish before proceeding to the next sitemap.
    // Example implementation
    const getSitemapStream = (i: number) => {
      const sitemapStream = new SitemapStream();
      const path = `./sitemap-${i}.xml`;
      const writeStream = createWriteStream(path);
      
      // Pipe the sitemap stream to the file
      sitemapStream.pipe(writeStream);
      
      // Return the tuple: [URL for index, the sitemap stream, the file destination]
      return [`https://example.com/${path}`, sitemapStream, writeStream];
    };
  5. Configure Sitemap entry options

    master

    When building sitemap entries, you can use SitemapItem for strict, normalized entries or SitemapItemLoose for more flexible input before normalization.

    SitemapItem (Strict)

    Requires explicit arrays for img, video, and links.

    SitemapItemLoose (Flexible)

    Allows for single values or arrays for img and video, and provides alternative ways to specify dates:

    • lastmodfile: string | Buffer | URL
    • lastmodISO: string
    • lastmodrealtime: boolean
    export interface SitemapItem extends SitemapItemBase {
      img: Img[];
      video: VideoItem[];
      links: LinkItem[];
    }
    
    export interface SitemapItemLoose extends SitemapItemBase {
      video?: VideoItemLoose | VideoItemLoose[];
      img?: string | Img | (string | Img)[];
      links?: LinkItem[];
      lastmodfile?: string | Buffer | URL;
      lastmodISO?: string;
      lastmodrealtime?: boolean;
    }
  6. Configure XML namespaces with NSArgs

    master

    The xmlns option in SitemapStream accepts an NSArgs object to enable specific XML namespaces in the <urlset> tag. By default, news, xhtml, image, and video are enabled.

    Available Keys:

    • news: Google News sitemap namespace.
    • video: Google Video sitemap namespace.
    • xhtml: XHTML namespace.
    • image: Image sitemap namespace.
    • custom: An array of strings for custom namespace declarations (e.g., xmlns:prefix="uri" or prefix:attribute="value").

    Security Note: Custom namespaces are strictly validated to prevent XML injection and DoS attacks. They must follow the format prefix:name="value".

  7. Use the sitemap CLI to generate sitemaps

    master

    The sitemap CLI tool converts a list of URLs (provided via stdin or a file) into a sitemap XML. It supports generating single sitemaps, sitemap indexes, and compressing output with Gzip.

    Basic Usage

    To generate a single sitemap from a file and output it to stdout:

    npx sitemap < listofurls.txt > sitemap.xml

    Generate a Sitemap Index

    To create a sitemap index file and multiple individual sitemaps, use the --index flag. You must provide a --index-base-url so the index can point to the correct locations of the generated sitemaps.

    npx sitemap --gzip --index --index-base-url https://example.com/path/to/sitemaps/ < listofurls.txt > sitemap-index.xml.gz

    Prepend to an existing sitemap

    To add new URLs to an existing sitemap file, use the --prepend flag:

    npx sitemap --prepend sitemap.xml < listofurls.json
    npx sitemap --gzip --index --index-base-url https://example.com/path/to/sitemaps/ < listofurls.txt > sitemap-index.xml.gz
  8. Generate a one-time sitemap programmatically

    master

    You can generate a sitemap from an array of link objects using SitemapStream and streamToPromise. This is useful for one-off generation tasks.

    Each link object can include properties like url, changefreq, and priority.

    // ESM
    import { SitemapStream, streamToPromise } from 'sitemap'
    import { Readable } from 'stream'
    
    // An array with your links
    const links = [{ url: '/page-1/',  changefreq: 'daily', priority: 0.3  }]
    
    // Create a stream to write to
    const stream = new SitemapStream( { hostname: 'https://...' } )
    
    // Return a promise that resolves with your XML string
    return streamToPromise(Readable.from(links).pipe(stream)).then((data) =>
      data.toString()
    )
  9. Validate XML sitemaps with xmlLint

    master

    The xmlLint function validates whether a provided XML string or Readable stream is a valid sitemap. This is a wrapper around the xmllint command-line tool and requires it to be installed on your system.

    Security Note: To prevent command injection, xmlLint accepts only XML content as a string or stream; it does not accept file paths.

    import { createReadStream, readFileSync } from 'fs';
    import { xmlLint } from 'sitemap';
    
    // Validate using a stream
    xmlLint(createReadStream('./example.xml')).then(
      () => console.log('xml is valid'),
      ([err, stderr]) => console.error('xml is invalid', stderr)
    )
    
    // Validate using a string
    const xmlContent = readFileSync('./example.xml', 'utf8');
    xmlLint(xmlContent).then(
      () => console.log('xml is valid'),
      ([err, stderr]) => console.error('xml is invalid', stderr)
    )
  10. Convert a stream to a Promise with streamToPromise

    master
    The streamToPromise utility takes a stream and returns a Promise that resolves when the stream emits the finish event. This is useful for capturing the full buffer emitted by a SitemapStream.
  11. Configure SitemapStream options

    master

    When instantiating SitemapStream, you can pass several configuration options to control the XML output, including namespaces and XSL stylesheets.

    Key options include:

    • hostname: The base URL for all links.
    • xslUrl: An optional XSL stylesheet URL.
    • lastmodDateOnly: If true, only the date is printed, not the time.
    • xmlns: An object to manage XML namespaces (e.g., news, xhtml, image, video, custom).
    // ESM
    import { SitemapStream, streamToPromise } from 'sitemap'
    import { Readable } from 'stream'
    
    const smStream = new SitemapStream({
      hostname: 'http://www.mywebsite.com',
      xslUrl: "https://example.com/style.xsl",
      lastmodDateOnly: false,
      xmlns: {
        news: true,
        xhtml: true,
        image: true,
        video: true,
        custom: [
          'xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"',
          'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"',
        ],
      }
    })
    
    // write an entry with various metadata
    smStream.write({
      url: 'http://test.com/page-1/',
      img: [
        {
          url: 'http://test.com/img1.jpg',
          caption: 'An image',
          title: 'The Title of Image One',
          geoLocation: 'London, United Kingdom',
          license: 'https://creativecommons.org/licenses/by/4.0/'
        }
      ],
      video: [
        {
          thumbnail_loc: 'http://test.com/tmbn1.jpg',
          title: 'A video title',
          description: 'This is a video'
        }
      ],
      links: [
        { lang: 'en', url: 'http://test.com/page-1/' },
        { lang: 'ja', url: 'http://test.com/page-1/ja/' }
      ],
      androidLink: 'android-app://com.company.test/page-1/',
      news: {
        publication: {
          name: 'The Example Times',
          language: 'en'
        },
        genres: 'PressRelease, Blog',
        publication_date: '2008-12-23',
        title: 'Companies, A, B in Merger Talks',
        keywords: 'business, merger, acquisition, A, B',
        stock_tickers: 'NASDAQ:A, NASDAQ:B'
      }
    })
    
    smStream.end()