Sitemap Generation
generateSitemap() builds a sitemap.xml string from your route tree. It's a pure function over tree data — no filesystem access, no server — so you run it wherever fits your deploy: a build script, a scheduled job, or an on-demand route handler.
import { generateSitemap } from '@emkodev/emroute/runtime/sitemap';
const xml = await generateSitemap(routeTree, {
baseUrl: 'https://example.com',
});
routeTree is the RouteNode tree. There's no getter for it on a running Emroute instance — ComponentContext doesn't carry it either — so read it the same way bootEmrouteApp() does: fetch ROUTES_MANIFEST_PATH off your Runtime and parse it.
Static routes are automatic
Every route with no dynamic segment is included with no extra config — /about, /pricing, directory-index catch-alls, all walked from the tree.
Dynamic routes need an enumerator
A route like /projects/:id has no fixed set of URLs, so it's excluded by default. Provide an enumerators function keyed by the route's pattern (not a URL, and not basePath-prefixed) to list the concrete values:
const xml = await generateSitemap(routeTree, {
baseUrl: 'https://example.com',
enumerators: {
'/projects/:id': async () => {
const projects = await db.projects.findAll();
return projects.map((p) => p.id);
},
},
});
Key format:
enumeratorsandroutes(below) are keyed by the raw route pattern as it appears in the tree —/projects/:id, not/html/projects/:id. ThebasePathoption controls the URL prefix written into<loc>, it does not change how you key these maps. Mixing the two up silently drops the override — no error, the route just falls back todefaults.
Per-route and default metadata
<lastmod>, <changefreq>, <priority> are all optional per the sitemap protocol. Set defaults for everything, then override specific routes:
const xml = await generateSitemap(routeTree, {
baseUrl: 'https://example.com',
defaults: { changefreq: 'weekly', priority: 0.5 },
routes: {
'/': { priority: 1.0, changefreq: 'daily' },
'/pricing': { priority: 0.8 },
},
});
basePath
As of 1.12.7, defaults to /html — the canonical, crawlable SSR view. /app renders the same shell for every route (nothing route-specific for a crawler to index) and bare paths just redirect, so neither belongs in a sitemap. On 1.12.6 and earlier, the default was an empty prefix — pass basePath: '/html' explicitly if you're pinned to an older release:
const xml = await generateSitemap(routeTree, {
baseUrl: 'https://example.com',
basePath: '/html', // default — usually omit this
});
Pass '' only if your route tree's patterns already include a path prefix you want reflected as-is.
Serving it: a build-time script, not a route
generateSitemap() returns a string. The route tree only changes when your code does — a new route is a redeploy either way — so treat sitemap.xml as a build artifact, not something computed per-request. This guide's own site does exactly that, in scripts/generate-sitemap.ts:
import { UniversalFsRuntime } from '@emkodev/emroute/runtime/universal/fs';
import { ROUTES_MANIFEST_PATH } from '@emkodev/emroute/runtime';
import { generateSitemap } from '@emkodev/emroute/runtime/sitemap';
import { resolve } from 'node:path';
const SITE_URL = 'https://example.com';
const appRoot = resolve(import.meta.dirname!, '..');
const runtime = new UniversalFsRuntime(appRoot);
const tree = await (await runtime.query(ROUTES_MANIFEST_PATH)).json();
const xml = await generateSitemap(tree, {
baseUrl: SITE_URL,
defaults: { changefreq: 'weekly', priority: 0.5 },
routes: { '/': { priority: 1.0 } },
});
await Deno.writeTextFile(resolve(appRoot, 'sitemap.xml'), xml);
Run it as a task (deno task sitemap / an npm script), commit the output, and let your server serve sitemap.xml as a static file alongside robots.txt. Re-run it whenever you add or remove routes.
Limits
Capped at 50,000 URLs per the sitemap protocol — generateSitemap() stops collecting once it hits the limit rather than producing an invalid file. For larger sites, shard by section (call it once per top-level route prefix) and serve a sitemap index that links to each shard.
Next: Server Setup