Simple sitemaps in tanstack-start
Sitemap generation with @unom/vite-sitemap-plugin
If you're running a Vite-based app with TanStack Start and want a solid SEO foundation, you quickly hit two perennials: sitemap.xml and robots.txt. TanStack Start's built-in sitemap support is still very rudimentary at the time of writing — no changefreq or priority, and the result often lands somewhere you don't actually want it.
So I built @unom/vite-sitemap-plugin — a small Vite plugin that reliably produces both files at build time, is fully configurable, and supports i18n via hreflang from the start.
The package isn't published on npmjs.org — it's available through my self-hosted Gitea registry at git.unom.io/unom/vite-sitemap-plugin.
What the plugin does
- Writes
sitemap.xmlin Sitemap 0.9 format to the output directory - Writes
robots.txtwith configurableAllow/Disallowrules - Supports i18n routing with
<xhtml:link rel="alternate" hreflang="…">for every locale/path combination - Allows per-route
changefreq,priorityandlastmodvalues - Can be fully customised with a custom
formatUrlfunction - No runtime dependencies — pure build-time plugin
Requirements
- Node.js ≥ 18 or Bun
- Vite ≥ 4 (peer dependency)
Step 1: Configure the registry
Since the package isn't on npmjs.org, the Gitea registry needs to be set as the source for the @unom scope.
With npm:
npm config set @unom:registry https://git.unom.io/api/packages/unom/npm/
With pnpm:
pnpm config set @unom:registry https://git.unom.io/api/packages/unom/npm/
Or directly in the .npmrc at the project root:
@unom:registry=https://git.unom.io/api/packages/unom/npm/
That way the package manager knows that all packages under @unom/* should be sourced from this registry — everything else still resolves through the default npmjs.org registry.
Step 2: Install the package
bun
bun add -D @unom/vite-sitemap-plugin
npm
npm install -D @unom/vite-sitemap-plugin
pnpm
pnpm add -D @unom/vite-sitemap-plugin
Step 3: Wire the plugin into vite.config.ts
The plugin gets added to the plugins array like any other Vite plugin:
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 },
],
}),
],
});After the next build (vite build) you'll find sitemap.xml and robots.txt in the output directory (defaults to public/).
Step 4: robots.txt configuration
By default the plugin produces a robots.txt with no restrictions. The robots option lets you customise that:
sitemapPlugin({
siteUrl: "https://example.com",
routes: ["", "/about"],
robots: {
userAgent: "*", // Standard: "*"
disallow: ["/admin/", "/api/"],
allow: ["/api/public/"],
extraLines: [
"Crawl-delay: 10",
],
},
})The generated robots.txt then looks like this:
User-agent: *
Disallow: /admin/
Disallow: /api/
Allow: /api/public/
Crawl-delay: 10
Sitemap: https://example.com/sitemap.xmlIf you don't want a robots.txt at all, pass robots: false.
Step 5: Multilingual sites with hreflang
For multilingual projects this is the main reason the plugin exists. With the locales option the plugin emits a separate <url> entry for every locale/path combination, including all <xhtml:link rel="alternate"> references:
sitemapPlugin({
siteUrl: "https://example.com",
locales: ["de", "en", "fr"],
defaultLocale: "de",
routes: [
"",
"/ueber-uns",
"/kontakt",
{ path: "/blog", changefreq: "weekly", priority: 0.9 },
],
})That produces URLs in the form https://example.com/de/, https://example.com/en/, https://example.com/fr/ etc. Each <url> block then carries the full hreflang alternates plus an x-default pointing to the defaultLocale.
Excerpt from the resulting 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>Step 6: Customising the URL format
By default the plugin builds URLs following the pattern ${siteUrl}/${locale}${path}. If your own routing looks different — e.g. de.example.com instead of example.com/de/ or query parameters instead of path segments — that can be fully overridden via formatUrl:
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
})All options at a glance
Option | Type | Default | Description |
|---|---|---|---|
|
| (required) | Base URL of the site |
|
| (required) | List of paths to index |
|
| not set | Locale codes for i18n hreflang |
|
| first locale | Locale for the |
|
|
| Custom URL formatter |
|
| build time | Global |
|
|
| Output directory |
|
|
| Sitemap filename |
|
|
| robots.txt filename |
|
|
| robots.txt configuration |
SitemapEntry accepts: path, lastmod, changefreq, priority.
RobotsOptions accepts: userAgent, disallow, allow, extraLines.
Full example
A realistic setup for a German-language primary site with English translation, a blog and careers pages:
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/"],
},
}),
],
});Why not just vite-plugin-sitemap?
There are a few sitemap plugins for Vite already. Issues I ran into:
- No or weak hreflang handling for multilingual projects
- No control over
robots.txtfrom the same plugin - TanStack Start's own sitemap support is still very early — no configuration knobs, no hreflang
- I wanted a plugin I fully understand and control, with no runtime overhead
The result is a plugin in ~200 lines of TypeScript, with no external dependencies, and exactly the options I needed.
Links
- Repository: git.unom.io/unom/vite-sitemap-plugin
- Registry:
https://git.unom.io/api/packages/unom/npm/ - Package name:
@unom/vite-sitemap-plugin
