The App Router replaced next/head with the typed Metadata API. Two ways to use it — static export for fixed pages, generateMetadata for dynamic ones:
// app/layout.tsx — site-wide defaults with a title template
import type { Metadata } from "next";
export const metadata: Metadata = {
metadataBase: new URL("https://example.com"),
title: {
template: "%s | Acme",
default: "Acme — Web Development Studio",
},
description: "Default description used when a page doesn't set its own.",
};
// app/blog/[slug]/page.tsx — dynamic metadata per post
export async function generateMetadata({ params }): Promise<Metadata> {
const post = await getPost(params.slug);
if (!post) return { title: "Post not found" };
return {
title: post.title, // becomes "Post title | Acme" via the template
description: post.excerpt,
};
}
Practical rules that actually move rankings:
- Title: ~50–60 characters, primary keyword near the beginning, unique per page. Google rewrites bad titles — don't give it a reason.
- Description: ~150–160 characters. It doesn't affect ranking directly, but it is your ad copy in the search results and drives CTR.
metadataBaseis mandatory — without it, relative OG image and canonical URLs break in production.- Deduplicate the fetch between
generateMetadataand the page component with Reactcache()— otherwise you hit your CMS twice per request.
import { cache } from "react";
export const getPost = cache(async (slug: string) => {
return fetchPostFromCMS(slug);
});
Checklist:
-
metadataBaseset in the root layout - Title template defined once, pages set only their own part
- Every page has a unique title and description
- CMS-driven pages use
generateMetadatawith acache()-wrapped fetch - Fallback metadata for 404 / missing-content states