7 SvelteKit Sitemap Checks for Dynamic Routes

Check SvelteKit dynamic sitemaps with a complete XML endpoint, route inventory, canonical URLs, prerender settings and practical publication checks.

Editorial policy

A SvelteKit sitemap for dynamic routes needs a real content inventory, an XML endpoint and a delivery mode that matches the deployment. A route such as /blog/[slug] defines how a URL is handled; it does not enumerate every published slug or automatically put those pages into sitemap.xml.

Use these seven checks to separate a routing problem from missing records, incorrect URLs, invalid XML or stale delivery. A sitemap that returns 200 can still be an HTML fallback, an old build or an incomplete list.

Check Evidence to inspect
1. Endpoint contract Installed version, route file and actual response type
2. Dynamic route inventory Complete source records and concrete parameter values
3. Execution mode Runtime handler or prerendered XML artifact
4. Public URL format Origin, base path and canonical slash policy
5. XML and eligibility Intended records, escaping, dates and limits
6. Publication freshness Publish, rename and unpublish behavior
7. Deployed result Ordinary public sitemap and representative page targets

The examples assume an existing /blog/[slug] page route. They provide a small demonstration source and sitemap handler, not a complete CMS integration or application scaffold. The domains, records and dates are fictional. Treat a correct sitemap as discovery support, not proof that its URLs are indexed.

1. Confirm the endpoint uses your installed SvelteKit contract

Start in the application directory:

Shell
npm ls @sveltejs/kit

These examples follow the current SvelteKit documentation checked on October 26, 2026; the package registry listed 3.1.0. An older tutorial may target a different API. This is a reason to check compatibility, not a requirement to upgrade your application for a sitemap.

The current routing documentation uses a +server.js or +server.ts file with an uppercase GET handler returning a Response. For /sitemap.xml, the JavaScript file is:

Plain text
src/routes/sitemap.xml/+server.js

A historical src/routes/sitemap.xml.js file exporting lowercase get() and returning { body, headers } is not the current endpoint recipe. Also inspect whether a static file, custom server rule or catch-all route owns the address you are requesting.

As a temporary routing probe, the endpoint can return a minimal XML response:

JavaScript
// src/routes/sitemap.xml/+server.js
export function GET() {
  return new Response(
    '<?xml version="1.0" encoding="UTF-8"?>' +
    '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"/>',
    { headers: { 'Content-Type': 'application/xml; charset=utf-8' } }
  );
}

This empty response proves only that the handler responds. Replace it with the populated handler in check 5 before using the sitemap. Do not submit the probe as your site's page inventory.

Inspect the body as well as the status. If it begins with your application shell or a login page, the sitemap route has not passed. A successful HTML fallback cannot substitute for XML.

Check configuration examples against the installed major too. The current configuration documentation describes SvelteKit 3 configuration in the Vite plugin; older svelte.config.js recipes belong to the earlier configuration model. Avoid copying unrelated deployment rewrites from an old tutorial to resolve an endpoint problem you have not identified.

2. Reconcile content records with real dynamic routes

A [slug] directory cannot tell the sitemap which slugs exist in a CMS. Use the same publication inventory that serves the public pages, and account for every page of the scoped API result. A successful first API page is not a complete collection.

For a runnable demonstration, create src/lib/server/sitemap-data.js:

JavaScript
// Demonstration records only. Replace with your server-side CMS adapter.
export async function getSitemapRecords() {
  return {
    complete: true,
    records: [
      { path: '', published: true, indexable: true, lastmod: null },
      { path: 'blog', published: true, indexable: true, lastmod: null },
      { path: 'blog/first-guide', published: true, indexable: true,
        lastmod: '2026-10-20' },
      { path: 'blog/reference-note', published: true, indexable: false,
        lastmod: null },
      { path: 'blog/future-guide', published: false, indexable: true,
        lastmod: null }
    ]
  };
}

The adapter contract matters more than these field names. path is a decoded path relative to the site's public root, without a leading or trailing slash. published means publicly available now, including any scheduled-publication rule. indexable is your deliberate page policy. lastmod is a known significant-change date or null.

Set complete: true only after all intended records have been retrieved and reconciled with source counts or IDs. The flag is an adapter assertion; the sitemap cannot independently prove that pagination finished. If retrieval fails or a pagination limit is reached, fail the operation rather than returning an empty successful list.

For statically generated dynamic pages, SvelteKit's page-options documentation supports an entries function. For this example's existing /blog/[slug] route, the page's +page.server.js can include:

JavaScript
import { getSitemapRecords } from '$lib/server/sitemap-data.js';

export const prerender = true;

export async function entries() {
  const snapshot = await getSitemapRecords();
  if (snapshot.complete !== true || !Array.isArray(snapshot.records)) {
    throw new Error('Incomplete route inventory');
  }
  return snapshot.records
    .filter((row) => row.published === true &&
      typeof row.path === 'string' && /^blog\/[^/]+$/.test(row.path))
    .map((row) => ({ slug: row.path.slice('blog/'.length) }));
}

Merge the relevant exports into the existing page module; do not replace its working load implementation. Match your actual route parameters and matchers, including locale or nested path segments when present.

There are two different sets here. The public reference-note page may need a generated HTML file even though it deliberately stays out of the sitemap. The future page should not be served as published content yet. In this fixture, entries() returns two blog slugs, while the sitemap later contains three eligible URLs: the root, blog index and first guide. Generating page files and selecting search-discovery URLs are separate decisions.

3. Choose when the sitemap endpoint is executed

An async GET() function does not necessarily run for every visitor. SvelteKit can execute the endpoint during the build and publish its response as a file.

Choose the endpoint's behavior explicitly:

Sitemap endpoint option Delivery model What makes new CMS content appear?
export const prerender = true XML captured during prerendering A successful rebuild and release containing the new artifact
export const prerender = false A handler on a server-capable adapter Fresh source retrieval and the effective application/cache behavior

SvelteKit's prerender documentation distinguishes these modes. A prerendered route is excluded from the dynamic server manifest. The auto option serves a different mixed-generation purpose; it is not a general freshness switch.

A subtle detail matters for sitemaps: +server.js files are not affected by parent layouts. A server route can acquire a prerender default from a page that fetches it, so inspect the endpoint's own option rather than assuming the root layout settles its behavior. The complete runtime example in check 5 exports prerender = false. For a static deployment, change that endpoint export to true and verify the generated XML artifact.

With adapter-static, the deployed host serves files. It cannot run a CMS-backed endpoint later simply because the source contains a handler. Check the adapter's configured output, confirm sitemap.xml is present and ensure the deployment includes it. An SPA fallback page is not a replacement for the missing XML.

Keep page generation in the same review. A correct sitemap pointing at a dynamic page that was never generated on a static host still produces an unusable destination. Verify both the XML artifact and the expected page artifacts before calling the static release ready.

4. Match the public origin, base path and slash policy

Build sitemap URLs from the intended production address, not blindly from the incoming request. Development, preview deployments and prerendering can provide an origin that is unsuitable for your public inventory.

For this example, create src/lib/server/sitemap-format.js:

JavaScript
const PUBLIC_ROOT = 'https://www.example.com/';

export function publicUrl(path) {
  if (typeof path !== 'string' || /[?#\\%]/.test(path) ||
      path.startsWith('/') || path.endsWith('/')) {
    throw new Error('Expected a decoded content path relative to PUBLIC_ROOT');
  }
  const segments = path === '' ? [] : path.split('/');
  if (segments.some((part) => part === '' || part === '.' || part === '..')) {
    throw new Error('Invalid content path segment');
  }
  return new URL(segments.map(encodeURIComponent).join('/'), PUBLIC_ROOT).href;
}

Paths are relative to PUBLIC_ROOT without a leading slash. The empty path represents the root. Other paths use the no-trailing-slash convention, such as blog/first-guide. The helper rejects query strings, fragments, backslashes, pre-encoded percent sequences and dot segments. It encodes each decoded segment once, rather than assuming a CMS slug is already a safe URL.

Use your application's real canonical URL builder when its rules differ. For a subpath deployment, align the public root and path policy with paths.base; do not prepend the base twice. Confirm the homepage's exact canonical form separately. Localized, nested or already-encoded paths need a deliberate mapping, not a guessed cleanup rule.

The current paths documentation also warrants care when copying snippets: legacy base, assets and resolveRoute exports were removed in SvelteKit 3. The documented resolve() helper can return relative paths during server rendering. A navigation helper's output is not automatically an absolute canonical sitemap URL.

Compare representative output with the actual page response and canonical declaration. If the page normalizes /blog/first-guide/ to /blog/first-guide, the sitemap should agree with the intended final form. A canonical annotation is a signal, not proof of Google's selected canonical. Screpy's Canonical Tag Checker can help inspect the declaration on a live page.

5. Serialize only eligible records into valid XML

Now replace the temporary endpoint from check 1 with this complete handler in src/routes/sitemap.xml/+server.js. It uses the two server modules above and starts in request-time mode. For a static deployment, change this endpoint's prerender value to true and follow the build checks in section 3.

JavaScript
import { getSitemapRecords } from '$lib/server/sitemap-data.js';
import { publicUrl } from '$lib/server/sitemap-format.js';

export const prerender = false;

function escapeXml(value) {
  return value.replace(/[&<>"']/g, (character) => ({
    '&': '&amp;', '<': '&lt;', '>': '&gt;',
    '"': '&quot;', "'": '&apos;'
  })[character]);
}

function lastmodElement(value) {
  if (value === null) return '';
  if (typeof value !== 'string' ||
      !/^\d{4}-\d{2}-\d{2}$/.test(value) || value.startsWith('0000-')) {
    throw new Error('Expected a known calendar date or null');
  }
  const date = new Date(`${value}T00:00:00.000Z`);
  if (!Number.isFinite(date.getTime()) ||
      date.toISOString().slice(0, 10) !== value) {
    throw new Error('Invalid lastmod date');
  }
  return `<lastmod>${value}</lastmod>`;
}

export async function GET() {
  const snapshot = await getSitemapRecords();
  if (snapshot.complete !== true || !Array.isArray(snapshot.records) ||
      snapshot.records.length === 0) {
    throw new Error('Incomplete sitemap inventory');
  }

  const seen = new Set();
  const entries = [];
  for (const row of snapshot.records) {
    if (!row || typeof row.published !== 'boolean' ||
        typeof row.indexable !== 'boolean') {
      throw new Error('Invalid publication policy');
    }
    if (!row.published || !row.indexable) continue;

    const loc = publicUrl(row.path);
    if (seen.has(loc)) throw new Error('Duplicate sitemap URL');
    seen.add(loc);
    entries.push(`<url><loc>${escapeXml(loc)}</loc>${lastmodElement(row.lastmod)}</url>`);
  }

  if (entries.length === 0 || entries.length > 50_000) {
    throw new Error('Sitemap needs a nonempty inventory of at most 50000 URLs');
  }
  const xml = '<?xml version="1.0" encoding="UTF-8"?>\n' +
    '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n' +
    entries.join('\n') + '\n</urlset>';
  if (new TextEncoder().encode(xml).byteLength > 52_428_800) {
    throw new Error('Sitemap exceeds the uncompressed size limit');
  }

  return new Response(xml, {
    headers: {
      'Content-Type': 'application/xml; charset=utf-8',
      'Cache-Control': 'no-store'
    }
  });
}

The demonstration records should produce three URLs: the homepage, /blog, and /blog/first-guide. The public but non-indexable reference note remains available to the route renderer while staying out of this sitemap. The unpublished guide appears in neither inventory of public pages.

This handler deliberately fails when its source is incomplete or its eligible URL list is empty. An error is visible to monitoring; a successful empty response can silently replace a useful sitemap. Adapt that policy deliberately if your site genuinely has no eligible pages. The complete flag still depends on your CMS adapter honestly finishing pagination; the handler cannot prove completeness from a boolean.

The namespace uses http://, even when every page URL uses HTTPS. XML escaping and URL encoding solve different problems: encode path segments when forming a URL, then escape the resulting text for XML. Do not paste raw CMS values into markup.

Dates are optional. This example accepts a real YYYY-MM-DD date or null, and rejects impossible dates such as February 30. Map the field to a significant change to that page's content, not the time a visitor requested the sitemap or a cosmetic sitewide deployment. Google ignores priority and changefreq, so they add no value here. See Google's sitemap guidance.

The sitemap protocol limits each uncompressed sitemap to 50,000 URLs and 52,428,800 bytes. Split larger inventories into separate sitemap files and a sitemap index; removing the guard without implementing that split is not a solution.

Cache-Control: no-store is an intentional starting point for observing request-time behavior. It is not a promise that your hosting platform has no other cached response. Once correctness is established, choose a cache policy that matches your real publication process.

6. Verify publication freshness across build and cache layers

A correct XML generator can still serve yesterday's inventory. Trace one controlled content change through the full delivery path, using a staging site or a planned real publication rather than inventing a live article just to test a sitemap.

Content change Expected sitemap change Separate page check
Publish a new eligible guide Add its canonical URL The complete page is publicly available
Rename a public slug Replace the old URL with the new canonical URL The old URL follows the intended redirect; the destination works
Unpublish or remove a guide Remove its URL The old page follows your intended removal or access policy
Make a public guide noindex Remove its URL The page can still be served, with the intended indexing directive
Correct a substantial factual error Update its reliable lastmod date, if available The corrected content is actually visible

For a prerendered sitemap, a CMS update alone does not replace the deployed XML file. Verify that your publication process builds from the updated inventory and releases both the sitemap and affected pages together. Check the generated file and then the ordinary production URL. A successful webhook delivery is not proof that a new release became active.

For a request-time sitemap, distinguish the CMS response, application-level data caching, the endpoint response and any hosting cache. An async function can read stale data. Inspect the actual response headers and content rather than assuming that executing a handler implies a fresh source.

Use the same canonical sitemap URL that readers and crawlers receive. A preview address, local development server or query parameter that happens to avoid a cache does not establish what the ordinary URL serves. If a cache keeps serving the old inventory, record that as a failed freshness check and resolve it through the site's existing release or cache process.

Also check the two directions independently: a new URL in XML does not prove its page has been deployed, and a new page in the blog does not prove it has reached the sitemap. This is especially important when the endpoint runs on a server but the target pages are static build artifacts.

Finally, preserve useful dates. A build triggered by an unrelated page should not advance every entry's lastmod. Reliable modification information is more useful than a sitemap whose entire history appears to change on every request.

7. Check the deployed sitemap and its actual page targets

Start by saving the ordinary sitemap response. This command does not follow redirects, so an unexpected redirect remains visible in the headers. In Windows PowerShell, use curl.exe if curl is an alias for another command.

Shell
curl -sS -D sitemap-headers.txt --output sitemap.xml https://www.example.com/sitemap.xml

Inspect the status and Content-Type, then parse the saved body. A 200 response containing a hosting fallback page is a failure. For the fictional inventory in this article, save the following as check-sitemap.py beside sitemap.xml and run it with Python 3:

Python
from pathlib import Path
import xml.etree.ElementTree as ET

namespace = '{http://www.sitemaps.org/schemas/sitemap/0.9}'
root = ET.fromstring(Path('sitemap.xml').read_bytes())
assert root.tag == namespace + 'urlset', 'Wrong XML root or namespace'

urls = root.findall(namespace + 'url')
locations = []
for entry in urls:
    loc = entry.find(namespace + 'loc')
    assert loc is not None and loc.text, 'Missing location'
    locations.append(loc.text)

expected = {
    'https://www.example.com/',
    'https://www.example.com/blog',
    'https://www.example.com/blog/first-guide',
}
assert len(locations) == len(set(locations)), 'Duplicate locations'
assert set(locations) == expected, 'Missing or unexpected locations'
assert len(urls) == 3, 'Unexpected entry count'
print('The three demonstration URLs match the expected inventory.')

Replace the fictional expected set with an independently checked list of your site's eligible canonical URLs. Comparing two equal counts is not enough: a missing guide and an unwanted draft can cancel each other out. For a large site, reconcile source record IDs and generated URLs before splitting files, then check each sitemap's membership. This small script checks the demonstration's namespace and membership; it is not a complete XML schema or live-page validator.

Then inspect representative target pages, including a dynamic page and any route using a base path or unusual characters. Verify a successful response, the expected substantive page, its intended canonical and no accidental noindex in HTML or HTTP headers. Check redirects explicitly. A sitemap should list final canonical destinations rather than relying on a chain of redirects to fix its URLs.

You can use Screpy's Sitemap Validator for an additional sitemap check. Screpy publishes this guide; the tool does not replace the CMS inventory reconciliation or the release checks above.

Make the canonical sitemap discoverable through the site's existing robots.txt, for example:

Plain text
Sitemap: https://www.example.com/sitemap.xml

You can also submit that URL in the property's Sitemaps report in Google Search Console. Check its fetch and processing results after the real file is available. Submission does not guarantee that every listed page will be crawled or indexed; use individual URL inspection when investigating a specific page. Google's sitemap documentation explains the supported discovery methods.

The release is ready when the content inventory, XML response and actual target pages agree at their ordinary public URLs. For broader decisions about which pages belong in the file, see our guide to optimizing XML sitemaps.

Related posts

Keep reading practical SEO guides from the Screpy blog.

View all posts