generateMetadata vs static metadata, generateViewport(), dynamic OG images with ImageResponse, generateImageMetadata() for multiple images, generateSitemaps() for large catalogs, and draftMode() for CMS preview.
P-8 — SEO, Metadata, Open Graph, and Sitemaps
Who this is for: Engineers who know metadata exists but treat it as an afterthought — copy-pasted title tags, missing OG images, sitemaps that were set up once and never maintained. This module covers the full metadata system in Next.js 15, including the parts most tutorials skip:
generateViewport, dynamic OG images,generateImageMetadata, and production-safe sitemap splitting.
Why Metadata Matters More in Next.js Than in a SPA
If you built with Create React App or Vite, you likely added something like react-helmet and called it a day. The problem is that when a crawler — Googlebot, the LinkedIn link previewer, the Slack unfurl bot — fetches your page, it runs a quick fetch and reads the HTML. SPAs send a near-empty HTML shell; the metadata is injected by JavaScript after the page loads. Crawlers often do not execute JavaScript, or execute it badly, or time out before it runs.
Next.js with the App Router solves this at the architecture level. Server Components render to actual HTML before the response leaves your server. The <title>, <meta>, and <link> tags are in the initial HTML payload, not injected by a hydration script. Googlebot, Twitter's card validator, and iMessage link previews all see fully-formed metadata on the first request. This is not a minor convenience — it's a fundamental shift in what SEO tooling can do.
The metadata system in Next.js is designed to take advantage of this. You export metadata from route files, and the framework renders it into the document head at build time or request time depending on whether the metadata is static or dynamic.
Static Export vs generateMetadata
The simple case is a static metadata object. You export it from any page.tsx or layout.tsx and Next.js merges it into the document:
That's it. Next.js picks this up at build time, no work at request time. Use this for any page where the metadata doesn't change based on the content being viewed.
The moment your metadata depends on data — a blog post title, a product name, a user's profile — you need generateMetadata. This is an async function that runs on the server at request time (or at build time for statically-generated routes):
One thing that trips people up: generateMetadata shares the same data-fetching context as the page component. If both the page and generateMetadata call getPost(slug), React's request memoization deduplicates the fetch — you are not making two network requests. This is by design. Don't contort your code to avoid calling the same data function twice.
generateViewport — the Forgotten Sibling
In Next.js 15, viewport-related settings were split out of the metadata object into a separate export. If you try to put viewport, colorScheme, or themeColor inside a metadata export now, you get a deprecation warning.
The themeColor array with media queries is the right pattern for apps that support both light and dark modes. Chrome on Android uses this to color the browser chrome. Safari on iOS uses it for the status bar. It does matter, particularly for PWAs.
Like metadata, you can also export a generateViewport function if your viewport settings depend on runtime data (unusual, but possible).
Title Templates and Brand Suffixes
Every real product has a brand name in every page title: "Dashboard — Acme" or "Acme | Dashboard." Manually appending this to every page title is an error waiting to happen. The title.template field handles it automatically.
Define the template in your root layout and use the %s placeholder:
Then any child page just sets the bare title:
The default field is what renders when a child page doesn't export any title at all — useful for pages that haven't been updated yet or dynamic route segments that don't match. If you want a nested segment (say, the settings area) to have its own template like "%s — Settings — Acme", you can export a new template from app/settings/layout.tsx and it overrides the parent for everything below it.
metadataBase — the Mistake You Will Make in Production
Relative URLs in metadata objects don't work for Open Graph images. The OG protocol and Twitter Cards require absolute URLs — https://example.com/og.png, not /og.png. Next.js knows this, and it will automatically make relative URLs absolute if you tell it the base:
Sign in to keep reading
The rest of this module is free — sign in with Google to unlock it and track your progress.
Sign in & RegisterDiscussion
0Join the discussion