Skip to content

Metadata & SEO

Think of metadata as a product label on a package:

  • Title = The product name (what shows up in search results)
  • Description = What’s inside (the summary under the title)
  • Open Graph = The social media preview (what shows when you share on Twitter/Facebook)
  • Keywords = Search terms (less important now, but still used)

Export a metadata object from any page.tsx or layout.tsx:

import type { Metadata } from "next";
export const metadata: Metadata = {
title: "About Us | MyApp",
description: "Learn about our mission, team, and values.",
keywords: ["about", "company", "team"],
};

For pages with dynamic content (blog posts, products, user profiles):

import type { Metadata } from "next";
interface Props {
params: Promise<{ slug: string }>;
}
// Fetch data and generate metadata dynamically
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params;
const post = await fetch(`https://api.example.com/posts/${slug}`)
.then((r) => r.json());
return {
title: `${post.title} | My Blog`,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [{ url: post.coverImage }],
},
};
}
export default async function BlogPost({ params }: Props) {
const { slug } = await params;
const post = await fetch(`https://api.example.com/posts/${slug}`)
.then((r) => r.json());
return <article>{post.content}</article>;
}

FieldPurposeExample
titlePage title (shows in browser tab + search results)`“Blog
descriptionSearch result snippet"Read our latest articles..."
openGraphSocial media preview (Facebook, Twitter)Title, description, image
robotsControl indexing{ index: true, follow: true }
alternatesCanonical URL + language variants{ canonical: "/blog/post" }
export const metadata: Metadata = {
openGraph: {
title: "Amazing Blog Post",
description: "You won't believe this...",
url: "https://myapp.com/blog/post",
siteName: "MyApp",
images: [
{
url: "https://myapp.com/og-image.png",
width: 1200,
height: 630, // Standard OG image size
},
],
locale: "en_US",
type: "website",
},
};

Set a template in the root layout — every page’s title gets wrapped automatically:

app/layout.tsx
export const metadata: Metadata = {
title: {
template: "%s | MyApp", // %s is the page title
default: "MyApp", // Fallback when no page title is set
},
};
// In any page:
export const metadata = { title: "Blog" };
// → Renders as: "Blog | MyApp"

app/sitemap.ts
import type { MetadataRoute } from "next";
export default function sitemap(): MetadataRoute.Sitemap {
return [
{
url: "https://myapp.com",
lastModified: new Date(),
changeFrequency: "yearly",
priority: 1,
},
{
url: "https://myapp.com/blog",
lastModified: new Date(),
changeFrequency: "weekly",
priority: 0.8,
},
];
}
app/robots.ts
import type { MetadataRoute } from "next";
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: "*",
allow: "/",
disallow: ["/admin/", "/api/"],
},
sitemap: "https://myapp.com/sitemap.xml",
};
}

  • Static metadata = export a metadata object (for simple, unchanging pages)
  • Dynamic metadata = use generateMetadata (for blog posts, products, user profiles)
  • Open Graph = controls how your page looks when shared on social media (set title, description, image)
  • Title templates = set a pattern in layout ("%s | MyApp") and each page just sets its own title
  • Sitemap + robots = help search engines find and index your pages correctly