app/sitemap.ts and app/robots.ts replace the static files you would otherwise hand-maintain. Both are route handlers that can read your database, and both prerender at build time unless they touch a request-time API.
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const books = await allBooks();
return [
{ url: "https://example.com/", changeFrequency: "daily", priority: 1 },
...books.map((b) => ({ url: `https://example.com/books/${b.slug}`,
lastModified: new Date(b.published),
changeFrequency: "weekly" as const, priority: 0.8 })),
];
}The entries are objects; the serializing is done for you, one <url> block per entry:
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <url> <loc>https://example.com/books/the-pragmatic-programmer</loc> <lastmod>2019-09-13T00:00:00.000Z</lastmod> <changefreq>weekly</changefreq> <priority>0.8</priority> </url> </urlset>
URLs here are fully qualified, so build them from the variable that also feeds metadataBase. An entry also takes alternates.languages for hreflang pairs and images or videos arrays. Google caps a sitemap at 50,000 URLs; past that, export generateSitemaps, which serves shards at /books/sitemap/0.xml.
robots.ts has a smaller shape: return MetadataRoute.Robots, a rules array of { userAgent, allow, disallow } objects plus sitemap and host. One rule for * disallowing /admin/ and /api/, a second blocking GPTBot, and those two keys produce the whole file:
User-Agent: * Allow: / Disallow: /admin/ Disallow: /api/ User-Agent: GPTBot Disallow: / Host: https://example.com Sitemap: https://example.com/sitemap.xml