sitemap.ts
Generate /sitemap.xml from code: app/sitemap.ts returns the list of URLs and GioJS writes the XML.
app/sitemap.ts
import type { MetadataRoute } from '@gio.js/core';
import { listPosts } from '../lib/posts';
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await listPosts();
return [
{ url: '/', changeFrequency: 'weekly', priority: 1 },
...posts.map((post) => ({ url: `/blog/${post.slug}`, lastModified: post.updatedAt })),
];
}Reference
File name and location
app/sitemap.ts or app/sitemap.js, at the root of app/ only: a sitemap.ts in a subfolder is an ordinary module. It answers /sitemap.xml.
Exports
| Field | Type | Default | Description |
|---|---|---|---|
default (required) | Sitemap | (() => Sitemap | Promise<Sitemap>) | - | The entries, or a function returning them. The function gets no arguments: the sitemap is the same for every visitor. |
revalidate | number | false | 3600 | Seconds the output is cached. false keeps it until the next deploy (one year), 0 (or a negative number) generates it on every request. |
Sitemap entries
| Field | Type | Default | Description |
|---|---|---|---|
url (required) | string | - | The page's URL, absolute or relative to GIO_SITE_URL. Becomes <loc>. |
lastModified | string | Date | - | Becomes <lastmod>; a Date is written as an ISO timestamp, a string as given. |
changeFrequency | 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never' | - | Becomes <changefreq>. |
priority | number | - | From 0 to 1. Becomes <priority>. |
alternates.languages | Record<string, string> | - | Translations of the URL, by language: one <xhtml:link rel="alternate" hreflang> each. |
The types are MetadataRoute.Sitemap (an array of SitemapEntry) and ChangeFrequency, from @gio.js/core.
Response
200,Content-Type: application/xml; charset=utf-8, aurlsetin the sitemaps.org namespace (with thexhtmlnamespace when an entry has alternates). Text is XML-escaped, and characters XML forbids are dropped.- Only
GETandHEAD: other methods get405withAllow: GET, HEAD. - The server's page cache keeps the output for
revalidateseconds (X-Gio-Cacheshows hits) and answers with anETag. Unlike HTML pages it gets no automaticCache-Control; add a[[headers]]rule if a CDN should cache it. - An invalid entry (not an object, no
url, an unknownchangeFrequency, apriorityoutside 0-1, an invalidDate) or a function that throws answers500Internal Server Error (ref <digest>), never cached, with the reason in the log. A sitemap search engines would reject is never served.
Examples
Output
app/sitemap.ts
import type { MetadataRoute } from '@gio.js/core';
export default function sitemap(): MetadataRoute.Sitemap {
return [
{ url: '/', lastModified: new Date('2026-10-01T00:00:00Z'), changeFrequency: 'weekly', priority: 1 },
{ url: '/blog/hello', alternates: { languages: { de: '/de/blog/hello' } } },
];
}With GIO_SITE_URL=https://example.com, /sitemap.xml is:
text
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">
<url>
<loc>https://example.com/</loc>
<lastmod>2026-10-01T00:00:00.000Z</lastmod>
<changefreq>weekly</changefreq>
<priority>1</priority>
</url>
<url>
<loc>https://example.com/blog/hello</loc>
<xhtml:link rel="alternate" hreflang="de" href="https://example.com/de/blog/hello"/>
</url>
</urlset>Refresh once a day
app/sitemap.ts
import type { MetadataRoute } from '@gio.js/core';
import { listProducts } from '../lib/catalog';
export const revalidate = 86400;
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const products = await listProducts();
return products.map((product) => ({ url: `/products/${product.id}`, lastModified: product.updatedAt }));
}Good to know
- Relative URLs are resolved against
GIO_SITE_URL(notmetadataBase, which belongs to page metadata). Without it they stay relative and the server warns once per URL: crawlers need absolute URLs. - A
public/sitemap.xmlwins: it is served before the request reaches the worker, the module never runs, and startup warns. A page orroute.tsat/sitemap.xmlstops startup. gio exportwritesout/sitemap.xmlfrom it. Without anapp/sitemap.ts, the export generates one listing every exported page, but only whenGIO_SITE_URLis set.- One file serves one sitemap: there is no
generateSitemapsfor sitemap indexes yet. A sitemap holds at most 50,000 URLs, so split larger sites by hand withroute.tsfiles. - In development a change to the file restarts the worker and clears the cache.
Related
- Metadata & SEO - the guide.
- robots.ts, manifest.ts, public/
- Environment variables (
GIO_SITE_URL)
Version history
| Version | Changes |
|---|---|
v0.1.0-beta.8 | Introduced: app/sitemap.ts serves /sitemap.xml, on the server, in standalone builds and in gio export. |
v0.1.0-beta.3 | gio export generates a sitemap.xml of the exported pages when GIO_SITE_URL is set. |