npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@casoon/astro-sitemap

v0.2.2

Published

Astro integration that generates sitemap.xml at build time — supports dynamic sources, pattern-based rules, i18n hreflang, serialize hook, and sitemap-index chunking.

Downloads

1,452

Readme

@casoon/astro-sitemap

CI npm License: MIT

Astro integration that generates sitemap.xml at build time.

  • Discovers pages via Astro's build pipeline — no dist/ scanning
  • Dynamic URL sources (content collections, CMS, APIs)
  • Pattern-based priority and changefreq rules
  • Per-item serialize hook for fine-grained control
  • i18n hreflang link generation
  • Automatic sitemap-index.xml chunking for large sites
  • Built-in audit with warnings for empty sitemaps, duplicates, and invalid values

Requirements: Astro 6+, Node 18+


Installation

npm install @casoon/astro-sitemap

Quick Start

// astro.config.mjs
import { defineConfig } from 'astro/config';
import astroSitemap from '@casoon/astro-sitemap';

export default defineConfig({
  site: 'https://example.com',
  integrations: [astroSitemap()],
});

This generates dist/sitemap.xml containing all pages discovered from the build.


Configuration

astroSitemap({
  // all options are optional
  siteUrl: 'https://example.com',
  sources: [...],
  exclude: [...],
  filter: (url) => true,
  priority: [...],
  changefreq: [...],
  serialize: async (entry) => entry,
  i18n: { defaultLocale: 'en', locales: { en: 'en', de: 'de-DE' } },
  output: { mode: 'single', maxUrls: 50_000, filename: 'sitemap.xml' },
  audit: { warnOnEmpty: true, errorOnDuplicates: false },
  debug: false,
})

siteUrl

siteUrl?: string

Base URL of your site. Auto-detected from site in astro.config if omitted.


base (Astro config — not a plugin option)

If your site is deployed to a subdirectory, set base in astro.config. The plugin reads it automatically — no plugin configuration needed.

// astro.config.mjs
export default defineConfig({
  site: 'https://example.com',
  base: '/my-app/',               // sitemap URLs become https://example.com/my-app/...
  integrations: [astroSitemap()],
});

Relative paths in sources entries are also prefixed with the base automatically.


sources

sources?: Array<() => Promise<SitemapEntry[]>>

Async functions that return additional URL entries. Use for dynamic content not generated as static pages.

astroSitemap({
  sources: [
    async () => {
      const { getCollection } = await import('astro:content');
      const posts = await getCollection('blog');
      return posts
        .filter((p) => !p.data.draft)
        .map((p) => ({
          loc: `/blog/${p.id}/`,
          lastmod: p.data.updatedAt ?? p.data.date,
          priority: 0.7,
        }));
    },
  ],
})

SitemapEntry fields:

| Field | Type | Description | |---|---|---| | loc | string | Full URL or root-relative path (e.g. /blog/post/) | | lastmod | string | ISO date string (defaults to today) | | priority | number | 0.0–1.0 (defaults to depth-based rule) | | changefreq | Changefreq | Defaults to built-in pattern rules |

Source entries are merged with static pages. On duplicate loc, the last entry wins — sources listed later override earlier sources and scanned pages.

Draft pages: Always filter drafts in the source function — the plugin only sees URLs, not frontmatter. For src/pages/*.md files with draft: true, Astro skips building them automatically (they never reach the sitemap). For content collections you must filter explicitly: getCollection('blog', ({ data }) => !data.draft)


exclude

exclude?: Array<string | RegExp>

URL patterns to exclude from the sitemap. String patterns match if the path equals or starts with the value.

astroSitemap({
  exclude: ['/intern/', /\/preview\//],
})

Built-in exclusions always apply regardless of this option: /sitemap*.xml, /robots.txt, /llms.txt, /rss.xml, /feed.xml, /404, /500, /_*, /api/*, /landing/*, /drafts/*


filter

filter?: (url: string) => boolean

Function-based URL filter. Receives the full absolute URL. Return false to exclude. Applied after exclude patterns.

astroSitemap({
  filter: (url) => !url.includes('/internal/'),
})

priority

priority?: Array<{ pattern: string | RegExp; priority: number }>

Custom priority rules evaluated before built-in defaults. First matching rule wins.

astroSitemap({
  priority: [
    { pattern: '/produkte/', priority: 0.9 },
    { pattern: /\/blog\/\d{4}\//, priority: 0.6 },
  ],
})

Gotcha — targeting the homepage: The string '/' matches every URL (all paths start with /), so it acts as a catch-all, not a homepage selector. Use the RegExp /^\/$/ to match only the root path:

// Wrong — sets priority 0.9 on ALL pages:
{ pattern: '/',      priority: 0.9 }

// Correct — sets priority 0.9 on the homepage only:
{ pattern: /^\/$/, priority: 0.9 }

The built-in defaults already assign / a priority of 1.0, so you only need this when overriding the homepage to a different value.

Built-in depth-based defaults (applied when no rule matches):

| Path | Priority | |---|---| | / | 1.0 | | Depth 1 (e.g. /about/) | 0.9 | | Depth 2 (e.g. /blog/post/) | 0.8 | | Depth 3+ | 0.7 |


changefreq

changefreq?: Array<{ pattern: string | RegExp; changefreq: Changefreq }>

Custom changefreq rules, evaluated before built-in defaults. First matching rule wins.

astroSitemap({
  changefreq: [
    { pattern: '/produkte/', changefreq: 'daily' },
    { pattern: '/impressum/', changefreq: 'yearly' },
  ],
})

Built-in defaults: / and /blog|artikel|news|posts|updates/weekly, everything else → monthly.


serialize

serialize?: (entry: ResolvedSitemapEntry) => ResolvedSitemapEntry | undefined | Promise<...>

Called for every resolved entry after deduplication. Return a modified entry to keep it, or undefined to drop it from the sitemap.

astroSitemap({
  serialize: async (entry) => {
    // Drop preview pages
    if (entry.loc.includes('/preview/')) return undefined;

    // Override lastmod from a CMS
    const lastmod = await fetchLastmod(entry.loc);
    return { ...entry, lastmod };
  },
})

ResolvedSitemapEntry fields: loc, lastmod, priority, changefreq, links (hreflang, set by i18n).


i18n

i18n?: {
  defaultLocale: string;
  locales: Record<string, string>; // locale prefix → hreflang value
}

Enables <xhtml:link rel="alternate" hreflang="..."> generation for multi-language sites. Pages are grouped by their path without the locale prefix; each page gets alternate links for all sibling locale variants.

astroSitemap({
  i18n: {
    defaultLocale: 'en',
    locales: {
      en: 'en',
      de: 'de-DE',
      fr: 'fr-FR',
    },
  },
})

Given pages /en/about/ and /de/about/, the output for /en/about/ will include:

<url>
  <loc>https://example.com/en/about/</loc>
  ...
  <xhtml:link rel="alternate" hreflang="en" href="https://example.com/en/about/"/>
  <xhtml:link rel="alternate" hreflang="de-DE" href="https://example.com/de/about/"/>
</url>

Pages with only one locale version get no xhtml:link elements.


output

output?: {
  mode?: 'single' | 'index';
  maxUrls?: number;
  filename?: string;
}

| Option | Default | Description | |---|---|---| | mode | auto | 'single' forces one file; 'index' forces index mode; auto-selects based on URL count | | maxUrls | 50000 | URLs per chunk before switching to index mode | | filename | 'sitemap.xml' | Output filename in single mode |

When the URL count exceeds maxUrls, the plugin automatically generates sitemap-index.xml + numbered sitemap-N.xml chunks.


audit

audit?: {
  warnOnEmpty?: boolean;
  errorOnDuplicates?: boolean;
}

| Option | Default | Description | |---|---|---| | warnOnEmpty | true | Warn when sitemap has zero entries | | errorOnDuplicates | false | Treat duplicate URLs as errors instead of warnings |


debug

debug?: boolean

Enables detailed logging during the build (collected page count, source batch sizes, deduplication results).


Recipes

Blog with content collections (static routes)

If blog posts are generated as static Astro pages (src/pages/blog/[slug].astro), they are automatically discovered from the build — no sources needed. Use priority/changefreq rules to tune the values:

// astro.config.mjs
astroSitemap({
  priority: [
    { pattern: '/blog/', priority: 0.7 },
  ],
  changefreq: [
    { pattern: '/blog/', changefreq: 'monthly' },
  ],
})

External / headless CMS sources

Use sources for URLs that are not generated as Astro pages — e.g. content from a headless CMS that you fetch via API. Do not use import('astro:content') inside sources, as that virtual module is not available outside the Vite build context.

astroSitemap({
  sources: [
    async () => {
      // Fetch from an external API
      const res = await fetch('https://cms.example.com/api/pages');
      const pages = await res.json();
      return pages.map((p) => ({
        loc: `/blog/${p.slug}/`,
        lastmod: p.updatedAt,
        priority: 0.7,
        changefreq: 'monthly',
      }));
    },
  ],
})

Blog + taxonomy pages (categories, series)

If taxonomy pages (/blog/category/[slug].astro) are also static Astro routes, they appear automatically too. Assign different priorities per path pattern:

astroSitemap({
  priority: [
    { pattern: '/blog/category/', priority: 0.6 },
    { pattern: '/blog/',          priority: 0.7 },
  ],
  changefreq: [
    { pattern: '/blog/category/', changefreq: 'weekly' },
    { pattern: '/blog/',          changefreq: 'monthly' },
  ],
})

Exclude pages and override priorities

astroSitemap({
  exclude: ['/intern/', '/preview/', /\/admin\//],
  filter: (url) => !url.includes('?'),
  priority: [
    { pattern: '/produkte/',  priority: 0.9 },
    { pattern: '/blog/',      priority: 0.7 },
    { pattern: '/impressum/', priority: 0.3 },
  ],
  changefreq: [
    { pattern: '/blog/',      changefreq: 'weekly' },
    { pattern: '/impressum/', changefreq: 'yearly' },
  ],
})

Override lastmod from a CMS

Use serialize to enrich or drop individual entries after all sources are merged.

astroSitemap({
  serialize: async (entry) => {
    // Drop anything not yet ready
    if (entry.loc.includes('/draft/')) return undefined;

    // Fetch accurate lastmod from your CMS
    const lastmod = await cms.getLastmod(entry.loc);
    return lastmod ? { ...entry, lastmod } : entry;
  },
})

ResolvedSitemapEntry fields: loc, lastmod, priority, changefreq, links (hreflang, set by i18n).

See the examples/ directory for complete, copy-paste-ready recipes.


Multi-language site (i18n / hreflang)

// astro.config.mjs
export default defineConfig({
  site: 'https://example.com',
  i18n: {
    defaultLocale: 'en',
    locales: ['en', 'de', 'fr'],
  },
  integrations: [
    astroSitemap({
      i18n: {
        defaultLocale: 'en',
        locales: { en: 'en', de: 'de-DE', fr: 'fr-FR' },
      },
    }),
  ],
});

Given pages /en/about/, /de/about/, /fr/about/, each URL gets <xhtml:link rel="alternate"> entries for all language variants automatically.


RSS feed alongside sitemap

// src/pages/rss.xml.ts
import { createRssRoute } from '@casoon/astro-sitemap/rss';
import { getCollection } from 'astro:content';

export const GET = createRssRoute({
  title: 'My Blog',
  description: 'Latest posts',
  language: 'en',
  copyright: `Copyright ${new Date().getFullYear()} ACME`,
  getItems: async (siteUrl) => {
    const posts = await getCollection('blog', ({ data }) => !data.draft);
    return posts
      .sort((a, b) => +new Date(b.data.date) - +new Date(a.data.date))
      .map((p) => ({
        title:       p.data.title,
        description: p.data.description,
        pubDate:     p.data.date,
        link:        `${siteUrl}/blog/${p.id}/`,
        author:      p.data.author,
        categories:  p.data.tags,
      }));
  },
});

How it works

  1. Config — Captures site, base, trailingSlash, build.format from Astro config. build.format: 'file' and 'preserve' automatically append .html to non-index page URLs
  2. Routesastro:routes:resolved collects i18n fallback routes
  3. Pagesastro:build:done provides the list of all rendered pages directly from Astro (no filesystem scan)
  4. Sources — Each source function is awaited and its entries collected
  5. Resolve — Each entry gets lastmod, priority, changefreq from user rules or built-in defaults
  6. Deduplicate — Duplicate loc values are collapsed (last entry wins)
  7. Serialize — The serialize hook runs per entry if configured
  8. i18n — hreflang links are attached when i18n is configured
  9. Audit — Warnings/errors are emitted for empty sitemaps, duplicates, invalid priorities
  10. Write — Output is written as sitemap.xml or sitemap-index.xml + chunks

Optional: RSS Feed Helper

The package ships an optional createRssRoute helper that generates an RSS 2.0 feed. It has no external dependencies and integrates naturally with Astro's API routes.

// src/pages/rss.xml.ts
import { createRssRoute } from '@casoon/astro-sitemap/rss';
import { getCollection } from 'astro:content';

export const GET = createRssRoute({
  title: 'My Blog',
  description: 'Latest posts',
  language: 'en',
  getItems: async (siteUrl) => {
    const posts = await getCollection('blog', ({ data }) => !data.draft);
    return posts.map(p => ({
      title:   p.data.title,
      pubDate: p.data.date,
      link:    `${siteUrl}/blog/${p.id}/`,
    }));
  },
});

createRssRoute options

| Option | Type | Description | |---|---|---| | title | string | Feed title | | description | string | Feed description | | getItems | (siteUrl: string) => RssItem[] \| Promise<RssItem[]> | Returns feed items. Receives resolved siteUrl (no trailing slash) | | siteUrl | string? | Override base URL (auto-detected from context.site) | | feedUrl | string? | URL of this feed file — used for <atom:link rel="self">. Defaults to {siteUrl}/rss.xml | | language | string? | Language code, e.g. 'en' or 'de-DE' | | copyright | string? | Copyright notice | | managingEditor | string? | Managing editor name or e-mail | | feedCustomData | string? | Raw XML injected inside <channel> |

RssItem fields

| Field | Type | Description | |---|---|---| | title | string | Item title | | pubDate | Date \| string | Publication date | | link | string | Full URL or root-relative path (auto-prefixed with siteUrl) | | description | string? | Item summary | | author | string? | Author name or e-mail | | categories | string[]? | Category / tag strings | | customData | string? | Raw XML injected inside <item> |


Differences to @astrojs/sitemap

| Feature | @casoon/astro-sitemap | @astrojs/sitemap | |---|---|---| | URL discovery | Astro pages array + routes hook | Astro pages array + routes hook | | URL filtering | exclude (patterns) + filter (function) | filter (function) | | Dynamic sources | sources[] (async functions) | customPages (string array) | | Priority | Depth-based defaults + pattern rules | Global default only | | Changefreq | Pattern rules + smart defaults | Global default only | | Per-item transform | serialize hook | serialize hook | | i18n / hreflang | i18n option | i18n option | | Audit | Warnings for empty/duplicates/invalid | None | | XML formatting | Indented, human-readable | Minified (via sitemap package) | | Chunked sitemaps | Auto + manual | Auto + chunks (named groups) | | XSL stylesheet | — | xslURL option |


License

MIT