Sitemap
AbsoluteJS automatically generates a sitemap.xml for your site. No plugin, no manual route list: it discovers your pages on server start.
#How It Works
When your server starts, AbsoluteJS automatically:
GET route registered on your Elysia app and inspects each handler's source for a call to one of the handle*PageRequest helpers (handleAngularPageRequest, handleReactPageRequest, etc.)./portal/*, it statically parses the SPA's framework router config (Angular's provideRouter, React Router's createBrowserRouter, Vue Router's createRouter, or AbsoluteJS's Svelte <Router>) and emits one entry per non-dynamic, non-redirect leaf.sitemap: { ... } option you pass to a handle*PageRequest call and applies it to that route's sitemap entries.:slug, **), redirects, and routes explicitly opted out via data.sitemap === 'exclude' (or each framework's equivalent) are dropped.This runs in the background and does not block server startup. The sitemap is available at /sitemap.xml as soon as generation completes. Everything happens via static source analysis — no user code is invoked, no synthetic requests are made, no framework bootstrap runs.
#Zero Config
Sitemap generation is enabled automatically. No additional configuration is needed. AbsoluteJS writes the sitemap directly to the build directory.
// absolute.config.ts : sitemap is automatic
import { defineConfig } from '@absolutejs/absolute';
export default defineConfig({
reactDirectory: './src/frontend'
// That's it. Sitemap generation is built in.
// On server start, AbsoluteJS discovers your page routes
// and writes sitemap.xml to the build directory.
});#Example Output
The generated sitemap.xml follows the standard sitemap protocol and includes only routes that return HTML pages:
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<!-- Top-level pages from .get() handlers -->
<url>
<loc>https://mysite.com/</loc>
<changefreq>daily</changefreq>
<priority>1</priority>
</url>
<url>
<loc>https://mysite.com/signin</loc>
<changefreq>monthly</changefreq>
<priority>0.3</priority>
</url>
<!-- SPA sub-routes discovered from the page module's router config -->
<url>
<loc>https://mysite.com/portal/dashboard</loc>
<changefreq>weekly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://mysite.com/portal/profile</loc>
<changefreq>weekly</changefreq>
<priority>0.6</priority>
</url>
</urlset>#Configuration
Add a sitemap object to your absolute.config.ts to customize the output. All fields are optional.
// absolute.config.ts : with sitemap options
import { defineConfig } from '@absolutejs/absolute';
export default defineConfig({
reactDirectory: './src/frontend',
publicDirectory: './public',
sitemap: {
// Public origin for production. Defaults to the server's listening
// origin (e.g. http://localhost:3000) when unset — typically you'd
// env-gate this so dev keeps localhost.
baseUrl: process.env.SITE_URL,
// Defaults applied to every URL unless overridden per-route.
defaultChangefreq: 'daily',
defaultPriority: 0.7,
// Drop matching routes from the sitemap. Strings match exactly;
// regexes test against the full URL path.
exclude: [
'/admin',
/^\/internal/,
/^\/admin(\/|$)/ // also drops every /admin/* sub-route
],
// Final override layer — beats handler-level sitemap options.
overrides: {
'/': { priority: 1.0, changefreq: 'daily' },
'/blog': { lastmod: '2026-03-28', priority: 0.9 }
}
}
});"weekly".0.8.#Per-Page Metadata in Handlers
Every framework's page-handler input accepts an optional sitemap field. AbsoluteJS reads it statically from the handler source at registration time and applies it to the sitemap entries produced from that route — no extra config file to keep in sync, no runtime cost in the handler itself.
// Backend route handler — pass an optional 'sitemap' option to
// handle*PageRequest. AbsoluteJS reads it statically from the handler
// source at registration time and uses it on the sitemap entries
// produced from that route.
import { handleAngularPageRequest } from '@absolutejs/absolute/angular';
app
.get('/', async ({ request }) =>
handleAngularPageRequest<typeof HomePage>({
request,
pagePath: 'Home',
indexPath: 'assets/index.js',
sitemap: { changefreq: 'daily', priority: 1.0 }
})
)
.get('/signin', async ({ request }) =>
handleAngularPageRequest<typeof SigninPage>({
request,
pagePath: 'Signin',
indexPath: 'assets/index.js',
sitemap: { changefreq: 'monthly', priority: 0.3 }
})
);
// The sitemap option is type-checked but has no runtime cost — the
// page handler ignores it. Values must be literals (strings / numbers);
// computed expressions can't be read statically, so use sitemap.overrides
// in absolute.config.ts for those.For a wildcard SPA route like /portal/*, the metadata applies to every SPA sub-route emitted from that mount point. To target a specific sub-route inside an SPA, use the framework's per-route metadata slot — see SPA Sub-Routes.
Precedence, highest first:
sitemap.overrides[path]inabsolute.config.ts- The
sitemapoption on the page handler call sitemap.defaultChangefreq/defaultPriorityin the config- Built-in defaults (
weekly/0.8)
Caveat : values must be literal strings and numbers — the field is read by inspecting the handler's source text, not by invoking the handler. A computed value like sitemap: getMetadata() won't be picked up; use sitemap.overrides in the config for those cases.
#SPA Sub-Routes
Single-page apps mounted under a wildcard Elysia route — like app.get('/portal/*', ...) — host many client-side URLs that the server never registers individually. AbsoluteJS discovers those URLs by statically reading your page module's framework router config and emits one sitemap entry per non-dynamic leaf, prefixed by the mount path.
// Backend Elysia route that hosts the Angular SPA at /portal/*.
// AbsoluteJS sees the wildcard, matches its mount path (/portal)
// against the page module's APP_BASE_HREF (/portal/), and emits one
// sitemap entry per non-dynamic Angular Route under that mount.
import { handleAngularPageRequest } from '@absolutejs/absolute/angular';
app.get('/portal/*', async ({ request }) =>
handleAngularPageRequest<typeof PortalPage>({
request,
pagePath: 'Portal',
indexPath: 'assets/portal/index.js',
sitemap: { changefreq: 'weekly', priority: 0.6 }
})
);
// The wildcard mount itself isn't emitted (it isn't a destination URL)
// — every leaf under the page's Router config is.The mount path comes from each framework's idiomatic base-URL slot. The route list comes from the same router config your app uses to navigate at runtime — there's no second source of truth to maintain.
- Dynamic segments (
:id,**,*) are skipped automatically. Use theroutescallback in the sitemap config to enumerate parameterised URLs from a database or CMS. - Per-sub-route opt-out : set
sitemap: 'exclude'on a single Route inside the SPA config (slot name varies by framework — see below). - Nested routes work too; child paths are joined to the parent's path during emission.
#Angular
Mount path comes from the page module's { provide: APP_BASE_HREF, useValue: '/portal/' }provider. Routes come from the first argument to provideRouter(...). Per-route opt-out uses the built-in Route.data slot.
// Angular page module (e.g. portal.ts) — no doc-specific changes.
// AbsoluteJS statically reads the APP_BASE_HREF useValue and the
// Routes array you pass to provideRouter() at sitemap-generation time.
import { APP_BASE_HREF } from '@angular/common';
import { provideRouter, type Routes } from '@angular/router';
const routes: Routes = [
{ path: 'dashboard', loadComponent: () => import('../dashboard/dashboard').then(m => m.DashboardComponent) },
{ path: 'profile', loadComponent: () => import('../profile/profile').then(m => m.ProfileComponent) },
{ path: 'users/:id', loadComponent: () => import('../user/user').then(m => m.UserComponent) }, // dynamic — auto-excluded
{ path: 'settings', data: { sitemap: 'exclude' }, loadComponent: () => import('../settings/settings').then(m => m.SettingsComponent) },
{ path: '', pathMatch: 'full', redirectTo: 'dashboard' }, // redirect — auto-excluded
];
export const providers = [
provideRouter(routes),
{ provide: APP_BASE_HREF, useValue: '/portal/' }
];
// Emits: /portal/dashboard, /portal/profile
// (skips :id, settings opt-out, and the empty redirect)#React
Mount path comes from the basename option of createBrowserRouter. Routes come from the first argument. Per-route opt-out uses Route.handle (React Router's free-form metadata slot).
// React page — AbsoluteJS reads the createBrowserRouter routes and
// the basename option at sitemap-generation time.
import { createBrowserRouter } from 'react-router-dom';
const routes = [
{ path: 'dashboard', element: <Dashboard /> },
{ path: 'profile', element: <Profile /> },
{ path: 'users/:id', element: <User /> }, // dynamic — auto-excluded
{ path: 'settings', handle: { sitemap: 'exclude' }, element: <Settings /> },
];
const router = createBrowserRouter(routes, { basename: '/portal' });
// Emits: /portal/dashboard, /portal/profile#Vue
Mount path comes from the argument to createWebHistory('/portal/'). Routes come from the routes option of createRouter. Per-route opt-out uses Route.meta.
// Vue page — AbsoluteJS reads the createWebHistory base and the routes
// option passed to createRouter() at sitemap-generation time.
import { createRouter, createWebHistory } from 'vue-router';
const routes = [
{ path: '/dashboard', component: Dashboard },
{ path: '/profile', component: Profile },
{ path: '/users/:id', component: User }, // dynamic — auto-excluded
{ path: '/settings', meta: { sitemap: 'exclude' }, component: Settings },
];
createRouter({
history: createWebHistory('/portal/'),
routes
});
// Emits: /portal/dashboard, /portal/profile#Svelte
Mount path comes from the basepath attribute on AbsoluteJS's <Router> component. Routes come from child <Route path="..."> tags.
<!-- Svelte page — AbsoluteJS reads <Router basepath> and child
<Route path> tags. -->
<script>
import Router from '@absolutejs/absolute/svelte/router/Router.svelte';
import Route from '@absolutejs/absolute/svelte/router/Route.svelte';
import Dashboard from '../dashboard.svelte';
import Profile from '../profile.svelte';
</script>
<Router basepath="/portal">
<Route path="/dashboard"><Dashboard /></Route>
<Route path="/profile"><Profile /></Route>
</Router>
<!-- Emits: /portal/dashboard, /portal/profile -->Caveat : analysis is source-level. Routes built by a runtime function (const routes = computeRoutes(env)) or a base path pulled from a variable (useValue: getBasePath()) can't be read statically — fall back to sitemap.routes in the config to supply those URLs by hand.
#Excluding Routes
Use the exclude array in absolute.config.ts to keep routes out of the sitemap. Each entry can be a string (exact match) or a regex (tested against the full URL path).
"/admin" drops only that path./^\/admin(\/|$)/ drops /admin and every /admin/* sub-route — useful for excluding an entire SPA wing.sitemap: 'exclude' (Angular Route.data, React Router Route.handle, Vue Router Route.meta).Three things are dropped automatically and don't need to appear in exclude:
- Routes with
:param,*, or**segments in their path (dynamic, can't enumerate). - Handlers that don't call any
handle*PageRequest(treated as API endpoints, not pages). - Pure
redirectToroutes inside an SPA router config — they aren't destination URLs.
Wildcard routes are kept when their handler is a page handler — they're the mount points for SPA sub-routes. The wildcard URL itself isn't emitted (it has no canonical destination); the sitemap emits the SPA's actual leaf URLs instead.
#Dynamic Routes
Parameterized routes like /blog/:slug can't be auto-discovered because AbsoluteJS doesn't know what slugs exist. Use the routes function to provide them. It can be async, so you can query a database or CMS.
// absolute.config.ts : with dynamic routes
import { defineConfig } from '@absolutejs/absolute';
export default defineConfig({
reactDirectory: './src/frontend',
publicDirectory: './public',
sitemap: {
baseUrl: 'https://mysite.com',
// Provide additional routes that can't be auto-discovered
// (e.g. parameterized routes like /blog/:slug).
routes: async () => {
const posts = await db.query('SELECT slug FROM posts');
return posts.map(p => `/blog/${p.slug}`);
}
}
});Routes returned by this function are added alongside the auto-discovered pages. They respect the same exclude patterns and overrides configuration.
#Type Reference
type SitemapConfig = {
baseUrl?: string;
exclude?: (string | RegExp)[];
defaultChangefreq?: ChangeFrequency;
defaultPriority?: number;
overrides?: Record<string, SitemapRouteOverride>;
routes?: () => string[] | Promise<string[]>;
};
type ChangeFrequency =
| 'always' | 'hourly' | 'daily'
| 'weekly' | 'monthly' | 'yearly' | 'never';
type SitemapRouteOverride = {
changefreq?: ChangeFrequency;
priority?: number;
lastmod?: string;
};
// Accepted as an optional 'sitemap' field on every framework's
// page-handler input (handleAngularPageRequest, handleReactPageRequest,
// handleSveltePageRequest, handleVuePageRequest, handleHTMLPageRequest).
type PageHandlerSitemapMetadata = SitemapRouteOverride;