Server rendering gives Next.js apps a head start with search engines, but real SEO requires correct metadata: titles, descriptions, social cards, sitemaps, and structured data. The App Router provides a dedicated Metadata API that generates all of it from your components. This lesson covers static metadata, dynamic metadata, and the special SEO files Next.js can build for you.
Any layout.js or page.js can export a metadata object:
// app/about/page.js
export const metadata = {
title: 'About Us | CodeYourCraft',
description:
'Learn about the CodeYourCraft mission: free, practical web development tutorials.',
};
export default function AboutPage() {
return <h1>About Us</h1>;
}Next.js renders these into head tags on the server, so crawlers see them without executing JavaScript. Metadata merges down the tree: a layout can set defaults and a page can override them.
Avoid repeating your site name in every page title by defining a template in the root layout:
// app/layout.js
export const metadata = {
title: {
template: '%s | CodeYourCraft',
default: 'CodeYourCraft - Learn Web Development',
},
description: 'Free coding tutorials for modern web developers.',
};Now a page exporting title: 'Next.js Course' renders as 'Next.js Course | CodeYourCraft' automatically.
For dynamic routes, metadata depends on data. Export an async generateMetadata function:
// app/blog/[slug]/page.js
export async function generateMetadata({ params }) {
const { slug } = await params;
const post = await getPost(slug);
if (!post) return { title: 'Post Not Found' };
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [{ url: post.coverImage }],
type: 'article',
},
};
}Fetch calls here are deduplicated with the page's own fetches, so requesting the post twice costs one network call. The openGraph block controls how links look when shared on social platforms; add a twitter block with card: 'summary_large_image' for the best rendering on X.
Instead of hand-maintaining XML, generate it. A sitemap.js file at the app root produces /sitemap.xml:
// app/sitemap.js
import { getAllPosts } from '@/lib/posts';
export default async function sitemap() {
const posts = await getAllPosts();
const postUrls = posts.map((p) => ({
url: 'https://example.com/blog/' + p.slug,
lastModified: p.updatedAt,
}));
return [
{ url: 'https://example.com', lastModified: new Date() },
...postUrls,
];
}Similarly, app/robots.js generates robots.txt, and opengraph-image.js can even generate social card images with code.
Rich results (star ratings, article cards) come from structured data. Render a JSON-LD script in your page:
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'Article',
headline: post.title,
datePublished: post.publishedAt,
};
<script type='application/ld+json'
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />Metadata cannot rescue a slow or confusing site. The rest of this course already covered the other pillars: server rendering for crawlable HTML, notFound() for correct 404 status codes, semantic headings, descriptive link text, and the image and font optimization from Lesson 11 that keeps Core Web Vitals green.
Next up: the final lesson - building for production, deploying your app, and a checklist of best practices.