Sitemap-Generierung mit @unom/vite-sitemap-plugin

Wer eine Vite-basierte App mit TanStack Start betreibt und ordentliche SEO-Grundlagen braucht, stößt schnell auf ein unbefriedigend gelöstes Problem: sitemap.xml und robots.txt. Der Sitemap-Support von TanStack Start ist zum Zeitpunkt dieses Artikels noch sehr rudimentär – keine i18n-Unterstützung, keine feingranulare Kontrolle über changefreq oder priority, und das Ergebnis landet oft irgendwo, wo man es gar nicht haben will.

Also habe ich @unom/vite-sitemap-plugin geschrieben – ein schlankes Vite-Plugin, das beide Dateien zuverlässig zur Build-Zeit erzeugt, vollständiges hreflang-Handling für mehrsprachige Seiten mitbringt, und sich mit wenigen Zeilen konfigurieren lässt.

Das Paket ist nicht auf npmjs.org veröffentlicht, sondern über meine selbst gehostete Gitea-Registry verfügbar. Die Quellen liegen unter git.unom.io/unom/vite-sitemap-plugin.


Was das Plugin macht

  • Schreibt sitemap.xml im Sitemap-0.9-Format in das Output-Verzeichnis
  • Schreibt robots.txt mit konfigurierbaren Allow/Disallow-Regeln
  • Unterstützt i18n-Routing mit <xhtml:link rel="alternate" hreflang="…"> für jede Locale/Pfad-Kombination
  • Erlaubt pro Route individuelle changefreq-, priority- und lastmod-Werte
  • Lässt sich mit einer eigenen formatUrl-Funktion vollständig anpassen
  • Keine Runtime-Abhängigkeiten, reines Build-Zeit-Plugin

Voraussetzungen

  • Node.js ≥ 18 oder Bun
  • Vite ≥ 4 (peer dependency)

Schritt 1: Registry konfigurieren

Da das Paket nicht auf npmjs.org liegt, muss die Gitea-Registry zuerst als Quelle für den @unom-Scope eingetragen werden.

Mit npm:

npm config set @unom:registry https://git.unom.io/api/packages/unom/npm/

Mit pnpm:

pnpm config set @unom:registry https://git.unom.io/api/packages/unom/npm/

Oder direkt in der .npmrc im Projekt-Root:

@unom:registry=https://git.unom.io/api/packages/unom/npm/

Damit weiß der Paketmanager, dass alle Pakete unter @unom/* von dieser Registry bezogen werden sollen – alles andere läuft weiterhin über das Standard-Registry.


Schritt 2: Paket installieren

# Bun
bun add -D @unom/vite-sitemap-plugin

# npm
npm install -D @unom/vite-sitemap-plugin

# pnpm
pnpm add -D @unom/vite-sitemap-plugin


Schritt 3: Plugin in vite.config.ts einbinden

Das Plugin wird wie jedes andere Vite-Plugin in der plugins-Array eingetragen:


vite.config.ts
import { defineConfig } from "vite";
import { sitemapPlugin } from "@unom/vite-sitemap-plugin";

export default defineConfig({
  plugins: [
    sitemapPlugin({
      siteUrl: "https://example.com",
      routes: [
        "",
        "/about",
        "/contact",
        { path: "/blog", changefreq: "weekly", priority: 0.8 },
      ],
    }),
  ],
});


Nach dem nächsten Build (vite build) liegen sitemap.xml und robots.txt im Output-Verzeichnis (standardmäßig public/).


Schritt 4: robots.txt konfigurieren

Standardmäßig erzeugt das Plugin eine robots.txt ohne Einschränkungen. Über die robots-Option lässt sich das anpassen:


vite.config.ts
sitemapPlugin({
  siteUrl: "https://example.com",
  routes: ["", "/about"],
  robots: {
    userAgent: "*",        // Standard: "*"
    disallow: ["/admin/", "/api/"],
    allow: ["/api/public/"],
    extraLines: [
      "Crawl-delay: 10",
    ],
  },
})


Die generierte robots.txt sieht dann so aus:


robots.txt
User-agent: *
Disallow: /admin/
Disallow: /api/
Allow: /api/public/
Crawl-delay: 10
Sitemap: https://example.com/sitemap.xml


Wer gar keine robots.txt will, übergibt robots: false.


Schritt 5: Mehrsprachige Seiten mit hreflang

Für mehrsprachige Projekte ist das der Hauptgrund, warum dieses Plugin entstanden ist. Mit der locales-Option erzeugt das Plugin für jede Locale/Pfad-Kombination einen eigenen <url>-Eintrag inklusive aller <xhtml:link rel="alternate">-Verweise:

vite.config.ts
sitemapPlugin({
  siteUrl: "https://example.com",
  locales: ["de", "en", "fr"],
  defaultLocale: "de",
  routes: [
    "",
    "/ueber-uns",
    "/kontakt",
    { path: "/blog", changefreq: "weekly", priority: 0.9 },
  ],
})


Das erzeugt URLs im Format https://example.com/de/https://example.com/en/https://example.com/fr/ usw. Jeder <url>-Block enthält dann die vollständigen hreflang-Alternates plus ein x-default, das auf die defaultLocale zeigt.

Ausschnitt aus der resultierenden sitemap.xml:


sitemap.xml
<url>
  <loc>https://example.com/de/</loc>
  <lastmod>2026-05-12</lastmod>
  <xhtml:link rel="alternate" hreflang="de" href="https://example.com/de/"/>
  <xhtml:link rel="alternate" hreflang="en" href="https://example.com/en/"/>
  <xhtml:link rel="alternate" hreflang="fr" href="https://example.com/fr/"/>
  <xhtml:link rel="alternate" hreflang="x-default" href="https://example.com/de/"/>
</url>



Schritt 6: URL-Format anpassen

Standardmäßig baut das Plugin URLs nach dem Schema ${siteUrl}/${locale}${path}. Wenn das eigene Routing anders aussieht – z.B. de.example.com statt example.com/de/ oder Query-Parameter statt Pfad-Segmenten – kann das mit formatUrlvollständig überschrieben werden:


vite.config.ts
sitemapPlugin({
  siteUrl: "https://example.com",
  locales: ["de", "en"],
  defaultLocale: "de",
  routes: ["", "/about"],
  formatUrl: (locale, path) =>
    locale === "de"
      ? `https://example.com${path}`        // Deutsch ohne Locale-Prefix
      : `https://${locale}.example.com${path}`, // Subdomains für andere Locales
})




Alle Optionen im Überblick

OptionTypStandardBeschreibung

siteUrl

string

(Pflicht)

Basis-URL der Website

routes

Array<string | SitemapEntry>

(Pflicht)

Liste der zu indexierenden Pfade

locales

string[]

nicht gesetzt

Locale-Codes für i18n hreflang

defaultLocale

string

erste Locale

Locale für x-default hreflang

formatUrl

(locale, path) => string

${siteUrl}/${locale}${path}

Benutzerdefinierte URL-Formatierung

lastmod

string | Date

Build-Zeitpunkt

Globales lastmod für alle Einträge

outDir

string

"public"

Ausgabeverzeichnis

sitemapFilename

string

"sitemap.xml"

Dateiname der Sitemap

robotsFilename

string

"robots.txt"

Dateiname der robots.txt

robots

RobotsOptions | false

{}

robots.txt-Konfiguration

SitemapEntry akzeptiert: pathlastmodchangefreqpriority.

RobotsOptions akzeptiert: userAgentdisallowallowextraLines.


Vollständiges Beispiel

Ein realistisches Setup für eine deutschsprachige Hauptseite mit englischer Übersetzung, einem Blog und einem geschützten Admin-Bereich:


vite.config.ts
import { defineConfig } from "vite";
import { sitemapPlugin } from "@unom/vite-sitemap-plugin";

export default defineConfig({
  plugins: [
    sitemapPlugin({
      siteUrl: "https://meineprojekt.de",
      locales: ["de", "en"],
      defaultLocale: "de",
      routes: [
        { path: "",        changefreq: "daily",   priority: 1.0 },
        { path: "/about",  changefreq: "monthly", priority: 0.5 },
        { path: "/blog",   changefreq: "weekly",  priority: 0.8 },
        { path: "/contact",changefreq: "monthly", priority: 0.6 },
      ],
      robots: {
        disallow: ["/admin/", "/api/internal/"],
      },
    }),
  ],
});




Warum nicht einfach vite-plugin-sitemap?

Es gibt bereits einige Sitemap-Plugins für Vite. Die Probleme, auf die ich gestoßen bin:

  • Kein oder mangelhaftes hreflang-Handling für mehrsprachige Projekte
  • Keine Kontrolle über robots.txt im selben Plugin
  • TanStack Starts eigene Sitemap-Unterstützung ist noch sehr früh – keine Konfigurationsmöglichkeiten, kein i18n
  • Ich wollte ein Plugin, das ich vollständig verstehe und kontrolliere, ohne Runtime-Overhead

Das Ergebnis ist ein Plugin mit ~200 Zeilen TypeScript, keinen externen Abhängigkeiten, und genau den Features, die für meine Projekte gebraucht werden. MIT-lizenziert, Quellen offen.


Übersicht

Startseite

Social Media

instagram