Verified against Claude Code · 2026-07-20
Wire up generateMetadata so every page actually gets dynamic SEO tags
Adds a proper generateMetadata function per route — real fetched data, metadataBase, canonical URLs, and explicit noindex handling — instead of a static metadata object copied across every page.
The prompt
Ready to copy — highlighted parts are example details you can swap.
You are adding or fixing generateMetadata for a specific route. If the page's title or description depend on real data — a product name, a post title — generateMetadata must be an async function that fetches that same data using the exact same shared data-fetching function the page component itself uses, so the request gets deduped against the page's own fetch instead of firing as a second, separate query.
PAGE TYPE AND ROUTE
A blog post detail page at /blog/[slug]
DATA SOURCE
getPost(slug) — a shared, memoized data-fetching function used by both the page component and generateMetadata
SITE DOMAIN
https://blog.example.com
INDEXING RULES
Published posts should be indexed; draft posts (reachable via a share link with ?preview=true) must never be indexed
REQUIREMENTS
Export generateMetadata as an async function accepting the same params and searchParams the page component receives, and return title, description, and openGraph fields built from the real fetched data, never placeholder text left in as a stand-in. Set metadataBase in the root layout's metadata export to the domain given above, so that relative Open Graph and Twitter image paths resolve to absolute URLs — do not hardcode the domain into every individual page's metadata export, since that duplicates a single fact across every route and drifts the moment the domain changes. Set a canonical URL per page via alternates.canonical, especially for any route reachable through more than one URL pattern, such as query-parameter variants or a trailing-slash difference, since search engines otherwise treat those as separate, competing pages. For any page that shouldn't be indexed — draft content, an internal tool, a duplicate parameterized view — set robots: { index: false } explicitly on that page's own metadata rather than relying on it being excluded from a sitemap alone, since exclusion from a sitemap doesn't stop a crawler from finding and indexing the page through some other link path. If the data fetch inside generateMetadata fails or returns nothing — a deleted product, a post that no longer exists — return sensible fallback metadata, such as a generic title paired with noindex, rather than letting the function throw and take down the entire page render over what should only affect the page's metadata.
OUTPUT FORMAT
The generateMetadata function code, plus one line confirming the fetch it makes is the exact same cached, deduped call the page component uses, named by function, and one line listing which routes on this app should be marked noindex and why each one qualifies.Customize
Optional — swap in your own details for the highlighted parts above.
Why this works
generateMetadata runs on the server and resolves before the page's HTML shell is sent, and it has to use the same shared data-fetching function as the page component rather than its own separate query for a concrete performance reason, not a style preference: Next.js's fetch memoization dedupes identical requests made during the same render pass, so calling getPost(slug) from both generateMetadata and the page component costs exactly one network request, not two, but only if it's genuinely the same function call with the same arguments, not a copy-pasted variant that happens to fetch equivalent data through a slightly different code path that memoization can't recognize as identical. The metadataBase requirement fixes a mistake that's specifically invisible during local development and only breaks in production: a relative Open Graph image path resolves fine against localhost during testing, and only fails once a social platform's crawler tries to fetch that same relative path with no origin to resolve it against, which means this bug ships clean through every local check and only surfaces the first time someone shares a link and the preview card comes back broken. The explicit noindex-on-the-page-itself rule, rather than relying on sitemap exclusion alone, closes a gap that a sitemap-only approach genuinely can't cover: a sitemap tells a crawler what to prioritize, but it does nothing to stop that same crawler from discovering and indexing a page through an entirely different path, such as an internal link or a shared URL, so a draft page excluded from the sitemap but missing its own robots directive can still end up indexed the moment anything else on the web links to it. And the mandatory fallback-on-failure rule matters because generateMetadata throwing on a deleted or not-yet-published item would otherwise take down the entire page render, not just its metadata — an error in a function whose only job is to produce two or three text fields shouldn't be capable of turning into a full 500 for the visitor.
What you get back
generateMetadata for /blog/[slug] returns { title: post.title + ' — Example Blog', description: post.excerpt, openGraph: { images: [post.heroImage] } } using the exact same getPost(slug) call the page component uses, so the two calls dedupe into one fetch. If getPost returns null (post not found or unpublished), metadata falls back to { title: 'Post not found', robots: { index: false } } instead of throwing. Noindex routes: /blog/[slug]?preview=true (draft posts, reachable only via a private share link) — flagged robots: { index: false } directly on the page's own metadata, not left to sitemap exclusion alone.
Verified against
Claude Code Sonnet 4.6 · 2026-07-20
GitHub Copilot 2026.7 · 2026-07-31
Changelog
- 2026-07-20 — Initial publish, verified against Claude Code (Sonnet 4.6) and GitHub Copilot on Next.js 16 dynamic blog and product pages.
Need this built into your business?
If a prompt isn't enough — custom software, built and maintained for you — that's Scult's day job.
EXPLORE CUSTOM SOFTWARE
