React
Generated sitemap.ts and robots.ts
Two TypeScript files produce /sitemap.xml and /robots.txt from the same code as the routes. Types, hreflang, the 50,000 URL cap, the static file.
Key points
- app/sitemap.ts and app/robots.ts are routes: they produce /sitemap.xml and /robots.txt.
- The MetadataRoute type enforces shape, not valid URLs.
- Beyond 50,000 URLs per file you need generateSitemaps.
- A public/robots.txt sits badly next to app/robots.ts: keep one source.
Two TypeScript files generate the artefacts crawlers read: app/sitemap.ts produces /sitemap.xml and app/robots.ts produces /robots.txt. They are routes like any other, with the same caching limits.
The sitemap
// app/sitemap.ts
import type { MetadataRoute } from "next";
const BASE = "https://ui.eneawork.it";
const PAGES = ["", "/pricing", "/blog"];
export default function sitemap(): MetadataRoute.Sitemap {
return PAGES.map((path) => ({
url: `${BASE}${path}`,
lastModified: new Date(),
changeFrequency: "weekly" as const,
priority: path === "" ? 1 : 0.7,
alternates: {
languages: { it: `${BASE}/it${path}`, en: `${BASE}/en${path}` },
},
}));
}
The MetadataRoute.Sitemap type accepts url, lastModified, changeFrequency, priority and alternates.languages. The alternates field writes the xhtml:link entries for hreflang: without it the sitemap stays valid but says nothing about translations.
The robots file
// app/robots.ts
import type { MetadataRoute } from "next";
export default function robots(): MetadataRoute.Robots {
return {
rules: [{ userAgent: "*", allow: "/", disallow: ["/api/", "/admin/"] }],
sitemap: "https://ui.eneawork.it/sitemap.xml",
host: "https://ui.eneawork.it",
};
}
The 50,000 cap
One sitemap file holds at most 50,000 URLs and 50 MB uncompressed. Past that threshold you use generateSitemaps, which returns an array of ids and leaves one file per id to you.
// app/sitemap.ts
export async function generateSitemaps() {
return [{ id: 0 }, { id: 1 }, { id: 2 }];
}
export default async function sitemap({ id }: { id: number }) {
const rows = await loadUrls();
return rows.slice(id * 50000, id * 50000 + 50000);
}
Static or dynamic
| File | Route | When it is static |
|---|---|---|
app/sitemap.ts | /sitemap.xml | If it reads no cookie, header or query |
app/robots.ts | /robots.txt | Almost always |
public/sitemap.xml | /sitemap.xml | Always, but it must go if app/sitemap.ts exists |
The routes that end up in a sitemap are pages: blog blocks, pagination and breadcrumbs cover the archive without inventing URLs.
Index or single file is in sitemap index vs a single file; submission to Google in Search Console and indexing; internal links in an internal linking strategy. Comparison: Next.js vs Vite for a marketing site.
curl -s http://localhost:3000/sitemap.xml
Known limitation: a sitemap declares URLs, it does not check them. A page.tsx removed without dropping its entry produces a 404 that Search Console reports; the check belongs in CI, not in your eyes.
Frequently asked questions
Are sitemap.ts and robots.ts static or dynamic?
It depends on the code. If they read no cookie, header or query, Next.js generates them once at build; if they read dynamic data they run on every request.
Can I go past 50,000 URLs?
Yes, with generateSitemaps: return an array of ids and Next.js exposes one file per id, each below the cap.
Where do I declare languages in the sitemap?
In the alternates.languages field of each entry. It becomes an xhtml:link block with an hreflang for every language.
public/robots.txt or app/robots.ts?
app/robots.ts, if you want the values to come from code. The static file is fine only when it never changes, and it must not live beside the route.
Does a sitemap help ranking?
No, it helps discovery. It gets new or deep URLs found; position depends on content, links and page quality.