Lesson 20 of 81
A lesson's <title> tag, its description, its social share image — none of it should be hand-written into a <head> tag, because the App Router already resolves it for you from every layout down to the page, merging as it goes. The part that actually trips people up is what 'merging' means once nested segments disagree.
12 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
Question 1 of 3
A root layout sets `openGraph: { title: 'Acme', description: 'Acme is a...' }`. A child page sets `openGraph: { title: 'Blog' }` and nothing else. What does the child page's final resolved openGraph metadata look like?
Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
Next.js resolves <head> metadata (title, description, Open Graph tags, and more) from two possible exports on a layout.tsx or page.tsx:
export const metadata = {...} — a plain object, for metadata that's the same on every render. Use this whenever the values don't depend on anything request-specific.export async function generateMetadata() {...} — for metadata that depends on route params or fetched data — a blog post's title coming from its own content, a product page's description coming from a database row. You can't know these ahead of time from a static object, so you compute them.A route segment can export one or the other, never both — mixing metadata and generateMetadata in the same file is a real conflict, not just redundant.
Metadata resolution walks the tree from the root layout down to the page, and every segment along the way can contribute. The results are merged, not replaced wholesale — but "merged" specifically means a shallow merge at the top-level key: if a child segment defines its own openGraph object, that whole openGraph object replaces the parent's, field-for-field losses included, rather than deep-merging individual nested fields.
// app/learn/[track]/[lesson]/page.tsx
import type { Metadata } from "next";
export async function generateMetadata({
params,
}: {
params: Promise<{ track: string; lesson: string }>;
}): Promise<Metadata> {
const { track, lesson } = await params;
const data = getLesson(track, lesson);
if (!data) return {};
const { title, summary: description } = data.frontmatter;
const url = `/learn/${track}/${lesson}`;
return {
title,
description,
alternates: { canonical: url },
openGraph: { type: "article", url, title, description },
twitter: { card: "summary_large_image", title, description },
};
}
This is this app's real lesson-page metadata. It has to be generateMetadata, not a static metadata object, because title and description come from params — there's no way to know which lesson's frontmatter to read without seeing the request. Note the returned openGraph.url is relative (/learn/${track}/${lesson}), not a full https://... URL — that only resolves correctly because the root app/layout.tsx sets metadataBase: new URL(SITE_URL), which every URL-based metadata field in every child segment composes against to become a fully qualified URL.
openGraph: { title, description } and a child page sets openGraph: { title } alone, the child's openGraph entirely replaces the parent's — the inherited description is gone, not preserved. This is the single most common source of "why did my OG description disappear" bugs.openGraph entirely and you inherit the parent's whole object; define any part of openGraph and you own the whole object now.title.template only applies to child segments, never to the segment that defines it — and it has no effect at all unless a title.default is also set, since a template needs something to prefix or suffix when a child doesn't provide its own title.metadata and generateMetadata from the same file. Reach for generateMetadata the moment any value depends on params, searchParams, or a fetch — not "just in case," since the static form is preferred whenever it's sufficient.generateMetadata and metadata are Server-Component-only exports. There's no client-side equivalent; metadata has to be resolved before the page renders so it can land in the initial HTML."A blog's homepage has a nice Open Graph description, but every individual post page shows a blank one" is a debugging scenario that maps directly onto the shallow-merge behavior — a candidate who reaches for "re-spread the parent's openGraph fields in the child" understands the actual merge rule, not just that metadata "cascades" in some vague sense.