Metadata + SEO cheatsheet.
Static metadata
// app/page.tsx
export const metadata = {
title: "Home",
description: "Welcome to my app",
};
Title templates
// app/layout.tsx
export const metadata = {
title: {
default: "MyApp",
template: "%s — MyApp", // child pages get "X — MyApp"
},
description: "Default description",
};
// app/posts/page.tsx
export const metadata = { title: "Posts" };
// Renders: "Posts — MyApp"
Dynamic metadata
// app/posts/[id]/page.tsx
export async function generateMetadata({ params }) {
const { id } = await params;
const post = await fetchPost(id);
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [post.image],
},
};
}
Open Graph
export const metadata = {
openGraph: {
title: "Title",
description: "Description",
url: "https://example.com/page",
siteName: "MyApp",
images: [
{ url: "https://example.com/og.png", width: 1200, height: 630, alt: "..." },
],
locale: "en_US",
type: "article",
publishedTime: "2026-01-01",
authors: ["A"],
tags: ["tag1"],
},
};
twitter: {
card: "summary_large_image",
title: "...",
description: "...",
images: ["https://example.com/og.png"],
creator: "@user",
},
Icons
export const metadata = {
icons: {
icon: "/icon.png",
apple: "/apple-icon.png",
shortcut: "/shortcut.png",
},
};
Or use file convention:
app/
├── favicon.ico
├── icon.png # any size
├── icon.tsx # dynamic icon
└── apple-icon.png
Dynamic icon
// app/icon.tsx
import { ImageResponse } from "next/og";
export const size = { width: 32, height: 32 };
export const contentType = "image/png";
export default function Icon() {
return new ImageResponse(
<div style={{ width: "100%", height: "100%", background: "black", color: "white", display: "flex", alignItems: "center", justifyContent: "center" }}>
M
</div>,
{ ...size },
);
}
OG image (dynamic)
// app/posts/[id]/opengraph-image.tsx
import { ImageResponse } from "next/og";
export const size = { width: 1200, height: 630 };
export const contentType = "image/png";
export default async function OG({ params }) {
const { id } = await params;
const post = await fetchPost(id);
return new ImageResponse(
<div style={{ width: "100%", height: "100%", display: "flex", flexDirection: "column", padding: 80, background: "white" }}>
<h1 style={{ fontSize: 64 }}>{post.title}</h1>
<p>{post.excerpt}</p>
</div>,
{ ...size },
);
}
Generated per post at request time.
sitemap.ts
// app/sitemap.ts
import type { MetadataRoute } from "next";
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await db.post.findMany();
return [
{
url: "https://example.com",
lastModified: new Date(),
changeFrequency: "weekly",
priority: 1,
},
...posts.map((p) => ({
url: `https://example.com/posts/${p.id}`,
lastModified: p.updatedAt,
changeFrequency: "monthly",
priority: 0.7,
})),
];
}
Available at /sitemap.xml.
robots.ts
// 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",
};
}
Canonical URL
export const metadata = {
alternates: {
canonical: "https://example.com/posts/123",
languages: {
"en-US": "https://example.com/en/posts/123",
"es-ES": "https://example.com/es/posts/123",
},
},
};
Structured data (JSON-LD)
export default async function Page({ params }) {
const post = await fetchPost(params.id);
const jsonLd = {
"@context": "https://schema.org",
"@type": "Article",
headline: post.title,
image: post.image,
datePublished: post.publishedAt,
author: { "@type": "Person", name: post.author },
};
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<Article post={post} />
</>
);
}
metadataBase
// app/layout.tsx
export const metadata = {
metadataBase: new URL("https://example.com"),
// Now relative URLs resolve against this
};
Without this, absolute URLs throw warnings.
viewport
export const viewport = {
themeColor: "#000",
colorScheme: "dark light",
width: "device-width",
initialScale: 1,
};
manifest.ts (PWA)
// app/manifest.ts
import type { MetadataRoute } from "next";
export default function manifest(): MetadataRoute.Manifest {
return {
name: "MyApp",
short_name: "MyApp",
description: "...",
start_url: "/",
display: "standalone",
background_color: "#fff",
theme_color: "#000",
icons: [
{ src: "/icon-192.png", sizes: "192x192", type: "image/png" },
{ src: "/icon-512.png", sizes: "512x512", type: "image/png" },
],
};
}
Common mistakes
- Missing
metadataBase→ relative URLs warnings. - Dynamic metadata without await on params (Next 15+).
- Same
titleeverywhere — kills search ranking. - OG image 404 → social previews break.
- Forgetting
sitemap→ poor crawl.
Read this next
If you want my SEO + OG patterns, they’re at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .