SyntaxStudy
Sign Up
Next.js Sitemap, Robots.txt, and Open Graph Images
Next.js Beginner 1 min read

Sitemap, Robots.txt, and Open Graph Images

Next.js provides file-based conventions for generating a sitemap and robots.txt. Creating app/sitemap.ts and exporting a default function that returns an array of sitemap entries automatically serves a sitemap.xml at /sitemap.xml. The function can be async, so you can fetch URLs from your CMS or database. Similarly, app/robots.ts exports a function returning a Robots object that is served at /robots.txt. Open Graph images are the preview images shown when your pages are shared on social media. Next.js supports generating them dynamically via the ImageResponse API from next/og. Create an app/opengraph-image.tsx file in any route segment and export a default function that returns an ImageResponse. The function renders a React component to a PNG image using Satori under the hood — the component can use flexbox and basic CSS, though some advanced CSS features are not supported. The ImageResponse constructor accepts a JSX element and options including width, height, and a fonts array for custom fonts. Generating OG images dynamically lets you include the page title, author, and branding in each preview image without pre-generating thousands of images at build time. The result is cached by Next.js and can be revalidated using the same revalidate export.
Example
// app/sitemap.ts
import type { MetadataRoute } from 'next';

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const baseUrl = 'https://acme.com';

  // Fetch dynamic routes from your data source
  const posts = await fetch(`${baseUrl}/api/posts?fields=slug,updatedAt`)
    .then((r) => r.json());

  const postEntries: MetadataRoute.Sitemap = posts.map(
    (post: { slug: string; updatedAt: string }) => ({
      url: `${baseUrl}/blog/${post.slug}`,
      lastModified: new Date(post.updatedAt),
      changeFrequency: 'weekly',
      priority: 0.8,
    }),
  );

  return [
    { url: baseUrl, lastModified: new Date(), changeFrequency: 'daily', priority: 1 },
    { url: `${baseUrl}/about`, lastModified: new Date(), changeFrequency: 'monthly', priority: 0.5 },
    ...postEntries,
  ];
}

// app/robots.ts
import type { MetadataRoute } from 'next';

export default function robots(): MetadataRoute.Robots {
  return {
    rules: [
      { userAgent: '*', allow: '/', disallow: ['/dashboard/', '/api/'] },
    ],
    sitemap: 'https://acme.com/sitemap.xml',
  };
}

// app/blog/[slug]/opengraph-image.tsx — dynamic OG image
import { ImageResponse } from 'next/og';

export const runtime = 'edge';
export const size = { width: 1200, height: 630 };
export const contentType = 'image/png';

export default async function OgImage({ params }: { params: { slug: string } }) {
  const post = await fetch(`https://api.example.com/posts/${params.slug}`)
    .then((r) => r.json());

  return new ImageResponse(
    <div style={{ display: 'flex', background: '#0f172a', width: '100%', height: '100%', padding: 60 }}>
      <h1 style={{ color: 'white', fontSize: 60 }}>{post.title}</h1>
    </div>,
    size,
  );
}