How to Fix: `generateMetadata` is not called anymore if it is in `page.tsx`, it’s called only from `layout.tsx` when upgrading from v14.2.7 to v14.2.8, making all the pages dynamic titles to disappear.
Next.js 14.2.8 Broke generateMetadata in page.tsx: Why Dynamic Titles Disappear and How to Fix Them
If your dynamic page titles suddenly vanished after upgrading from Next.js 14.2.7 to 14.2.8, and generateMetadata now seems to run only from layout.tsx instead of page.tsx, you are hitting a version-specific App Router metadata regression. The result is easy to spot: routes still render, but per-page SEO metadata stops updating, causing every page to inherit stale or static titles from the layout layer.
You can review the original reproduction from the issue using the linked sandbox example.
Symptoms
This bug usually appears with the following behavior:
- generateMetadata inside page.tsx is no longer invoked.
- generateMetadata inside layout.tsx still runs.
- Dynamic metadata such as title, description, canonical values, and social tags stop changing per route.
- Pages may appear to share the same title, often inherited from the nearest parent layout.
- The issue starts immediately after upgrading from 14.2.7 to 14.2.8.
Understanding the Root Cause
In the Next.js App Router, metadata is resolved across the route tree from parent layouts down to pages. Under normal behavior, layout.tsx contributes shared metadata, while page.tsx can override or extend it using generateMetadata. In 14.2.8, a regression affected parts of this resolution flow, causing page-level metadata generation to be skipped in cases where it should still execute.
Technically, this is not a misuse of the API. It is a framework-level issue introduced during the upgrade. That is why:
- Your code works in 14.2.7.
- The same code stops working in 14.2.8.
- Moving metadata logic into layout.tsx appears to work, but only as a workaround.
The practical impact is significant for SEO and content platforms because page-specific metadata often depends on route params, fetched entities, slugs, locales, or CMS content. When page-level metadata is skipped, the framework falls back to metadata resolved earlier in the tree, which makes titles appear static.
In short, the root cause is a Next.js version regression in metadata evaluation order or invocation behavior, not an application-level routing mistake.
Step-by-Step Solution
The safest fix is to use one of the following approaches depending on your release constraints.
Option 1: Pin Next.js back to 14.2.7
If you need an immediate production-safe resolution, downgrade to the last known working version.
npm install next@14.2.7
Or with Yarn:
yarn add next@14.2.7
Or with pnpm:
pnpm add next@14.2.7
Then remove lockfile drift if needed and reinstall dependencies:
rm -rf node_modules package-lock.json .next
npm install
npm run dev
This restores expected behavior where generateMetadata in page.tsx is called again.
Option 2: Upgrade to a version containing the fix
If the bug has already been patched in a later release, upgrade directly instead of staying on 14.2.8. Check the Next.js changelog and release notes for metadata-related fixes, then install the corrected version.
npm install next@latest
After upgrading, verify page-level metadata execution by adding a temporary log:
export async function generateMetadata({ params }) {
console.log('page metadata called', params)
return {
title: `Post ${params.slug}`,
}
}
If the log appears during route resolution and the browser title updates correctly, the regression is resolved.
Option 3: Temporary workaround by lifting metadata to layout.tsx
If you cannot downgrade or upgrade immediately, move the metadata logic to the nearest layout that still receives the required route params.
Before in app/blog/[slug]/page.tsx:
export async function generateMetadata({ params }) {
const post = await getPost(params.slug)
return {
title: post.title,
description: post.excerpt,
}
}
export default async function Page({ params }) {
const post = await getPost(params.slug)
return <article>{post.title}</article>
}
Temporary workaround in app/blog/[slug]/layout.tsx:
export async function generateMetadata({ params }) {
const post = await getPost(params.slug)
return {
title: post.title,
description: post.excerpt,
}
}
export default function BlogPostLayout({ children }) {
return children
}
This is not always architecturally ideal, but it preserves dynamic SEO metadata until the framework fix is applied.
Option 4: Avoid duplicate fetches while applying the workaround
If both your layout and page fetch the same entity, use a shared server utility so the logic remains consistent.
import { cache } from 'react'
export const getPost = cache(async (slug) => {
const res = await fetch(`https://example.com/api/posts/${slug}`, {
next: { revalidate: 60 },
})
if (!res.ok) throw new Error('Failed to load post')
return res.json()
})
Then reuse it in both files:
import { getPost } from './data'
export async function generateMetadata({ params }) {
const post = await getPost(params.slug)
return { title: post.title }
}
export default async function Page({ params }) {
const post = await getPost(params.slug)
return <article>{post.title}</article>
}
This reduces the performance cost of the workaround and keeps metadata and page rendering aligned.
Option 5: Clear cache and validate the route tree
Even though this issue is version-specific, cache artifacts can make debugging harder. After changing versions, always clear the build output:
rm -rf .next
Also validate that the route structure is correct:
app/
blog/
[slug]/
layout.tsx
page.tsx
And confirm that your page-level file really exports the metadata function:
export async function generateMetadata() {
return {
title: 'Expected dynamic title',
}
}
Common Edge Cases
1. Static optimization masks the bug
If your page is being statically optimized, you may think generateMetadata is not running when the actual issue is cached output. If metadata depends on request-time data, ensure your route configuration matches that behavior.
export const dynamic = 'force-dynamic'
Use this only when truly necessary, because it changes rendering strategy.
2. Params are only available at a nested segment
If you move metadata into a parent layout as a workaround, that layout must still sit at a route segment where the needed params exist. A layout above [slug] will not have access to params.slug.
3. Metadata appears correct in development but wrong in production
Development mode can behave differently from a production build because of caching, bundling, and route precomputation. Always test with:
npm run build
npm run start
4. Mixing static metadata and generateMetadata
If you export both a static metadata object and generateMetadata in conflicting locations, the resolved result may be harder to reason about. Keep metadata ownership clear between layout and page files.
5. Fetching failures silently degrade SEO
If your metadata function depends on an API call that fails during rendering, you may end up with fallback titles or parent metadata. Add explicit error handling:
export async function generateMetadata({ params }) {
try {
const post = await getPost(params.slug)
return {
title: post.title,
description: post.excerpt,
}
} catch {
return {
title: 'Post not found',
description: 'Unable to load metadata for this page.',
}
}
}
FAQ
Why does layout.tsx metadata still work while page.tsx metadata does not?
Because the regression affects page-level metadata invocation, not the entire metadata system. Parent or segment layouts may still participate in metadata resolution, which is why you see inherited titles instead of route-specific ones.
Is this caused by incorrect App Router usage?
No. If the exact same code works in 14.2.7 and breaks in 14.2.8, that strongly indicates a framework regression rather than misuse. You should still validate your route structure, but the version jump is the key signal.
What is the best production fix right now?
The best immediate fix is to either downgrade to 14.2.7 or upgrade to a version where the bug is patched. Moving logic to layout.tsx is a practical workaround, but it should usually be treated as temporary unless it also matches your intended architecture.
The core takeaway is simple: if generateMetadata in page.tsx stopped running after upgrading to Next.js 14.2.8, the most reliable path is to revert or move forward to a fixed release, then only use the layout-based workaround when you need business continuity before the version issue is resolved.