A dynamic Next.js page can display the correct article while returning the homepage's canonical URL, an old description or the wrong sharing image. Fixing that mismatch means tracing how the route's metadata is exported, populated, inherited, cached and inspected.
Start with two different URLs that use the same template. Compare their titles, descriptions, canonicals and social metadata with the content each URL actually serves. Then investigate the layer responsible for the difference.
These 15 Next.js metadata mistakes focus on the App Router. The examples use the Promise-shaped route parameters documented for Next.js 15 and later; Cache Components issues are identified separately. The code is illustrative and should be adapted to your content contract and rendering configuration. Correct metadata helps describe the intended page, but does not guarantee indexing, rankings or a particular search snippet.
Start with a route-level metadata record
Before changing a shared layout, identify the installed Next.js version, the App Router segment and whether Cache Components is enabled. A copied solution for an older Pages Router application may address a different API or rendering behavior.
In your own project, the installed version can be checked with:
node -p "require('next/package.json').version"
Record the intended result for two real sibling pages and a nonexistent slug. The following values describe a fictional documentation site, not a Screpy test:
| Requested path | Expected content | Expected canonical |
|---|---|---|
/blog/cache-basics |
The cache basics article | https://www.example.com/blog/cache-basics |
/blog/request-memoization |
The request memoization article | https://www.example.com/blog/request-memoization |
/blog/not-a-real-article |
Missing-resource handling | No successful article identity |
Also record each page's expected title, description, sharing image and indexing policy. These values should come from the intended content record, rather than whatever the current response happens to contain.
Keep the address being inspected separate from the canonical origin. An authorized preview might serve /blog/cache-basics on a temporary hostname while correctly identifying the planned production URL as canonical. Judge that output against the release's policy; do not automatically replace its canonical with the preview address.
Fix metadata exports and route inputs
1. Exporting metadata from a Client Component
The App Router's metadata and generateMetadata exports belong in Server Components. Adding 'use client' to the file that owns those exports creates a boundary problem; changing the title string will not fix it.
Keep the route's metadata and content loading in a server page or layout, and move the interactive feature into a separate Client Component:
app/products/[slug]/page.tsx Server page and generateMetadata
app/products/[slug]/buy-button.tsx Client Component with 'use client'
The server page can render the button. There is no need to remove interactivity from the product experience. Also avoid the opposite oversimplification: Client Components can participate in first-load HTML prerendering. The specific problem here is who is allowed to export metadata.
2. Using an unsupported metadata export
Choose a static metadata object or a generateMetadata function for a particular file. Do not export both from that file. A layout providing defaults and a child page generating route-specific values is a valid composition; those are different owners.
Another misleading pattern is export const metadata = async () => .... An arbitrary asynchronous function assigned to metadata does not replace the documented generated-metadata export. Likewise, calling a helper during page rendering and storing its result in a local variable does not register those values with Next.js.
Use a Metadata object for fixed values and the named generateMetadata export for values derived from route content. Check the export itself before debugging the content API. The Metadata API reference defines both supported forms.
3. Reading the wrong or unresolved route params
Current App Router examples await params before reading their values. A [slug] route can use this shape:
type Props = {
params: Promise<{ slug: string }>
}
// Inside an async page or generateMetadata function:
const { slug } = await params
Match the key to the folder name. A [productId] segment supplies productId, not slug; a [...slug] catch-all supplies an array. An ordinary /about page does not acquire a slug merely because its component's type declares one.
The page convention reference explains the Promise-based inputs and older synchronous behavior. If every route receives the homepage metadata, inspect the actual parameter and lookup key before adding a default empty slug. That default can turn an identifiable input error into a convincing wrong answer.
Keep metadata attached to the correct content
4. Fetching different content for the page and metadata
A route can use the right slug in its page component and a different lookup in generateMetadata. For example, the body might request an article by slug while the metadata helper reads the first item in a collection. Both requests succeed, so the mismatch survives a basic availability check.
Use one route-scoped loader for the content record, then derive the visible page and metadata from that record. Include other identity inputs, such as locale, where your application needs them. Avoid mutating one shared metadata object as different requests arrive.
For non-fetch data access, Next.js documents sharing a loader with React's cache. That addresses repeated work within rendering; it is not a CMS publication-invalidation strategy.
A separate trap is a browser-style request such as fetch('/api/articles/' + slug) inside server metadata generation. Use direct server data access or an appropriate absolute content API URL. metadataBase resolves metadata fields; it does not provide a base URL for that fetch.
5. Trusting unvalidated CMS fields
A TypeScript declaration does not inspect JSON returned by a CMS. A field typed as description: string in your application can still arrive as null, a rich-text object or a string containing internal editorial notes.
Define the content contract before building metadata:
| Field | Useful validation or review |
|---|---|
| Slug | Matches the requested content identity and expected path shape |
| Title | Nonempty plain text identifying this page |
| Description | A relevant, publishable plain-text summary |
| Publication state | An explicit supported value, not inferred from record existence |
| Image, if used | An intended public asset with an appropriate URL and description |
Validate types at the data boundary, then review meaning in the publishing workflow. Do not repair every invalid field by silently returning the homepage title. Also avoid arbitrary substring cuts through HTML or Markdown: they can produce broken or misleading summaries. A valid string is necessary for this contract, but its length alone does not establish a useful description.
6. Treating missing records and service failures as the same page
A content API's 404, an unpublished record, a timeout and an upstream 503 describe different situations. Converting all of them into “Read our latest article” metadata hides the failure and can give a nonexistent route a successful-looking identity.
Use the application's missing-resource path for genuinely absent content. Let service failures reach the appropriate error handling instead of describing another article. Call notFound() outside a broad catch that would swallow its control-flow exception.
Check the actual response as well as the metadata object. The current notFound reference explains that it adds noindex; if streaming has already started, the response may retain 200 rather than changing to 404. A generic title is therefore insufficient evidence of correct missing-page behavior. Keep error status, rendered content and indexing directives in the same investigation. The broader JavaScript crawlability guide explains why delivery evidence matters beyond the component's source code.
Check title templates and canonical identity
7. Applying title templates at the wrong level
A title template formats descendant titles. It does not replace the need for a page-specific title, and a template defined in a terminal page.tsx has no child pages to format. A layout's template also does not apply to its same-segment page title.
For a layout that owns descendant article routes, a simple configuration is:
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: {
default: 'Atlas Guides',
template: '%s | Atlas Guides',
},
}
Inspect the final title rather than assuming the configuration produced it:
| Descendant page value | Intended resolved result |
|---|---|
title: 'Cache basics' |
Cache basics | Atlas Guides |
title: 'Cache basics | Atlas Guides' |
A duplicated suffix, which should be corrected |
title: { absolute: 'Cache basics reference' } |
Cache basics reference, bypassing the inherited template |
Nested layouts can introduce another template, so trace the segment that actually owns the formatting. Review the wording for a clear page topic; a particular character count does not prove the title is useful or ensure Google displays it unchanged.
8. Reusing a canonical that belongs to another URL
A homepage canonical in a shared layout can survive into article routes that never override it. Another failure constructs the title from the current article but leaves alternates.canonical pointing to a previously copied slug.
Build the preferred article URL from the validated content identity and an explicit URL policy. For the fictional /blog/cache-basics record, that might be https://www.example.com/blog/cache-basics. Inspect the resulting link element, not just the TypeScript property.
Query strings require a content decision. A tracking parameter may leave an article unchanged, while a page number or meaningful filter may represent different content. Do not remove every query parameter, point every variant at the homepage, or consolidate unrelated articles to fix a template error.
Google treats canonical declarations as preference signals, rather than a command that guarantees its selection. Use an absolute resulting URL and align it with the intended representative content. The canonical-tag guide covers the broader consolidation decision.
9. Resolving metadata against the wrong origin or base path
metadataBase provides a base for relative metadata URLs. Set it deliberately; do not accidentally use localhost or a temporary deployment hostname as the production identity.
export const metadata: Metadata = {
metadataBase: new URL('https://www.example.com'),
}
Also inspect its pathname. Next.js metadata URL composition is not ordinary directory traversal: a base such as https://www.example.com/docs can prefix the path of a relative metadata field, including one beginning with /. That can turn an intended blog canonical into /docs/blog/....
If the field already supplies an absolute URL, it does not need that base to resolve. Either approach can work; the actual resolved origin and path must match your policy. Keep this separate from the content API address: changing metadataBase does not repair a relative server-side fetch call.
Rebuild inherited social metadata deliberately
10. Assuming nested metadata deep-merges
A child can replace a nested metadata object rather than extending each of its fields. If a layout provides an Open Graph description and image, returning only a new openGraph.title from the page can remove the other inherited values.
Decide which fields belong to the site and which belong to the article. Build the intended object explicitly, for example:
const siteSocial = {
siteName: 'Atlas Guides',
locale: 'en_US',
}
// Within generateMetadata, after loading the article:
return {
openGraph: {
...siteSocial,
type: 'article',
title: article.title,
description: article.description,
url: canonical,
},
}
If you use the resolved parent argument, await it and select the relevant inherited values intentionally. Do not retain an unrelated homepage or previous article's image merely to avoid an empty field. Check the resolved description, URL and images together; finding the correct og:title is only one part of the result.
11. Overlooking a higher-priority image file
You can correct openGraph.images in code and still receive another image because a metadata file has priority. Look for existing opengraph-image and twitter-image files at the root and in the affected route segments.
An inventory might include:
app/opengraph-image.png
app/blog/opengraph-image.png
app/blog/[slug]/opengraph-image.tsx
This is an inspection example, not a recommendation to create all three. Identify the intended owner before removing or adding files. The Open Graph image convention documents static and generated assets; the more specific applicable image convention can change the route's result.
Open the image URL actually emitted for each article. Confirm it returns the intended usable image, rather than an HTML error page or a private asset. Verify its dimensions and relevant alt text. Static conventions can use an accompanying opengraph-image.alt.txt; generated image files can export alt.
If your response and asset are correct but a sharing service still shows an old card, investigate that service's cached preview separately. Rewriting the article title repeatedly is not evidence that the Next.js metadata function failed.
Match caching to the rendering and publishing model
12. Mistaking request memoization for publish-time freshness
Sharing a loader between a page and generateMetadata can avoid duplicate work during rendering. It does not guarantee that an edited CMS description will reach the next visitor. Persistent data caching, prerendered output and downstream caches can still serve an earlier revision.
Trace the data path that actually supplies the stale field. Record the content revision, cache options and rendered value before and after a controlled edit. If the title changes but the description does not, investigate whether they use separate queries or cache entries before invalidating everything.
The fetch reference distinguishes render-pass memoization from persistent caching. It also documents development HMR reuse, which can make a local refresh an unreliable publishing-freshness check. Recheck the relevant production-mode behavior in an authorized candidate environment.
For cached content, connect publication to the correct assigned tags or paths. Understand the chosen invalidation semantics:
| Mechanism | Important behavior |
|---|---|
revalidateTag(tag, 'max') |
Marks assigned tagged data stale; a subsequent request can receive stale content while revalidation runs. |
updateTag(tag) |
Expires assigned tagged data for read-your-own-writes, and is restricted to Server Actions. |
The official revalidateTag and updateTag references explain the distinction. A CMS webhook Route Handler cannot simply substitute updateTag. Nor does calling a tag name refresh data that was never associated with that tag.
13. Applying a generic fix to a Cache Components metadata error
A build failure involving generateMetadata may concern prerenderability rather than stale data. With Cache Components, otherwise prerenderable content and uncached or request-bound metadata need an intentional rendering strategy.
The current uncached metadata diagnostic is part of the Instant Navigations behavior documented from Next.js 16.3. Match its conditions to your version and configuration before copying the fix.
| Actual dependency | Direction to investigate |
|---|---|
| Shared CMS metadata that can be reused | A cacheable data function or metadata result, with an explicit freshness and invalidation policy |
| Metadata that genuinely needs request-time values | The documented dynamic-rendering boundary appropriate to that route |
| A cached helper reading cookies or headers internally | Move the request-bound read outside that cached scope, or reconsider the caching design |
For the specific case where metadata must remain per-request while the body prerenders, the diagnostic shows a small component that awaits connection(), returns null and sits inside a Suspense boundary. The static article remains outside that boundary. This is an explicit rendering decision, not a requirement for ordinary CMS metadata, and wrapping the entire page in an empty fallback defeats the intended static shell.
The use cache reference defines the request-API and serialization constraints. For example, returning a URL instance inside a cached metadata result is different from using new URL(...) in ordinary static metadata. Use supported serializable values for the cached result.
Do not enable Cache Components, disable its validation, or make the whole page dynamic simply because a tutorial mentions those settings. Resolve the actual dependency and verify the resulting static or dynamic behavior. The next example deliberately uses a different, explicitly bounded runtime configuration.
A reference pattern for a dynamic article page
This example connects one validated article record to the visible page and its metadata. It assumes a runtime-capable App Router application without Cache Components enabled and the Promise-shaped parameters used in current releases. It is a documentation-based illustration, not a tested integration with your CMS.
The content endpoint is fictional. Replace it with your authorized content source and adapt the schema. The expected JSON contract is:
{
"slug": "cache-basics",
"title": "Cache basics",
"description": "Understand the difference between a cached response and repeated work within one render.",
"body": "Plain-text article content for this illustration.",
"published": true
}
Define the public origin
Use the same intended public origin when configuring your existing root layout's metadataBase and building article URLs. Retain the root title template shown earlier.
// app/lib/site.ts
export const PUBLIC_ORIGIN = 'https://www.example.com'
Load and validate one article
The loader treats missing content differently from a failed service or invalid response. React's cache shares the loader's work during rendering; this example's no-store fetch policy is explicit rather than relying on a persistent-cache default.
// app/lib/articles.ts
import { cache } from 'react'
import { notFound } from 'next/navigation'
type Article = {
slug: string
title: string
description: string
body: string
published: boolean
}
function isArticle(value: unknown): value is Article {
if (typeof value !== 'object' || value === null) return false
const record = value as Record<string, unknown>
const textFields = [
record.slug, record.title, record.description, record.body,
]
return textFields.every(
field => typeof field === 'string' && field.trim().length > 0,
) && typeof record.published === 'boolean'
}
export const getArticle = cache(async (slug: string): Promise<Article> => {
const endpoint = new URL(
`/articles/${encodeURIComponent(slug)}`,
'https://content.example.com',
)
const response = await fetch(endpoint, { cache: 'no-store' })
if (response.status === 404) notFound()
if (!response.ok) throw new Error('Article content is unavailable.')
const value: unknown = await response.json()
if (
typeof value === 'object' && value !== null &&
'published' in value && value.published === false
) notFound()
if (!isArticle(value) || value.slug !== slug) {
throw new Error('Article content has an invalid format.')
}
return value
})
Derive metadata and content from the same record
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next'
import { getArticle } from '@/app/lib/articles'
import { PUBLIC_ORIGIN } from '@/app/lib/site'
type Props = { params: Promise<{ slug: string }> }
export async function generateMetadata(
{ params }: Props,
): Promise<Metadata> {
const { slug } = await params
const article = await getArticle(slug)
const canonical = new URL(
`/blog/${encodeURIComponent(article.slug)}`,
PUBLIC_ORIGIN,
).href
return {
title: article.title,
description: article.description,
alternates: { canonical },
openGraph: {
type: 'article',
siteName: 'Atlas Guides',
title: article.title,
description: article.description,
url: canonical,
},
}
}
export default async function ArticlePage({ params }: Props) {
const { slug } = await params
const article = await getArticle(slug)
return (
<article>
<h1>{article.title}</h1>
<p>{article.body}</p>
</article>
)
}
The example assumes one path segment per article slug and renders body as plain text. A rich-content CMS needs its own validated rendering pipeline. Add sharing images only when you have verified the asset and its metadata-file ownership; no image endpoint is implied by this example.
For cached production content, replace the deliberately uncached fetch policy with a supported policy and publication invalidation appropriate to your application. For Cache Components, adapt the data function and rendering boundary using the preceding diagnostic. In both cases, retain the important properties: one content identity, validated external data, explicit missing/error behavior and a canonical built from the intended public origin.
Verify the response and keep a useful evidence record
14. Inspecting only the first HTML chunk or browser tab
A browser tab can show a new title while another relevant metadata value remains wrong. The first HTML chunk can also omit metadata that arrives later. Neither observation alone establishes the result a crawler processes.
Current Next.js supports streamed metadata for dynamic rendering. HTML-limited bots receive blocking metadata, with behavior influenced by the user agent and htmlLimitedBots. The configuration reference also notes that a custom matcher replaces the default list. Do not disable streaming globally merely because an audit looked only at an early chunk.
Capture the response to a complete GET in an authorized environment. These are example shell commands, not results from a tested site:
curl --silent --show-error \
--dump-header page-headers.txt \
--output page-response.html \
'https://www.example.com/blog/cache-basics'
Read the response status and destination before interpreting the saved body. If it is a redirect, investigate the intended destination. Then compare the complete response with the rendered document after metadata has resolved. For canonicals in particular, check the effective document head; a matching URL somewhere in a response is insufficient evidence of a valid canonical link.
Compare equivalent request profiles when investigating bot-specific behavior. Sending a bot's user-agent string can help reproduce a response path, but does not prove what a genuine Googlebot request received or what Google indexed. Use Search Console's actual available crawl/render evidence for that question, preserving its observation time.
15. Overlooking a conflicting robots directive
The article title and canonical can be correct while an independent X-Robots-Tag: noindex header prevents the intended indexing outcome. Conversely, a preview may correctly remain excluded. Compare the actual response with the page's intended publishing state.
Inspect robots and Googlebot meta tags as well as response headers. An added index: true property is not a way to cancel another applicable restrictive directive. Google's robots directive specification explains how these rules combine.
Do not rely on client-side code to remove an initial noindex: Google may decide not to render that page. Also keep crawling and indexing separate. Blocking a path in robots.txt can prevent Google from reading a newly added noindex directive. Private content still needs appropriate access control; a metadata flag is not authentication.
Keep the route, revision and observation together
Request page A, page B and page A again using the same profile. Compare each result with the intended content revision. Repeat important direct-entry and client-navigation paths where relevant; those observations can expose a shared-state or stale-navigation problem that a single successful request misses.
Save a compact record for each finding:
Release / environment:
Requested URL / response status / final destination:
Content record / intended revision:
Expected title / description / canonical / social image:
Observed values / saved response / rendered-head evidence:
Robots meta / X-Robots-Tag / intended indexing policy:
Decision: correct, needs a fix, or unverified
Retest after the change:
Mark unavailable rendering, content or crawler evidence as unverified. A successful build, a valid TypeScript object or an accepted indexing request does not establish that the published page has the intended metadata. The Claude Code release-check guide provides a broader handoff process when several templates change together.
Screpy, which publishes this guide, offers Website Audit to group recorded findings such as missing or repeated titles, descriptions and canonical targets. Use that evidence to identify affected URLs and compare equivalent recrawls. Trace the responsible Next.js function and cache policy separately; the crawl alone cannot establish which application branch ran or whether Google indexed the corrected version.