Sitemap Plugin

An XML sitemap generator plugin for Svelar/SvelteKit with a fluent builder API, model-based URL generation, sitemap index support, image sitemaps, scheduled regeneration, and both static file output and dynamic route serving.

Package: @beeblock/svelar-sitemap

Requires Node.js >=22.12.0 and Svelar ^0.8.0. Sitemap builders and plugin registration are server APIs.

Install:

npx svelar plugin:install @beeblock/svelar-sitemap

Imports:

// Plugin registration
import SvelarSitemapPlugin from '@beeblock/svelar-sitemap/plugin';

// Core API
import { Sitemap, SitemapGenerator, SitemapIndex, SitemapUrl } from '@beeblock/svelar-sitemap';

// Server-side (controller, scheduled task)
import { SitemapController, SitemapTask } from '@beeblock/svelar-sitemap/server';

// Types
import type { ChangeFreq, SitemapImage, SitemapUrlOptions, SitemapModelOptions, SitemapConfig, SitemapPluginConfig, SitemapIndexEntry } from '@beeblock/svelar-sitemap';

Quick Start

1. Register the Plugin

// src/lib/plugins.ts
import SvelarSitemapPlugin from '@beeblock/svelar-sitemap/plugin';

export const sitemapPlugin = new SvelarSitemapPlugin({
  baseUrl: 'https://example.com',
  defaultChangeFreq: 'weekly',
  defaultPriority: 0.5,
  endpoint: '/sitemap.xml',
});

2. Serve the Sitemap Dynamically

// src/routes/sitemap.xml/+server.ts
import { SitemapController } from '@beeblock/svelar-sitemap/server';
import { Post } from '$lib/models/Post';

// Reviewed public functional-RBAC exemption: intentionally public URLs only.
export const GET = SitemapController.handle(async (sitemap) => {
  sitemap.add('/', { changeFreq: 'daily', priority: 1.0 });
  sitemap.add('/about', { changeFreq: 'monthly', priority: 0.8 });
  sitemap.add('/contact', { changeFreq: 'monthly', priority: 0.5 });

  await sitemap.addModel(Post, {
    url: (post) => `/blog/${post.slug}`,
    lastmod: (post) => post.updated_at,
    changeFreq: 'weekly',
    priority: 0.7,
    query: (q) => q.where('published', true),
  });

  return sitemap;
});

Configuration

The SvelarSitemapPlugin constructor accepts:

Option Type Default Description
baseUrl string 'http://localhost' Base URL for all sitemap entries
defaultChangeFreq ChangeFreq 'weekly' Default change frequency for entries
defaultPriority number 0.5 Default priority (0.0 to 1.0)
outputPath string 'static/sitemap.xml' File path for static sitemap generation
maxUrlsPerSitemap number 50000 Explicit chunk size, an integer from 1 to 50000
endpoint string '/sitemap.xml' Stored route preference; the published stub uses /sitemap.xml

ChangeFreq values: 'always', 'hourly', 'daily', 'weekly', 'monthly', 'yearly', 'never'

The plugin extends Svelar's Plugin class and default-exports its class from /plugin; the named /server export remains available. Constructor options are exposed through .options, while .config() returns the framework descriptor. Move the published route file when using a different endpoint. Facade configuration is shared between the root, plugin, and server entries.

Sitemap.configure() and Sitemap.create() reject zero, negative, non-integer, non-finite, or over-50000 chunk limits with RangeError. Splitting is explicit: use .splitUrls(), generate each child sitemap, and build a SitemapIndex for those files.


Core API

Sitemap Builder

The main class for constructing sitemaps. Create instances via Sitemap.create():

import { Sitemap } from '@beeblock/svelar-sitemap';

Static methods:

Method Returns Description
Sitemap.configure(config) void Set global configuration
Sitemap.getConfig() Readonly<SitemapConfig> Get current global configuration
Sitemap.create(config?) Sitemap Create a new builder instance

Instance methods:

Method Returns Description
.add(path, options?) Sitemap Add a single URL entry
.addModel(model, options) Promise<Sitemap> Add URLs from model records
.toXml() Promise<string> Generate the sitemap XML string
.writeToFile(path?) Promise<void> Write XML to a file
.count number Total number of URLs
.getUrls() readonly SitemapUrl[] Get all URL entries
.needsIndex() boolean Whether the sitemap exceeds maxUrlsPerSitemap
.splitUrls() SitemapUrl[][] Split URLs into chunks for index generation

Adding static URLs:

const sitemap = Sitemap.create();

sitemap.add('/', {
  lastmod: '2026-04-01',
  changeFreq: 'daily',
  priority: 1.0,
});

sitemap.add('/about', {
  changeFreq: 'monthly',
  priority: 0.8,
  images: [
    { url: '/images/team.jpg', title: 'Our Team', caption: 'The team photo' },
  ],
});

Adding model URLs:

await sitemap.addModel(Post, {
  url: (post) => `/blog/${post.slug}`,
  lastmod: (post) => post.updated_at,
  changeFreq: 'weekly',
  priority: 0.7,
  images: (post) => post.featured_image
    ? [{ url: post.featured_image, title: post.title }]
    : [],
  query: (q) => q.where('published', true),
});

SitemapUrlOptions:

Option Type Description
lastmod string | Date Last modification date (formatted as YYYY-MM-DD)
changeFreq ChangeFreq How frequently the page changes
priority number Priority from 0.0 to 1.0 (clamped automatically)
images SitemapImage[] Image entries per the image sitemap extension

SitemapModelOptions:

Option Type Description
url (record) => string Required. Returns the URL path for each record
lastmod (record) => string | Date | undefined Optional. Returns the last modification date
changeFreq ChangeFreq Default change frequency for model URLs
priority number Default priority for model URLs
images (record) => SitemapImage[] Optional. Returns images for each record
query (q) => any Optional. Filters the model query

SitemapUrl

Represents a single URL entry in the sitemap:

const url = new SitemapUrl('/blog/my-post', {
  lastmod: '2026-04-01',
  changeFreq: 'weekly',
  priority: 0.7,
  images: [{ url: '/images/post.jpg', title: 'Post Image' }],
});

url.loc;        // '/blog/my-post'
url.lastmod;    // '2026-04-01'
url.changeFreq; // 'weekly'
url.priority;   // 0.7
url.images;     // SitemapImage[]

const xml = url.toXml('https://example.com');

SitemapGenerator

Low-level XML generator. Used internally by Sitemap.toXml():

import { SitemapGenerator } from '@beeblock/svelar-sitemap';

const xml = SitemapGenerator.generate(urls, 'https://example.com');

Automatically includes the image namespace if any URL has images.

SitemapIndex

Builds a sitemap index XML document for large sites that split across multiple sitemaps:

import { SitemapIndex } from '@beeblock/svelar-sitemap';

const index = SitemapIndex.create('https://example.com');

index.addSitemap('/sitemap-posts.xml', '2026-04-01');
index.addSitemap('/sitemap-products.xml', new Date());
index.addSitemap('/sitemap-pages.xml');

const xml = index.toXml();
Method Returns Description
SitemapIndex.create(baseUrl?) SitemapIndex Create a new index builder
.addSitemap(loc, lastmod?) SitemapIndex Add a sub-sitemap reference
.toXml() string Generate the sitemap index XML

Server-Side

SitemapController

Handles dynamic sitemap serving from SvelteKit routes:

Review these GET routes as public functional-RBAC exemptions only for intentionally public URLs. Filter model queries to published/public records and preserve any required tenant boundary. The controller uses public caching; do not place private user-specific URLs or metadata in that response. It returns an empty XML document with status 500 on generation failure without exposing the exception message.

Register the reviewed public binding in the application's literal manifest (--public when scaffolding the route/controller). Keep the common authentication/policy/validation middleware active. These read-only GET exemptions do not authorize scheduler control or any write endpoint.

// src/routes/sitemap.xml/+server.ts
import { SitemapController } from '@beeblock/svelar-sitemap/server';

// Reviewed public functional-RBAC exemption: intentionally public URLs only.
export const GET = SitemapController.handle(async (sitemap) => {
  sitemap.add('/', { changeFreq: 'daily', priority: 1.0 });
  sitemap.add('/about');
  // ... add more URLs
  return sitemap;
});
// src/routes/sitemap-index.xml/+server.ts
import { SitemapController } from '@beeblock/svelar-sitemap/server';
import { SitemapIndex } from '@beeblock/svelar-sitemap';

// Reviewed public functional-RBAC exemption: public child-sitemap references only.
export const GET = SitemapController.handleIndex(async () => {
  const index = SitemapIndex.create('https://example.com');
  index.addSitemap('/sitemap-pages.xml', new Date());
  index.addSitemap('/sitemap-posts.xml', new Date());
  return index.toXml();
});

Response headers set by the controller on success:

  • Content-Type: application/xml; charset=utf-8
  • Cache-Control: public, max-age=3600, s-maxage=3600
  • X-Robots-Tag: noindex

SitemapTask

An abstract class that extends core ScheduledTask for scheduled sitemap regeneration. Extend it and implement buildSitemap():

// src/lib/scheduler/GenerateSitemap.ts
import { SitemapTask } from '@beeblock/svelar-sitemap/server';
import { Post } from '$lib/models/Post';

export default class GenerateSitemap extends SitemapTask {
  schedule() {
    return this.daily();
  }

  async buildSitemap(sitemap) {
    sitemap.add('/', { changeFreq: 'daily', priority: 1.0 });
    sitemap.add('/about', { changeFreq: 'monthly', priority: 0.8 });

    await sitemap.addModel(Post, {
      url: (p) => `/blog/${p.slug}`,
      lastmod: (p) => p.updated_at,
      changeFreq: 'weekly',
      priority: 0.7,
      query: (q) => q.where('published', true),
    });

    return sitemap;
  }
}

Scheduling methods inherited from core ScheduledTask:

Method Description
.everyMinute() Run every minute
.everyFiveMinutes() Run every 5 minutes
.everyFifteenMinutes() Run every 15 minutes
.everyThirtyMinutes() Run every 30 minutes
.hourly() Run every hour
.daily() Run daily at midnight
.dailyAt(time) Run daily at a specific time (e.g. '03:00')
.weekly() Run weekly (Sunday at midnight)
.monthly() Run monthly (1st at midnight)
.cron(expression) Set a custom cron expression
.timezone(name) Interpret the schedule in an explicit timezone
.preventOverlap() Use the core distributed overlap lock
.outputPath(path) Set a custom output file path

The default schedule is 0 3 * * * (daily at 3 AM). The handle() method builds the sitemap and writes it to the configured output path.

Register instances with the application's core Scheduler; inherited execution reports failures through the normal task result and callbacks. Distributed locks or persistent scheduling require the corresponding core migrations. The sitemap plugin creates no database tables at runtime. Operator run-now controls should use the configured persistent scheduler request API rather than calling handle() from a route.


Full Working Example

// src/lib/plugins.ts
import SvelarSitemapPlugin from '@beeblock/svelar-sitemap/plugin';

export const sitemapPlugin = new SvelarSitemapPlugin({
  baseUrl: 'https://myapp.com',
  defaultChangeFreq: 'weekly',
  defaultPriority: 0.5,
  outputPath: 'static/sitemap.xml',
  maxUrlsPerSitemap: 50000,
});
// src/routes/sitemap.xml/+server.ts
import { SitemapController } from '@beeblock/svelar-sitemap/server';
import { Post } from '$lib/models/Post';
import { Product } from '$lib/models/Product';

// Reviewed public functional-RBAC exemption: published/public model URLs only.
export const GET = SitemapController.handle(async (sitemap) => {
  // Static pages
  sitemap
    .add('/', { changeFreq: 'daily', priority: 1.0 })
    .add('/about', { changeFreq: 'monthly', priority: 0.8 })
    .add('/contact', { changeFreq: 'monthly', priority: 0.5 })
    .add('/pricing', { changeFreq: 'weekly', priority: 0.9 });

  // Blog posts
  await sitemap.addModel(Post, {
    url: (post) => `/blog/${post.slug}`,
    lastmod: (post) => post.updated_at,
    changeFreq: 'weekly',
    priority: 0.7,
    images: (post) => post.featured_image
      ? [{ url: post.featured_image, title: post.title }]
      : [],
    query: (q) => q.where('published', true),
  });

  // Products
  await sitemap.addModel(Product, {
    url: (product) => `/products/${product.slug}`,
    lastmod: (product) => product.updated_at,
    changeFreq: 'daily',
    priority: 0.8,
    query: (q) => q.where('published', true), // use the model's public-visibility predicate
  });

  return sitemap;
});
// For large sites with multiple sitemaps
// src/routes/sitemap-index.xml/+server.ts
import { SitemapController } from '@beeblock/svelar-sitemap/server';
import { SitemapIndex } from '@beeblock/svelar-sitemap';

// Reviewed public functional-RBAC exemption: public child-sitemap references only.
export const GET = SitemapController.handleIndex(async () => {
  const index = SitemapIndex.create('https://myapp.com');
  index.addSitemap('/sitemap-pages.xml', new Date());
  index.addSitemap('/sitemap-posts.xml', new Date());
  index.addSitemap('/sitemap-products.xml', new Date());
  return index.toXml();
});
// Static generation via scheduled task
// src/lib/scheduler/GenerateSitemap.ts
import { SitemapTask } from '@beeblock/svelar-sitemap/server';
import { Post } from '$lib/models/Post';

export default class GenerateSitemap extends SitemapTask {
  schedule() {
    return this.dailyAt('03:00');
  }

  async buildSitemap(sitemap) {
    sitemap.add('/', { changeFreq: 'daily', priority: 1.0 });
    sitemap.add('/about', { priority: 0.8 });

    await sitemap.addModel(Post, {
      url: (p) => `/blog/${p.slug}`,
      lastmod: (p) => p.updated_at,
      query: (q) => q.where('published', true),
    });

    return sitemap;
  }
}
// Programmatic static generation (e.g., in a build script)
import { Sitemap } from '@beeblock/svelar-sitemap';

Sitemap.configure({
  baseUrl: 'https://myapp.com',
  outputPath: 'static/sitemap.xml',
});

const sitemap = Sitemap.create();
sitemap.add('/', { priority: 1.0 });
sitemap.add('/about');

await sitemap.writeToFile();
// Writes to static/sitemap.xml
Svelar © 2026 · MIT License