Titles that sell the click
SEO, search-engine optimization, was introduced in Unit 1 as the practice of making pages easy for search engines to read and rank. For a content site it decides whether anyone finds you at all.
It starts embarrassingly simple: every page needs a good title and meta description. In Next.js you never touch the document head directly. You export a metadata object instead:
// app/about/page.js export const metadata = { title: "About us | Acme", description: "Meet the team building Acme.", }; export default function AboutPage() { /* … */ }
Next.js renders the tags into the HTML head on the server. Because Unit 1 established that the HTML is real, rather than injected by client JavaScript, crawlers actually see it. This is a core reason content sites choose server rendering.
Dynamic pages need dynamic titles
/blog/[slug] cannot hardcode one title for every post. Export a generateMetadata function instead:
// app/blog/[slug]/page.js export async function generateMetadata({ params }) { const { slug } = await params; const post = await getPost(slug); return { title: post.title, description: post.summary }; }
Decode it:
- Same
await paramsrule as the page itself. - It may fetch, and thanks to the request memoization from Unit 4, fetching the same post here and in the page costs one request, not two.
- Return shape is the same metadata object as the static version.
Title templates: brand every page once
Real sites suffix every title with the brand, producing "Pricing | Acme" and "About us | Acme". Typing "| Acme" on every page invites drift: someone forgets it, someone else writes - Acme instead. Metadata composes down the tree the way layouts do, so the layout can set the pattern once:
// app/layout.js export const metadata = { title: { template: "%s | Acme", // %s is replaced by the child page's title default: "Acme", // used when a page sets no title }, };
Now a page exporting title: "Pricing" renders a document title of Pricing | Acme, and a page with no title of its own falls back to Acme. One definition, every page consistent. It is the same reasoning that made layouts better than copy-pasting a nav bar into thirty files.
How a title template resolves
Say the root layout sets title: { template: "%s | Acme", default: "Acme" } and the pricing page exports metadata = { title: "Pricing" }. On /pricing the browser tab reads Pricing | Acme.
The child page supplies the value for the %s slot in the layout's template, and Next.js performs the substitution. The default value, "Acme", only appears on pages that set no title at all, which makes it a safety net rather than a fallback you rely on.
Defining the pattern once in the layout keeps every page's branding consistent. It is the metadata version of the shared-shell idea from Unit 2: put the repeated part in the layout and let each page fill in only what is unique to it.
Metadata exports versus setting document.title in an effect
Both approaches change the tab text, but only one of them helps search engines and link previews.
A metadata export is rendered into the server HTML, so the tags are present in the response itself. Anything that reads raw HTML sees them, including crawlers and the social-preview bots behind chat apps and social networks.
A useEffect that assigns document.title only runs in the browser, after hydration. A human user eventually sees the right tab text, but any consumer that reads the HTML without executing JavaScript misses it entirely, which means your search snippet and your shared-link preview fall back to whatever the raw HTML said.
This is the SEO payoff of server rendering, stated concretely. Metadata that exists before JavaScript runs is metadata that everyone can read.
Giving every post its own title
Suppose every post at /blog/[slug] currently shows the site-wide title "My Blog" in search results. The function to export from app/blog/[slug]/page.js is generateMetadata.
It receives params, awaited exactly like the page component does, can fetch the post, and returns a per-page metadata object such as { title: post.title }.
Why it works out that way
- A static
metadataobject is evaluated once and cannot vary by slug, so it can only ever produce one title for every post. generateMetadatais the async sibling of themetadataexport, designed for precisely this case, and it may await data before deciding what the title should be.- Thanks to the request memoization from Unit 4, fetching the same post in both
generateMetadataand the page costs one request rather than two.