Sitemap focused on ease of use
and making it impossible to forget to add your paths.
🎉NEW v2.0: For TanStack Start and SvelteKit.
- Features
- Installation
- Usage
- Robots.txt
- Playwright test
- Tip: Querying your database to get param values
- Example sitemap output
- Migrating from v1 to v2
- Changelog
- 🤓 Supports any rendering method.
- 🪄 Automatically gathers routes + data for route parameters provided by you.
- 👻 Exclude routes via
excludeRoutePatterns(e.g./^\/dashboard/, paginated routes, etc) - 🧠 Easy maintenance. Accidental omission of data for a parameterized route
throws an error until either, a.) the route is excluded via
excludeRoutePatterns, or b.) data is provided for its param value(s). - 🚀 Defaults to 1h CDN cache, no browser cache.
- 💆 Set custom headers to override default headers:
sitemap.response({ headers: { 'cache-control': 'max-age=0, s-maxage=60' } }). - 💡 Google, and other modern search engines, ignore
priorityandchangefreqand use their own heuristics to determine when to crawl pages on your site. As such, these properties are not included by default to minimize KB size and enable faster crawling. Optionally, you can enable them via:sitemap.response({ defaultChangefreq: 'daily', defaultPriority: 0.7 }). - 🗺️ Automatic sitemap index.
- 🌎 i18n
- 🧪 Well tested.
- ✨ Zero runtime dependencies.
- 🫶 Built with TypeScript.
npm i super-sitemap
or
bun add super-sitemap
Then see the Usage, Robots.txt, & Playwright Test sections.
// /src/routes/sitemap[.]xml.ts
import { createFileRoute } from '@tanstack/react-router';
import { response } from 'super-sitemap/tanstack-start';
import { getRouter } from '../router';
export const Route = createFileRoute('/sitemap.xml')({
server: {
handlers: {
GET: () =>
response({
origin: 'https://example.com',
router: getRouter,
}),
},
},
});// /src/routes/sitemap.xml/+server.ts
import type { RequestHandler } from '@sveltejs/kit';
import { response } from 'super-sitemap/sveltekit';
export const GET: RequestHandler = async () => {
return await response({
origin: 'https://example.com',
});
};- Always include the
.xmlextension on your route name–e.g.sitemap.xml. This ensures your web server sends the correctapplication/xmlcontent type even if you decide to prerender your sitemap to a static file. - Automatic route discovery:
- The SvelteKit adapter discovers routes using Vite's
import.meta.glob. - The TanStack Start adapter discovers routes via TanStack Start's official
getRouter, which is derived from its generated route manifest file. This means that all TanStack Start routing methods are fully supported: file-based routing, code-based routing, or virtual file routes.
- The SvelteKit adapter discovers routes using Vite's
- For all frameworks: server-only routes are excluded automatically and do not need to be listed in your route exclusions.
All config properties shown here are optional, except for origin and
paramValues to provide data for parameterized routes.
TanStack Start example
// /src/routes/sitemap[.]xml.ts
import { createFileRoute } from '@tanstack/react-router';
import * as blog from '../lib/data/blog';
import * as sitemap from 'super-sitemap/tanstack-start';
import { getRouter } from '../router';
export const Route = createFileRoute('/sitemap.xml')({
server: {
handlers: {
GET: async () => {
// Get data for parameterized routes however you need to; this is only
// an example.
let blogSlugs, blogTags;
try {
[blogSlugs, blogTags] = await Promise.all([blog.getSlugs(), blog.getTags()]);
} catch (err) {
throw new Error('Could not load data for param values.');
}
return await sitemap.response({
origin: 'https://example.com',
router: getRouter,
locales: {
default: 'en',
alternates: ['de', 'pt-br'],
},
excludeRoutePatterns: [
/^\/dashboard/, // i.e. routes starting with `/dashboard`
/\{\-\$page\}/, // i.e. route keys containing `{-$page}`–e.g. `/blog/{-$page}`
/^\/admin(?:$|\/)/, // i.e. routes within an admin section
],
paramValues: {
// paramValues can be a 1D array of strings
'/blog/$slug': blogSlugs, // e.g. ['hello-world', 'another-post']
'/blog/tag/$tag': blogTags, // e.g. ['red', 'green', 'blue']
// Or a 2D array of strings
'/campsites/$country/$state': [
['usa', 'new-york'],
['usa', 'california'],
['canada', 'toronto'],
],
// Or an array of ParamValue objects
'/athlete-rankings/$country/$state': [
{
values: ['usa', 'new-york'], // required
lastmod: '2025-01-01T00:00:00Z', // optional
changefreq: 'daily', // optional
priority: 0.5, // optional
},
{
values: ['usa', 'california'],
lastmod: '2025-01-01T00:00:00Z',
changefreq: 'daily',
priority: 0.5,
},
],
},
headers: {
'custom-header': 'foo', // case insensitive; xml content type & 1h CDN cache headers are included by default
},
additionalPaths: [
'/foo.pdf', // for example, to a file in your public dir
],
defaultChangefreq: 'daily',
defaultPriority: 0.7,
sort: 'alpha', // default is false; 'alpha' sorts paths alphabetically.
processPaths: (paths) => {
// Optional callback to allow arbitrary processing of your path objects. See the
// processPaths() section of the README.
return paths;
},
});
},
},
},
});SvelteKit example
// /src/routes/sitemap.xml/+server.ts
import { error } from '@sveltejs/kit';
import type { RequestHandler } from '@sveltejs/kit';
import * as blog from '$lib/data/blog';
import * as sitemap from 'super-sitemap/sveltekit';
export const prerender = true; // optional
export const GET: RequestHandler = async () => {
// Get data for parameterized routes however you need to; this is only an
// example.
let blogSlugs, blogTags;
try {
[blogSlugs, blogTags] = await Promise.all([blog.getSlugs(), blog.getTags()]);
} catch (err) {
throw error(500, 'Could not load data for param values.');
}
return await sitemap.response({
origin: 'https://example.com',
locales: {
default: 'en',
alternates: ['de', 'pt-br'],
},
excludeRoutePatterns: [
/^\/dashboard/, // i.e. routes starting with `/dashboard`
/\[page=integer\]/, // i.e. route keys containing `[page=integer]`–e.g. `/blog/[page=integer]`
/\(authenticated\)/, // i.e. routes within a group
],
paramValues: {
// paramValues can be a 1D array of strings
'/blog/[slug]': blogSlugs, // e.g. ['hello-world', 'another-post']
'/blog/tag/[tag]': blogTags, // e.g. ['red', 'green', 'blue']
// Or a 2D array of strings
'/campsites/[country]/[state]': [
['usa', 'new-york'],
['usa', 'california'],
['canada', 'toronto'],
],
// Or an array of ParamValue objects
'/athlete-rankings/[country]/[state]': [
{
values: ['usa', 'new-york'], // required
lastmod: '2025-01-01T00:00:00Z', // optional
changefreq: 'daily', // optional
priority: 0.5, // optional
},
{
values: ['usa', 'california'],
lastmod: '2025-01-01T00:00:00Z',
changefreq: 'daily',
priority: 0.5,
},
],
},
headers: {
'custom-header': 'foo', // case insensitive; xml content type & 1h CDN cache headers are included by default
},
additionalPaths: [
'/foo.pdf', // for example, to a file in your static dir
],
defaultChangefreq: 'daily',
defaultPriority: 0.7,
sort: 'alpha', // default is false; 'alpha' sorts paths alphabetically.
processPaths: (paths) => {
// Optional callback to allow arbitrary processing of your path objects. See the
// processPaths() section of the README.
return paths;
},
});
};You only need to read and enable if you have >50,000 URLs in your sitemap, which is the number recommended by sitemaps.org.
You can enable sitemap index support with just two changes.
See the Sitemap Index docs.
Routes that contain parameters need to have their values defined. You can
provide these values as: string[], string[][], or
ParamValue[].
TanStack Start example
paramValues: {
// Required params use TanStack's `$param` syntax.
'/blog/$slug': ['hello-world', 'another-post'],
// Optional params use TanStack's `{-$param}` syntax.
'/products/{-$category}': ['shoes', 'shirts'],
// Multiple params use a 2D array, matched positionally.
'/campsites/$country/$state': [
['usa', 'colorado'],
['canada', 'toronto'],
],
// Splat/rest params use TanStack's bare `$` segment.
'/docs/$': ['intro/getting-started'],
// Keep locale segments in the key, but omit locale values from the value
// array because locale values are specified once at the config level.
'/$locale/blog/$slug': ['hello-world'],
'/{-$locale}/docs/$slug': ['intro'],
// Pathless layout segments and route group directories are omitted from keys.
// For example, `/_layout/(dashboard)/users/$id` is keyed as:
'/users/$id': ['42'],
// Optional params expand into route variants. The base route (`/foo`)
// needs no values, but dynamic variants need values unless excluded. For
// multiple optional params, provide values for each emitted dynamic variant
// that you keep.
'/foo/{-$paramA}': ['foo', 'bar'],
'/foo/{-$paramA}/{-$paramB}': [
['foo', 'one'],
['bar', 'two'],
],
// If you need per-entry metadata, use ParamValue objects.
'/athlete-rankings/$country/$state': [
{
values: ['usa', 'new-york'], // required
lastmod: '2025-01-01T00:00:00Z', // optional
changefreq: 'daily', // optional
priority: 0.5, // optional
},
{
values: ['usa', 'california'],
lastmod: '2025-01-01T01:16:52Z',
changefreq: 'daily',
priority: 0.5,
},
],
},SvelteKit example
paramValues: {
// Required params use SvelteKit's `[param]` syntax.
'/blog/[slug]': ['hello-world', 'another-post'],
// Optional params use SvelteKit's `[[param]]` syntax.
'/products/[[category]]': ['shoes', 'shirts'],
// Matcher params preserve the matcher name in the key.
'/blog/[page=integer]': ['2', '3'],
'/archive/[[year=integer]]': ['2024', '2025'],
// Multiple params use a 2D array, matched positionally.
'/campsites/[country]/[state]': [
['usa', 'colorado'],
['canada', 'toronto'],
],
// Rest params use SvelteKit's `[...rest]` syntax.
'/docs/[...rest]': ['intro/getting-started'],
// Keep locale segments in the key, but omit locale values from the value
// array because locale values are specified once at the config level.
'/[[locale]]/blog/[slug]': ['hello-world'],
'/[locale]/docs/[slug]': ['intro'],
// Route groups are omitted from keys.
// For example, `/(dashboard)/users/[id]` is keyed as:
'/users/[id]': ['42'],
// Optional params expand into route variants. The base route (`/foo`)
// needs no values, but dynamic variants need values unless excluded.
'/foo/[[paramA]]': ['foo', 'bar'],
'/foo/[[paramA]]/[[paramB]]': [
['foo', 'one'],
['bar', 'two'],
],
// If you need per-entry metadata, use ParamValue objects.
'/athlete-rankings/[country]/[state]': [
{
values: ['usa', 'new-york'], // required
lastmod: '2025-01-01T00:00:00Z', // optional
changefreq: 'daily', // optional
priority: 0.5, // optional
},
{
values: ['usa', 'california'],
lastmod: '2025-01-01T01:16:52Z',
changefreq: 'daily',
priority: 0.5,
},
],
},Keys in the paramValues object must match Super Sitemap's expected syntax.
View allowed keys
| Route feature | TanStack Start key | SvelteKit key |
|---|---|---|
| Required param | '/blog/$slug' |
'/blog/[slug]' |
| Optional param | '/products/{-$category}' |
'/products/[[category]]' |
| Required params (2+) | '/campsites/$country/$state' |
'/campsites/[country]/[state]' |
| Optional params (2+) | On disk: /foo/{-$paramA}/{-$paramB}Use: '/foo/{-$paramA}''/foo/{-$paramA}/{-$paramB}' |
On disk: /foo/[[paramA]]/[[paramB]]Use: '/foo/[[paramA]]''/foo/[[paramA]]/[[paramB]]' |
| Splat / rest param | '/docs/$' |
'/docs/[...rest]' |
| Param matcher | (No equivalent) | '/blog/[page=integer]' |
| Optional matcher | (No equivalent) | '/archive/[[year=integer]]' |
| Route groups are omitted | On disk: /(dashboard)/users/$idUse: '/users/$id' |
On disk: /(dashboard)/users/[id]Use: '/users/[id]' |
| Pathless layout segments are omitted | On disk: /_layout/users/$idUse: '/users/$id' |
(No equivalent) |
| Optional locale param | '/{-$locale}/blog/$slug' |
'/[[locale]]/blog/[slug]' |
| Required locale param | '/$locale/docs/$slug' |
'/[locale]/docs/[slug]' |
Syntax differs between framework adapters (TanStack Start, SvelteKit), a.) to support framework-specific features (like SvelteKit's param matchers or TanStack Start's pathless layout segments), and b.) to remain close to how each framework defines its routes.
If in doubt, call your sitemap route in a test. For prerendered sitemaps, also build your app. You'll see errors for any keys that are missing or don't match what Super Sitemap expects, so you can correct them.
Use excludeRoutePatterns to remove routes before paths are generated.
excludeRoutePatterns matches against route keys, which are the same keys used by
paramValues; see Keys for Param Values.
excludeRoutePatterns: [/^\/dashboard(?:$|\/)/],This excludes /dashboard and any route below it, like /dashboard/settings.
Routing organization segments, like pathless layout segments and route groups, are not present in route keys and cannot be matched against.
You only need to read this if you want to understand how Super Sitemap handles optional params and why.
Optional params expand into route variants. Super Sitemap will include each path
variation and will require you to either exclude those route patterns using
excludeRoutePatterns or provide param values for them using paramValues,
within your sitemap config object.
See the Optional Params docs to learn more.
The processPaths() callback is powerful, but rarely needed.
It allows you to arbitrarily process the path objects for your site before they become XML.
See the Process Paths docs.
Super Sitemap supports multilingual site annotations. This allows search engines to be aware of alternate language versions of your pages.
See the i18n docs.
getSamplePaths() is useful when you want one visitable path for each public route shape, usually for testing or monitoring purposes.
See the Get Sample Paths docs.
Create a robots.txt so search engines know where to find your sitemap.
Create it at:
- SvelteKit:
/static/robots.txt - TanStack Start:
/public/robots.txt
User-agent: *
Allow: /
Sitemap: https://example.com/sitemap.xml
It's recommended to set up an e2e test, like Playwright, that calls your sitemap route.
Why? For pre-rendered sitemaps, paramValues are loaded at build time, so
misconfigurations fail during the build. But for non-prerendered sitemaps,
paramValues are loaded at runtime, so a functional test is necessary to
catch configuration mistakes before deployment.
Playwright example
// /src/tests/sitemap.test.js
import { expect, test } from '@playwright/test';
test('/sitemap.xml is valid', async ({ page }) => {
const response = await page.goto('/sitemap.xml');
expect(response.status()).toBe(200);
// Ensure XML is valid. Playwright parses the XML here and will error if it
// cannot be parsed.
const urls = await page.$$eval('url', (urls) =>
urls.map((url) => ({
loc: url.querySelector('loc').textContent,
// changefreq: url.querySelector('changefreq').textContent, // if you enabled in your sitemap
// priority: url.querySelector('priority').textContent,
}))
);
// Sanity check
expect(urls.length).toBeGreaterThan(5);
// Ensure entries are in a valid format.
for (const url of urls) {
expect(url.loc).toBeTruthy();
expect(() => new URL(url.loc)).not.toThrow();
// expect(url.changefreq).toBe('daily');
// expect(url.priority).toBe('0.7');
}
});Examples of how to query an SQL database to obtain data to provide as
paramValues for your routes:
-- Route: /blog/[slug]
SELECT slug FROM blog_posts WHERE status = 'published';
-- Route: /blog/tag/[tag]
SELECT DISTINCT LOWER(category) FROM blog_posts WHERE status = 'published';
-- Route: /campsites/[country]/[state]
SELECT DISTINCT LOWER(country), LOWER(state) FROM campsites;Using DISTINCT prevents duplicates in your result set. Use this when your
table could contain multiple rows with the same params, like in the 2nd and 3rd
examples.
Convert the result into a supported param value type. This example uses a 2D array:
const arrayOfArrays = resultFromDB.map((row) => Object.values(row));
// [['usa','new-york'],['usa', 'california']]Then provide these values within your sitemap config's paramValues object for
the appropriate route.
Click to expand
<urlset
xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xhtml="http://www.w3.org/1999/xhtml"
>
<url>
<loc>https://example.com/</loc>
<changefreq>daily</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://example.com/about</loc>
<changefreq>daily</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://example.com/blog</loc>
<changefreq>daily</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://example.com/login</loc>
<changefreq>daily</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://example.com/pricing</loc>
<changefreq>daily</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://example.com/privacy</loc>
<changefreq>daily</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://example.com/signup</loc>
<changefreq>daily</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://example.com/support</loc>
<changefreq>daily</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://example.com/terms</loc>
<changefreq>daily</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://example.com/blog/hello-world</loc>
<changefreq>daily</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://example.com/blog/another-post</loc>
<changefreq>daily</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://example.com/blog/tag/red</loc>
<changefreq>daily</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://example.com/blog/tag/green</loc>
<changefreq>daily</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://example.com/blog/tag/blue</loc>
<changefreq>daily</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://example.com/campsites/usa/new-york</loc>
<changefreq>daily</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://example.com/campsites/usa/california</loc>
<changefreq>daily</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://example.com/campsites/canada/toronto</loc>
<changefreq>daily</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://example.com/foo.pdf</loc>
<changefreq>daily</changefreq>
<priority>0.7</priority>
</url>
</urlset>- Use the new, framework-specific import:
import * as sitemap from 'super-sitemap/sveltekit', orimport * as sitemap from 'super-sitemap/tanstack-start'
langwas renamed tolocales.- Use
locales: { default: 'en', alternates: ['de'] }.
- Use
- Dynamic locale route params must be named
locale.- SvelteKit: use
[[locale]], not[[lang]]. - TanStack Start: use
{-$locale}.
- SvelteKit: use
excludeRoutePatternsnow uses JavaScript regex literals, not strings.- E.g. Use
/^\/dashboard/, not"^/dashboard".
- E.g. Use
excludeRoutePatternsnow matches against validparamValueskeys.- route groups like
(authenticated)cannot be matched against.
- route groups like
sampledUrls()andsampledPaths()were removed.- Use
getSamplePaths()instead.
- Use
2.0.5- FixedparamValuesvalidation to allow empty arrays.2.0.0- BREAKING: Added framework-specific adapters for SvelteKit and TanStack Start. See Migrating from v1 to v2.- Public APIs now live at
super-sitemap/sveltekitandsuper-sitemap/tanstack-start. - The
langconfig option was renamed tolocales; dynamic locale route params must be namedlocale. excludeRoutePatternsnow acceptsRegExpobjects and matches normalized route keys.sampledUrls()andsampledPaths()were removed; usegetSamplePaths().
- Public APIs now live at
1.0.11- Remove all runtime dependencies!1.0.0- BREAKING:priorityrenamed todefaultPriority, andchangefreqrenamed todefaultChangefreq. NON-BREAKING: Support forparamValuesto contain eitherstring[],string[][], orParamValue[]values to allow per-path specification oflastmod,changefreq, andpriority.0.15.0- BREAKING: RenameexcludePatternstoexcludeRoutePatterns.0.14.20- Adds processPaths() callback.0.14.19- Support.mdand.svxroute extensions for mdsvex users.0.14.17- Support for param matchers (e.g.[[lang=lang]]) & required lang params (e.g.[lang]). Thanks @JadedBlueEyes & @epoxide!0.14.13- Support route files named to allow breaking out of a layout.0.14.12- Addsi18nsupport.0.14.11- Addsoptional paramssupport.0.14.0- Addssitemap indexsupport.0.13.0- Added legacysampledUrls()andsampledPaths()utilities.0.12.0- Adds config option to sort'alpha'orfalse(default).0.11.0- BREAKING: Rename tosuper-sitemapon npm! 🚀0.10.0- Adds ability to use unlimited dynamic params per route! 🎉0.9.0- BREAKING: Adds configurablechangefreqandpriorityand excludes these by default. See the README's features list for why.0.8.0- Adds ability to specifyadditionalPathsthat live outside/src/routes, such as/foo.pdflocated at/static/foo.pdf.
git clone https://github.com/jasongitmail/super-sitemap.git
bun install
bun run ready # lint, format, typecheck, and testsRunnable example apps live in examples/sveltekit and examples/tanstack-start.
Each is a self-contained app that imports the library from source and serves as
both an integration test and a dev playground:
cd examples/sveltekit # or examples/tanstack-start
bun install
bun run test # end-to-end sitemap tests against the real framework
bun run dev # browse the example, including /sitemap.xmlRun the interactive release helper:
bun run releaseChoose the semver type with arrow keys. patch is selected by default.
After publish, push the release commit and tag:
git push origin main --follow-tagsConfirm the published npm version:
npm view super-sitemap version time.modified dist-tags- Built by x.com/@zkjason_
- Made possible by TanStack Start and SvelteKit.