Fix Next.js App Router Metadata Not Updating: Root Causes and Minimal Checks
Quick Answer
- Root cause: Metadata in the App Router is static by default in Server Components; dynamic metadata requires explicit
generateMetadataexport or client-side handling. - First check: Ensure you are using
export const metadatain a Server Component (not a Client Component with'use client'), or implementgenerateMetadatafor dynamic values. - Minimal fix: Replace
export const metadata = { ... }withexport async function generateMetadata({ params }) { return { ... } }for route segments that depend on dynamic data. - Version boundary: This applies to Next.js 13+ (App Router); the Pages Router (
getServerSideProps) is not affected. - Environment: Works in both development and production; no additional packages required beyond
next@latest.
What Problem It Solves
When migrating from the Pages Router to the App Router in Next.js 13/14, developers often find that metadata (title, description, Open Graph tags) does not update when navigating between pages or when data changes. This happens because the App Router changes how metadata is defined and rendered. The Pages Router used next/head or custom _document.js approaches, while the App Router uses a declarative metadata export or generateMetadata function. Understanding this shift is critical for correct behavior.
Root Cause Analysis
The App Router introduces two distinct metadata patterns:
-
Static metadata:
export const metadata = { title: 'My Page' }— works only in Server Components and is evaluated at build time or during static generation. It does not re-run on client-side navigation. -
Dynamic metadata:
export async function generateMetadata({ params }) { ... }— runs on every request (for dynamic routes) and supports async data fetching. This is required when metadata depends on route parameters, search params, or fetched data.
The most common mistake is using static metadata in a route that depends on dynamic data, or accidentally placing metadata in a Client Component (which ignores the metadata export entirely).
Minimal Working Configuration
Static Metadata (for fixed pages)
TSX// app/about/page.tsx export const metadata = { title: 'About Us', description: 'Learn about our company', } export default function AboutPage() { return <div>About content</div> }
Dynamic Metadata (for route segments with params)
TSX// app/posts/[id]/page.tsx export async function generateMetadata({ params }: { params: { id: string } }) { // Fetch data or compute metadata based on params const post = await fetch(`https://api.example.com/posts/${params.id}`).then(res => res.json()) return { title: post.title, description: post.excerpt, } } export default function PostPage({ params }: { params: { id: string } }) { return <div>Post {params.id}</div> }
Common Errors and Fixes
| Error | Cause | Solution |
|---|---|---|
| Metadata not updating on navigation | Static metadata used in dynamic route | Switch to generateMetadata |
metadata export ignored | Component has 'use client' directive | Move metadata to a Server Component wrapper, or use useMetadata from a client-side library |
getServerSideProps not defined in app directory | Using Pages Router API in App Router | Migrate data fetching to Server Component or generateMetadata |
_app.js and _document.js not used | Global styles/head content not migrated | Move to app/layout.tsx; wrap Context Providers in Client Component |
Error: onLoad handler does not work in Server Components
Solution: Move components using onLoad, onReady, or onError (like <Script>) to a Client Component by adding 'use client' at the top of the file.
Production Notes and Security Checks
- CSP (Content Security Policy): The App Router does not automatically configure CSP. If you use inline scripts or styles, ensure your
next.config.jsor server headers include appropriate CSP directives. - API route authentication: When migrating API routes from
pages/apitoapp/api(Route Handlers), verify that authentication middleware is correctly applied. The App Router uses a different middleware pattern (middleware.tsat project root). - Incremental migration: You can keep
pagesandappdirectories side by side. Next.js prioritizesapproutes overpagesroutes for the same path. Removepagesdirectory only after all routes are migrated. - Rollback strategy: Keep a Git branch with the Pages Router version. If migration fails, revert to that branch. Test each migrated route individually before removing the
pagesequivalent.
FAQ
Q: Can I still use pages/api routes after migrating to App Router?
A: Yes, pages/api routes continue to work during migration. They are not affected by the App Router. After full migration, consider moving API routes to Route Handlers in app/api for consistency.
Q: How do I use React Context in the App Router?
A: React Context only works in Client Components. Create a wrapper component with 'use client' directive, export your Context Provider from it, then import and wrap it in app/layout.tsx. Ensure the Provider wraps all components that need context access.
Q: Will pages and app routes conflict?
A: Yes, if both directories define a route for the same path, the app route takes precedence. To avoid conflicts, migrate pages one at a time and verify no duplicate paths exist. Use next.config.js to control behavior if needed (though experimental.appDir is enabled by default in v13.4+).