Part 5 of 22

sitemap.ts and robots.ts: file conventions instead of hand-written XML

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:

  • lastModified should be the real content update date, not new 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.txt disallow doesn't remove pages from the index — it only stops crawling. To keep a page out of results, use noindex via 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.ts includes only canonical, indexable, 200-status URLs
  • lastModified reflects real edits
  • Sitemap submitted in Google Search Console and Bing Webmaster Tools
  • noindex vs robots.txt used correctly (they solve different problems)

Want this handled for you?

I audit and fix technical SEO on Next.js sites — indexing, performance, hreflang and AI visibility.

Technical SEO service