AbsoluteJS

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:

1
Discovers page routes
walks every 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.).
2
Walks framework routers
for any wildcard page route like /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.
3
Reads per-page metadata
extracts any literal sitemap: { ... } option you pass to a handle*PageRequest call and applies it to that route's sitemap entries.
4
Filters non-emittable URLs
parameterised routes (:slug, **), redirects, and routes explicitly opted out via data.sitemap === 'exclude' (or each framework's equivalent) are dropped.
5
Writes sitemap.xml
generates the XML and writes it to the build directory where the static file server picks it up.

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.

TS
// 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:

HTML
<?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.

TS
// 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 }
    }
  }
});
baseUrloverrides the auto-detected origin. Use this in production when behind a reverse proxy or load balancer where the internal URL differs from the public one.
defaultChangefreqhow often pages typically change. Defaults to "weekly".
defaultPriorityrelative importance of pages on your site (0.0 to 1.0). Defaults to 0.8.
overridesper-route settings for changefreq, priority, and lastmod.

#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.

TS
// 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] in absolute.config.ts
  • The sitemap option on the page handler call
  • sitemap.defaultChangefreq / defaultPriority in 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.

TS
// 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 the routes callback 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.

TS
// 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) }, // dynamicauto-excluded
  { path: 'settings',  data: { sitemap: 'exclude' }, loadComponent: () => import('../settings/settings').then(m => m.SettingsComponent) },
  { path: '', pathMatch: 'full', redirectTo: 'dashboard' }, // redirectauto-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).

TSX
// 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 /> }, // dynamicauto-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.

TS
// 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 }, // dynamicauto-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.

HTML
<!-- 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).

Exact strings"/admin" drops only that path.
Regex patterns/^\/admin(\/|$)/ drops /admin and every /admin/* sub-route — useful for excluding an entire SPA wing.
Per-route opt-outfor one SPA sub-route, set the framework's route metadata to 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 redirectTo routes 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.

TS
// 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

TS
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;