sitemap.ts and robots.ts

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.

app/sitemap.tsTypeScript
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:

Output of 80
<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:

Output of 80
User-Agent: *
Allow: /
Disallow: /admin/
Disallow: /api/
User-Agent: GPTBot
Disallow: /
Host: https://example.com
Sitemap: https://example.com/sitemap.xml