The App Router generates both from typed code — no more stale static XML:
// app/sitemap.ts
import type { MetadataRoute } from "next";
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await getPosts();
const postEntries = posts.map((post) => ({
url: `https://example.com/en/blog/${post.slug}`,
lastModified: post.updatedAt,
alternates: {
languages: {
pl: `https://example.com/pl/blog/${post.plSlug}`,
ru: `https://example.com/ru/blog/${post.ruSlug}`,
},
},
}));
return [
{ url: "https://example.com", lastModified: new Date(), priority: 1 },
...postEntries,
];
}
// app/robots.ts
import type { MetadataRoute } from "next";
export default function robots(): MetadataRoute.Robots {
return {
rules: [{ userAgent: "*", allow: "/", disallow: ["/api/", "/admin/"] }],
sitemap: "https://example.com/sitemap.xml",
};
}
Details that matter:
lastModifiedshould be the real content update date, notnew Date()on every build — lying about freshness trains Google to ignore the field.- Over 50,000 URLs? Use
generateSitemaps()to split into multiple files. - Don't block CSS/JS in robots.txt — Google needs them to render the page.
robots.txtdisallow doesn't remove pages from the index — it only stops crawling. To keep a page out of results, usenoindexvia metadata (robots: { index: false }) and don't block it in robots.txt (blocked pages can't be read, so the noindex is never seen).
Checklist:
-
sitemap.tsincludes only canonical, indexable, 200-status URLs -
lastModifiedreflects real edits - Sitemap submitted in Google Search Console and Bing Webmaster Tools
- noindex vs robots.txt used correctly (they solve different problems)