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

vike-sitemap-generator

v2.3.0

Published

Vite plugin for Vike that generates sitemap.xml at build time

Readme

vike-sitemap-generator

Vite plugin for Vike that automatically generates a sitemap.xml at build time.

  • Collects page URLs from Vike's prerender context (SSG) or route config (SSR)
  • Per-page metadata via co-located +sitemap.ts files (Vike extension)
  • Per-page URL enumeration via +sitemapUrls.ts for SSR apps with parameterized routes
  • Parallel metadata resolution with configurable concurrency
  • Automatic sitemap index splitting for large sites (50,000+ URLs)
  • External sitemap references for multi-app domains (sitemap index)
  • Configurable trailing slashes, <lastmod>, <priority>, <changefreq>, and <image:image>
  • Exclude paths by exact string, regex, or per-page config
  • Optional robots.txt integration and dry-run mode

Install

npm install vike-sitemap-generator
# or
bun add vike-sitemap-generator
# or
pnpm add vike-sitemap-generator

Requires vike >= 0.4.0 and vite >= 7.0.0 as peer dependencies.

Quick start

Add the plugin to your Vite config:

// vite.config.ts
import vike from "vike/plugin";
import { vikeSitemap } from "vike-sitemap-generator";

export default {
	plugins: [
		vike(),
		vikeSitemap({
			baseUrl: "https://example.com",
		}),
	],
};

This generates a sitemap.xml in your build output directory after the client build completes.

Per-page metadata with +sitemap.ts

You can co-locate sitemap metadata alongside your pages using Vike's extension system. First, register the extension in your root config:

// pages/+config.ts
import vikeSitemapConfig from "vike-sitemap-generator/config";

export default {
	extends: [vikeSitemapConfig],
};

Then create +sitemap.ts files in any page directory:

// pages/about/+sitemap.ts
import type { SitemapPageConfig } from "vike-sitemap-generator";

export default {
	priority: 0.8,
	changefreq: "monthly",
	lastmod: "2025-06-15",
} satisfies SitemapPageConfig;

Per-page values take precedence over global plugin options. For dynamic routes, export a function to receive page context including data from +data.ts:

// pages/blog/@slug/+sitemap.ts
import type { SitemapPageConfigFn } from "vike-sitemap-generator";
import type { Data } from "./+data";

export default ((context) => ({
	priority: 0.7,
	changefreq: "weekly",
	lastmod: context.data.updatedAt,
	images: context.data.images.map((img) => ({ loc: img.url, title: img.alt })),
})) satisfies SitemapPageConfigFn<Data>;

You can also exclude a page from the sitemap:

// pages/admin/+sitemap.ts
export default {
	exclude: true,
};

See the Per-page metadata guide for details.

URL enumeration with +sitemapUrls.ts

For SSR apps with parameterized routes (e.g. /blog/@slug), the plugin cannot discover concrete URLs automatically since there is no prerender context. You can provide a +sitemapUrls.ts file alongside your page to enumerate the URLs that should appear in the sitemap:

// pages/blog/@slug/+sitemapUrls.ts
import type { SitemapUrlsConfig } from "vike-sitemap-generator";

export default [
	"/blog/hello-world",
	"/blog/second-post",
] satisfies SitemapUrlsConfig;

You can also export a function (sync or async) for dynamic enumeration:

// pages/blog/@slug/+sitemapUrls.ts
import type { SitemapUrlsConfig } from "vike-sitemap-generator";

export default (async () => {
	const posts = await fetch("https://cms.example.com/posts").then((r) =>
		r.json(),
	);
	return posts.map((post) => `/blog/${post.slug}`);
}) satisfies SitemapUrlsConfig;

URLs from +sitemapUrls.ts are matched against page route patterns, so per-page +sitemap.ts config and route params are still resolved for them.

This requires the Vike extension to be registered (see Per-page metadata above).

See the Per-page metadata guide for details.

Options

All options are passed to vikeSitemap() in your Vite config.

baseUrl (required)

The base URL of your site. Must not end with a trailing slash.

vikeSitemap({ baseUrl: "https://example.com" });

outFile

Path for the generated sitemap, relative to the output directory. Defaults to "sitemap.xml".

vikeSitemap({
	baseUrl: "https://example.com",
	outFile: "custom/path/sitemap.xml",
});

outDir

Output directory independent of Vite's build.outDir. Accepts an absolute path or a path relative to your project root. When omitted, falls back to the client build's output directory.

vikeSitemap({
	baseUrl: "https://example.com",
	outDir: "dist/public",
});

trailingSlash

Configure trailing slashes on URLs in the sitemap. Accepts a boolean, an array of rules (first match wins), or a function.

// Add trailing slash to all URLs
vikeSitemap({ baseUrl: "...", trailingSlash: true });

// Remove trailing slash from all URLs
vikeSitemap({ baseUrl: "...", trailingSlash: false });

// Per-route rules (exact strings or RegExp)
vikeSitemap({
	baseUrl: "...",
	trailingSlash: [
		{ match: /^\/blog/, trailingSlash: true },
		{ match: /^\/docs/, trailingSlash: false },
	],
});

// Function with access to all URLs
vikeSitemap({
	baseUrl: "...",
	trailingSlash: (url, { urls }) => {
		return urls.some((u) => u !== url && u.startsWith(`${url}/`));
	},
});

lastmod

Async callback to resolve <lastmod> for each URL. Receives the URL path and should return an ISO 8601 date string, or undefined to omit.

vikeSitemap({
	baseUrl: "...",
	lastmod: async (url) => {
		const res = await fetch(`https://cms.example.com/meta?path=${url}`);
		const data = await res.json();
		return data.updatedAt;
	},
});

priority

Configure <priority> per route. Accepts a number, an array of rules (first match wins), or a function.

// Uniform priority
vikeSitemap({ baseUrl: "...", priority: 0.5 });

// Per-route rules
vikeSitemap({
	baseUrl: "...",
	priority: [
		{ match: "/", priority: 1.0 },
		{ match: /^\/blog/, priority: 0.8 },
	],
});

// Function
vikeSitemap({
	baseUrl: "...",
	priority: (url, { urls }) => {
		const hasChildren = urls.some(
			(u) => u !== url && u.startsWith(`${url === "/" ? "" : url}/`),
		);
		return hasChildren ? 0.8 : 0.5;
	},
});

changefreq

Configure <changefreq> per route. Accepts a single value or an array of rules.

Valid values: "always", "hourly", "daily", "weekly", "monthly", "yearly", "never".

vikeSitemap({ baseUrl: "...", changefreq: "weekly" });

// Per-route rules
vikeSitemap({
	baseUrl: "...",
	changefreq: [
		{ match: "/", changefreq: "daily" },
		{ match: /^\/blog/, changefreq: "weekly" },
		{ match: /^\/docs/, changefreq: "monthly" },
	],
});

images

Async callback to resolve <image:image> entries for each URL (Google Image Sitemap extension).

vikeSitemap({
	baseUrl: "...",
	images: async (url) => {
		const res = await fetch(`https://cms.example.com/images?path=${url}`);
		const data = await res.json();
		return data.images.map((img) => ({
			loc: img.url,
			title: img.alt,
		}));
	},
});

Each SitemapImage object supports:

| Field | Type | Description | | ------------- | -------- | ------------------------------------------------------ | | loc | string | (required) URL of the image | | caption | string | (optional) Caption / description of the image | | geoLocation | string | (optional) Geographic location (e.g. "New York, NY") | | title | string | (optional) Title of the image | | license | string | (optional) URL to the license for the image |

additionalUrls

Include URLs that Vike can't discover automatically, such as SSR apps with parameterized routes that aren't prerendered.

vikeSitemap({
	baseUrl: "...",
	additionalUrls: ["/product/1", "/product/2", "/product/3"],
});

Additional URLs are matched against your page route patterns, so per-page +sitemap.ts config and route params are still resolved for these URLs.

exclude

An array of paths to exclude from the sitemap. Each entry can be an exact path string or a RegExp pattern.

vikeSitemap({
	baseUrl: "...",
	exclude: [
		"/admin",
		/^\/internal/,
	],
});

String entries use exact matching. Use a RegExp like /^\/admin/ to exclude an entire subtree.

concurrency

Maximum number of concurrent metadata resolutions. Defaults to 10. Set to Infinity for unlimited parallelism.

When lastmod or images callbacks involve async I/O (git commands, API calls, CMS fetches), this controls how many run in parallel.

vikeSitemap({
	baseUrl: "...",
	lastmod: async (url) => { /* ... */ },
	concurrency: 20,
});

maxUrlsPerSitemap

Maximum URLs per sitemap file. When exceeded, the plugin automatically splits into multiple numbered sitemap files and generates a sitemap index. Defaults to 50000 (the sitemap protocol limit).

vikeSitemap({
	baseUrl: "...",
	maxUrlsPerSitemap: 10000,
});

This produces sitemap-0.xml, sitemap-1.xml, ..., and a sitemap.xml index that references them.

robots

When enabled, appends a Sitemap: directive to robots.txt in the output directory, pointing to the generated sitemap. Creates the file if it doesn't exist. Avoids duplicate entries. Defaults to false.

vikeSitemap({
	baseUrl: "https://example.com",
	robots: true,
});

dryRun

When enabled, logs the generated XML to the console without writing any files. Useful for debugging. Defaults to false.

vikeSitemap({
	baseUrl: "https://example.com",
	dryRun: true,
});

externalSitemaps

An array of external sitemap URLs to include in the generated sitemap index. When provided, the plugin always produces a sitemap index that references both the locally generated sitemap(s) and the external URLs. This is useful when your domain hosts multiple independent sites that each generate their own sitemap (e.g. a docs site at /docs).

vikeSitemap({
	baseUrl: "https://example.com",
	externalSitemaps: [
		"https://example.com/docs/sitemap.xml",
		"https://example.com/blog/sitemap.xml",
	],
});

See the External sitemaps guide for details.

Fetchers

Built-in helpers for resolving <lastmod> from git history.

getLastModFromGit

Gets the last commit date for a file from the local git history.

import { getLastModFromGit, vikeSitemap } from "vike-sitemap-generator";

vikeSitemap({
	baseUrl: "https://example.com",
	lastmod: async (url) => {
		const filePath =
			url === "/" ? "pages/index/+Page.tsx" : `pages${url}/+Page.tsx`;
		return getLastModFromGit({ filePath });
	},
});

| Parameter | Type | Description | | ---------- | -------- | -------------------------------------------------------- | | filePath | string | Path to the file relative to cwd | | cwd | string | (optional) Working directory. Defaults to process.cwd() |

Returns YYYY-MM-DD or undefined.

getLastModFromGithub

Fetches the last commit date for a file from the GitHub API.

import { getLastModFromGithub, vikeSitemap } from "vike-sitemap-generator";

vikeSitemap({
	baseUrl: "https://example.com",
	lastmod: async (url) => {
		const filePath =
			url === "/" ? "pages/index/+Page.tsx" : `pages${url}/+Page.tsx`;
		return getLastModFromGithub({
			token: process.env.GH_TOKEN!,
			repo: "owner/repo",
			filePath,
		});
	},
});

| Parameter | Type | Description | | ---------- | -------- | ---------------------------------------------------------- | | token | string | GitHub personal access token (needs repo read access) | | repo | string | Repository in owner/repo format | | filePath | string | Path to the file in the repository |

Returns YYYY-MM-DD or undefined.

How it works

The plugin hooks into Vite's build pipeline and runs after the SSR build completes. It waits for the SSR build so that prerenderContext.pageContexts (populated during the SSR build's writeBundle) is available.

  1. configResolved -- captures the Vite resolved config
  2. closeBundle (SSR build only) --
    • Re-reads Vike's page configuration via getVikeConfig() to pick up prerenderContext and per-page +sitemap.ts / +sitemapUrls.ts metadata
    • Collects URLs from prerenderContext (SSG) or static route patterns (SSR), plus URLs enumerated by +sitemapUrls.ts files
    • Attaches per-page +sitemap.ts config to each URL
    • Filters out excluded paths (global exclude patterns and per-page exclude: true)
    • Applies trailing slash rules
    • Resolves metadata (lastmod, priority, changefreq, images) in parallel with the configured concurrency, merging per-page values over global callbacks
    • Resolves image asset imports via the client build manifest
    • Splits into multiple sitemap files if the URL count exceeds maxUrlsPerSitemap
    • Writes files to the resolved output directory (or logs in dry-run mode)
    • Appends a Sitemap: directive to robots.txt if enabled

Guides

License

MIT