mirror of
https://github.com/hanzoai/next.js.git
synced 2026-09-24 17:00:51 +00:00
### What
The dev-overlay fix cards' `Copy AI prompt` button used to copy a
multi-paragraph instruction string baked into each card (71 prompts
across docs + runtime). This PR replaces that with a dynamic template
the button builds at click time:
```
Apply the [Stream] "Wrap in or move into Suspense" fix to the Next.js Insight raised in this project.
Steps:
1. The failing code is in the error block below — it may be a data-access call, a hook call, a metadata or viewport export, or a component. The fix applies to that exact code; don't touch unrelated files.
2. Read the rule docs at <rule-url> for the full Insight explanation, then read the fix section at <fix-url>. Pick the pattern under "### Patterns" that matches the failing code, then read "### Gotchas" before editing — they list constraints that are easy to miss. Use the canonical imports and code shape from the page; don't improvise variations.
3. Apply the chosen pattern to the code identified in step 1.
## Error Type
Blocking Route
## Error Message
Route "/01-cookies-body": ...
at Page (app/01-cookies-body/page.tsx:4:26)
## Code Frame
> 4 | const c = await cookies();
| ^
...
Next.js version: 16.3.0-canary.47 (Turbopack)
```
### Why
Old verbatim prompts repeated what the docs already say and never
anchored the fix to the actual error. The new template:
- Names the chosen fix (group badge + title) so the agent picks the
right pattern.
- Points at the rule docs AND the specific fix anchor, with explicit
instructions to read `### Patterns` and `### Gotchas`.
- Prepends the live error block (same shape the existing 'Copy error
info' button emits) so the agent has the failing file, line, code frame,
and stack inline — no need to re-fetch the dev overlay.
### Changes
- `FixCard` type: `prompt?: string` → `copyable?: boolean`
- `CopyPromptButton`: takes `title + group + link + generateErrorInfo`,
builds the prompt
- Refactored `generateErrorInfo` from `errors.tsx` into a shared
`utils/generate-error-info.ts` so the existing 'Copy error info' button
and the fix-card 'Copy AI prompt' button format the error block
identically
- 14 error MDX pages: every `prompt={...}` removed (36 strings)
- `instant-guidance-data.ts`: every `prompt: '...'` → `copyable: true`
(35 strings)
- Skill (`.agents/skills/insight-error-page/SKILL.md`): updated
### Pairs with
vercel/front#73087 — same template in the docs-site `<FixOption>`
(without the error block, since there's no live error on a docs page).
<!-- NEXT_JS_LLM_PR -->
169 lines
10 KiB
Plaintext
169 lines
10 KiB
Plaintext
---
|
|
title: Next.js encountered uncached data in generateMetadata()
|
|
kind: insight
|
|
---
|
|
|
|
During [prerendering](/docs/app/glossary#prerendering), [`generateMetadata()`](/docs/app/api-reference/functions/generate-metadata) performed an uncached data access ([`fetch()`](https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch), database call, [`await connection()`](/docs/app/api-reference/functions/connection)). With [Cache Components](/docs/app/api-reference/config/next-config-js/cacheComponents) enabled, Next.js expects metadata to be prerenderable when the rest of the route is. This route's metadata is blocked, but the rest of its content can be prerendered.
|
|
|
|
Request-bound reads ([`cookies()`](/docs/app/api-reference/functions/cookies), [`headers()`](/docs/app/api-reference/functions/headers), [`params`](/docs/app/api-reference/file-conventions/page#params-optional), [`searchParams`](/docs/app/api-reference/file-conventions/page#searchparams-optional)) in `generateMetadata()` have different fixes. See [Next.js encountered runtime data in `generateMetadata()`](/docs/messages/blocking-prerender-metadata-runtime).
|
|
|
|
The viewport equivalent is handled at [Uncached data in `generateViewport()`](/docs/messages/blocking-prerender-viewport-dynamic).
|
|
|
|
For errors in the page body rather than metadata, see [Next.js encountered uncached data during prerendering](/docs/messages/blocking-prerender-dynamic).
|
|
|
|
> **Good to know**: In [`next dev`](/docs/app/api-reference/cli/next#next-dev-options), the error overlay points at the failing component. Run `next build --debug-prerender` to get the full list of blocking routes with stack traces. When iterating on specific routes, use `next build --debug-build-paths /dashboard /settings` to rebuild only those pages.
|
|
|
|
## Ways to fix this
|
|
|
|
<FixOption group="cache" href="#cache-the-metadata" title="Cache the metadata">
|
|
Cache the metadata function so the result is reused and the route stays
|
|
prerenderable.
|
|
</FixOption>
|
|
|
|
<FixOption
|
|
group="dynamic"
|
|
href="#mark-the-route-as-dynamic"
|
|
title="Mark the route as dynamic"
|
|
>
|
|
Tell Next.js the page itself has dynamic content, so the dynamic metadata is
|
|
allowed.
|
|
</FixOption>
|
|
|
|
## Cache the metadata
|
|
|
|
Choose this fix when the metadata comes from an external source (CMS, database) but doesn't need to change on every request. Add the [`use cache`](/docs/app/api-reference/directives/use-cache) directive as the first statement inside [`generateMetadata()`](/docs/app/api-reference/functions/generate-metadata). Next.js caches the returned metadata object and includes it in the prerender.
|
|
|
|
### Patterns
|
|
|
|
#### Add `use cache` to `generateMetadata`
|
|
|
|
Mark the function as cacheable. The metadata is evaluated once per cache window and reused.
|
|
|
|
```jsx filename="app/blog/[slug]/page.js"
|
|
import { cms } from './cms'
|
|
|
|
export async function generateMetadata({ params }) {
|
|
'use cache'
|
|
const { slug } = await params
|
|
const { title } = await cms.getPageData(slug)
|
|
return { title }
|
|
}
|
|
|
|
async function getPageText(slug) {
|
|
'use cache'
|
|
const { text } = await cms.getPageData(slug)
|
|
return text
|
|
}
|
|
|
|
export default async function Page({ params }) {
|
|
const { slug } = await params
|
|
const text = await getPageText(slug)
|
|
return <article>{text}</article>
|
|
}
|
|
```
|
|
|
|
Learn more: [Caching with `use cache`](/docs/app/api-reference/directives/use-cache).
|
|
|
|
#### Tag the metadata for invalidation
|
|
|
|
When you publish new content and want the metadata to refresh, tag the entry with [`cacheTag`](/docs/app/api-reference/functions/cacheTag). Invalidate from a Server Action with [`updateTag`](/docs/app/api-reference/functions/updateTag) (read-your-own-writes: the next request waits for fresh data) or from a Route Handler with [`revalidateTag`](/docs/app/api-reference/functions/revalidateTag).
|
|
|
|
```jsx filename="app/blog/[slug]/page.js"
|
|
import { cacheTag } from 'next/cache'
|
|
import { cms } from './cms'
|
|
|
|
export async function generateMetadata({ params }) {
|
|
'use cache'
|
|
const { slug } = await params
|
|
cacheTag(`meta-${slug}`)
|
|
const { title } = await cms.getPageData(slug)
|
|
return { title }
|
|
}
|
|
```
|
|
|
|
Learn more: [How revalidation works](/docs/app/guides/how-revalidation-works).
|
|
|
|
### Trade-off
|
|
|
|
Freshness depends on the cache configuration. The metadata stays the same until [`cacheLife`](/docs/app/api-reference/functions/cacheLife) revalidates or expires, or until [`cacheTag`](/docs/app/api-reference/functions/cacheTag) is invalidated. Plan invalidations alongside the code that mutates the content.
|
|
|
|
### Gotchas
|
|
|
|
- [`use cache`](/docs/app/api-reference/directives/use-cache) can't be combined with [`cookies()`](/docs/app/api-reference/functions/cookies) or [`headers()`](/docs/app/api-reference/functions/headers) in the same scope. Inside a cached function, you can't call request-bound APIs. If the metadata needs a request-bound value (a session token to call a protected API), read it outside the cached scope and pass it as an argument, or use [Mark the route as dynamic](#mark-the-route-as-dynamic) instead.
|
|
- If the metadata function reads `params`, the params become part of the cache key automatically. Each unique param set gets its own cached metadata entry.
|
|
- A short [`cacheLife`](/docs/app/api-reference/functions/cacheLife) (a profile whose `revalidate` is shorter than the prerender's effective lifetime) prevents the metadata from being included in the prerender. The route becomes partially dynamic. Use a longer profile if you want the metadata included in the static shell.
|
|
|
|
## Mark the route as dynamic
|
|
|
|
Choose this fix when the rest of the page is fully static and you want the metadata to remain dynamic. Add a small component that calls [`await connection()`](/docs/app/api-reference/functions/connection), render `null` from it, and wrap it in [`<Suspense>`](https://react.dev/reference/react/Suspense).
|
|
|
|
This error fires specifically because the metadata is the only dynamic part of an otherwise fully prerenderable route. Adding a dynamic marker is an explicit signal to Next.js that the page has intentional dynamic content streamed alongside the static shell, so the dynamic metadata is allowed.
|
|
|
|
### Patterns
|
|
|
|
#### Add a dynamic marker component
|
|
|
|
Create a small component that calls [`connection()`](/docs/app/api-reference/functions/connection) and renders nothing, wrapped in [`<Suspense>`](https://react.dev/reference/react/Suspense). The page content remains prerenderable and only the marker is excluded from the prerender.
|
|
|
|
```jsx filename="app/page.js"
|
|
import { Suspense } from 'react'
|
|
import { connection } from 'next/server'
|
|
|
|
export async function generateMetadata() {
|
|
const response = await fetch('https://api.example.com/meta')
|
|
const { title } = await response.json()
|
|
return { title }
|
|
}
|
|
|
|
async function DynamicMarker() {
|
|
await connection()
|
|
return null
|
|
}
|
|
|
|
export default function Page() {
|
|
return (
|
|
<>
|
|
<article>This article is completely static</article>
|
|
<Suspense>
|
|
<DynamicMarker />
|
|
</Suspense>
|
|
</>
|
|
)
|
|
}
|
|
```
|
|
|
|
Learn more: [`connection`](/docs/app/api-reference/functions/connection).
|
|
|
|
### Trade-off
|
|
|
|
The metadata and the dynamic marker run on every request, so the route cannot be fully static. The rest of the page content still prerenders, and only the metadata blocks the initial paint.
|
|
|
|
### Gotchas
|
|
|
|
- The `DynamicMarker` must be wrapped in [`<Suspense>`](https://react.dev/reference/react/Suspense). Without the boundary, the dynamic marker propagates up and the entire page is treated as blocking, surfacing the same blocking-route error this fix is meant to address.
|
|
- This pattern is intentionally verbose. If you find yourself adding a dynamic marker, reconsider whether the metadata can be cached instead. Most metadata doesn't need to be per-request.
|
|
- If the page already has a genuinely dynamic component (one that reads [`cookies()`](/docs/app/api-reference/functions/cookies) or uncached data inside a [`<Suspense>`](https://react.dev/reference/react/Suspense) boundary), you won't see this error. The page is already partially dynamic.
|
|
- Framework-synthesized routes (`/_not-found`, `/_global-error`) inherit the root layout's `generateMetadata` and must be statically prerendered. The dynamic marker doesn't help here, because these routes don't have a page body where you can place a Suspense'd marker. If your root layout's `generateMetadata` depends on uncached data, [Cache the metadata](#cache-the-metadata) instead, or move to [`global-not-found.js`](/docs/app/api-reference/file-conventions/not-found#global-not-foundjs-experimental), which bypasses the root layout entirely and avoids inheriting its `generateMetadata`.
|
|
|
|
## Don't want this validation?
|
|
|
|
Instant-navigation validation runs by default in [Cache Components](/docs/app/api-reference/config/next-config-js/cacheComponents) apps and is what surfaces this error.
|
|
|
|
- **One segment**: add [`export const instant = false`](/docs/app/api-reference/file-conventions/route-segment-config/instant) to the page or layout file. This opts out the segment itself. Child segments are still validated during client navigations.
|
|
- **Entire app**: set [`experimental.instantInsights.validationLevel`](/docs/app/api-reference/file-conventions/route-segment-config/instant#configuring-validation-defaults) to `'manual-warning'` in `next.config`. This limits validation to segments that explicitly export `instant`.
|
|
|
|
See [Ensuring instant navigations](/docs/app/guides/instant-navigation) for the full model.
|
|
|
|
## Useful links
|
|
|
|
- [`generateMetadata()`](/docs/app/api-reference/functions/generate-metadata)
|
|
- [Runtime data in `generateMetadata()`](/docs/messages/blocking-prerender-metadata-runtime)
|
|
- [Uncached data in `generateViewport()`](/docs/messages/blocking-prerender-viewport-dynamic)
|
|
- [`use cache` directive](/docs/app/api-reference/directives/use-cache)
|
|
- [`cacheLife`](/docs/app/api-reference/functions/cacheLife)
|
|
- [`cacheTag`](/docs/app/api-reference/functions/cacheTag)
|
|
- [`updateTag`](/docs/app/api-reference/functions/updateTag)
|
|
- [`revalidateTag`](/docs/app/api-reference/functions/revalidateTag)
|
|
- [`connection` function](/docs/app/api-reference/functions/connection)
|
|
- [Ensuring instant navigations](/docs/app/guides/instant-navigation)
|