Files
next.js/docs/01-app/03-api-reference/04-functions/generate-viewport.mdx
75af3bbd28 Uncached data suspense issue (#90450)
<!-- Thanks for opening a PR! Your contribution is much appreciated.
To make sure your PR is handled as smoothly as possible we request that
you follow the checklist sections below.
Choose the right checklist for the change(s) that you're making:

## For Contributors

### Improving Documentation

- Run `pnpm prettier-fix` to fix formatting issues before opening the
PR.
- Read the Docs Contribution Guide to ensure your contribution follows
the docs guidelines:
https://nextjs.org/docs/community/contribution-guide

### Fixing a bug

- Related issues linked using `fixes #number`
- Tests added. See:
https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs
- Errors have a helpful link attached, see
https://github.com/vercel/next.js/blob/canary/contributing.md

### Adding a feature

- Implements an existing feature request or RFC. Make sure the feature
request has been accepted for implementation before opening a PR. (A
discussion must be opened, see
https://github.com/vercel/next.js/discussions/new?category=ideas)
- Related issues/discussions are linked using `fixes #number`
- e2e tests added
(https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs)
- Documentation added
- Telemetry added. In case of a feature if it's used or not.
- Errors have a helpful link attached, see
https://github.com/vercel/next.js/blob/canary/contributing.md


## For Maintainers

- Minimal description (aim for explaining to someone not on the team to
understand the PR)
- When linking to a Slack thread, you might want to share details of the
conclusion
- Link both the Linear (Fixes NEXT-xxx) and the GitHub issues
- Add review comments if necessary to explain to the reviewer the logic
behind a change

### What?
Added a new section "Opting Into Fully Dynamic Rendering" to
`errors/blocking-route.mdx`.

### Why?
To surface the "Suspense over body" pattern as a general solution for
"Uncached data or connection() was accessed outside of <Suspense>"
errors. This pattern was previously only documented in
`next-prerender-dynamic-viewport.mdx` but is relevant to all blocking
route errors, providing a clear opt-out for users who want fully dynamic
rendering without granular Suspense boundaries. This directly addresses
a common user query.

### How?
Added a new section with a code example demonstrating wrapping the root
`<html>` in `<Suspense>` within `app/layout.tsx`.

Closes NEXT-
Fixes #

-->

---
[Slack
Thread](https://vercel.slack.com/archives/C07BS1WEYAZ/p1771948980142259?thread_ts=1771948980.142259&cid=C07BS1WEYAZ)

<p><a
href="https://cursor.com/agents/bc-a1a4ed9d-03cd-4241-b057-748fbf9487a9"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-web-light.png"><img
alt="Open in Web" width="114" height="28"
src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a>&nbsp;<a
href="https://cursor.com/background-agent?bcId=bc-a1a4ed9d-03cd-4241-b057-748fbf9487a9"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img
alt="Open in Cursor" width="131" height="28"
src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a>&nbsp;</p>

---------

Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Joseph <[email protected]>
2026-03-05 14:58:15 +01:00

286 lines
8.1 KiB
Plaintext

---
title: generateViewport
description: API Reference for the generateViewport function.
related:
title: Next Steps
description: View all the Metadata API options.
links:
- app/api-reference/file-conventions/metadata
- app/getting-started/caching
- app/api-reference/config/next-config-js/cacheComponents
---
You can customize the initial viewport of the page with the static `viewport` object or the dynamic `generateViewport` function.
> **Good to know**:
>
> - The `viewport` object and `generateViewport` function exports are **only supported in Server Components**.
> - You cannot export both the `viewport` object and `generateViewport` function from the same route segment.
> - If you're coming from migrating `metadata` exports, you can use [metadata-to-viewport-export codemod](/docs/app/guides/upgrading/codemods#metadata-to-viewport-export) to update your changes.
## The `viewport` object
To define the viewport options, export a `viewport` object from a `layout.jsx` or `page.jsx` file.
```tsx filename="layout.tsx | page.tsx" switcher
import type { Viewport } from 'next'
export const viewport: Viewport = {
themeColor: 'black',
}
export default function Page() {}
```
```jsx filename="layout.jsx | page.jsx" switcher
export const viewport = {
themeColor: 'black',
}
export default function Page() {}
```
## `generateViewport` function
`generateViewport` should return a [`Viewport` object](#viewport-fields) containing one or more viewport fields.
```tsx filename="layout.tsx | page.tsx" switcher
export function generateViewport({ params }) {
return {
themeColor: '...',
}
}
```
In TypeScript, the `params` argument can be typed via [`PageProps<'/route'>`](/docs/app/api-reference/file-conventions/page#page-props-helper) or [`LayoutProps<'/route'>`](/docs/app/api-reference/file-conventions/layout#layout-props-helper) depending on where `generateViewport` is defined.
```jsx filename="layout.js | page.js" switcher
export function generateViewport({ params }) {
return {
themeColor: '...',
}
}
```
> **Good to know**:
>
> - If the viewport doesn't depend on request information, it should be defined using the static [`viewport` object](#the-viewport-object) rather than `generateViewport`.
## Viewport Fields
### `themeColor`
Learn more about [`theme-color`](https://developer.mozilla.org/docs/Web/HTML/Element/meta/name/theme-color).
**Simple theme color**
```tsx filename="layout.tsx | page.tsx" switcher
import type { Viewport } from 'next'
export const viewport: Viewport = {
themeColor: 'black',
}
```
```jsx filename="layout.jsx | page.jsx" switcher
export const viewport = {
themeColor: 'black',
}
```
```html filename="<head> output" hideLineNumbers
<meta name="theme-color" content="black" />
```
**With media attribute**
```tsx filename="layout.tsx | page.tsx" switcher
import type { Viewport } from 'next'
export const viewport: Viewport = {
themeColor: [
{ media: '(prefers-color-scheme: light)', color: 'cyan' },
{ media: '(prefers-color-scheme: dark)', color: 'black' },
],
}
```
```jsx filename="layout.jsx | page.jsx" switcher
export const viewport = {
themeColor: [
{ media: '(prefers-color-scheme: light)', color: 'cyan' },
{ media: '(prefers-color-scheme: dark)', color: 'black' },
],
}
```
```html filename="<head> output" hideLineNumbers
<meta name="theme-color" media="(prefers-color-scheme: light)" content="cyan" />
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="black" />
```
### `width`, `initialScale`, `maximumScale` and `userScalable`
> **Good to know**: The `viewport` meta tag is automatically set, and manual configuration is usually unnecessary as the default is sufficient. However, the information is provided for completeness.
```tsx filename="layout.tsx | page.tsx" switcher
import type { Viewport } from 'next'
export const viewport: Viewport = {
width: 'device-width',
initialScale: 1,
maximumScale: 1,
userScalable: false,
// Also supported but less commonly used
// interactiveWidget: 'resizes-visual',
}
```
```jsx filename="layout.jsx | page.jsx" switcher
export const viewport = {
width: 'device-width',
initialScale: 1,
maximumScale: 1,
userScalable: false,
// Also supported but less commonly used
// interactiveWidget: 'resizes-visual',
}
```
```html filename="<head> output" hideLineNumbers
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"
/>
```
### `colorScheme`
Learn more about [`color-scheme`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta/name#:~:text=color%2Dscheme%3A%20specifies,of%20the%20following%3A).
```tsx filename="layout.tsx | page.tsx" switcher
import type { Viewport } from 'next'
export const viewport: Viewport = {
colorScheme: 'dark',
}
```
```jsx filename="layout.jsx | page.jsx" switcher
export const viewport = {
colorScheme: 'dark',
}
```
```html filename="<head> output" hideLineNumbers
<meta name="color-scheme" content="dark" />
```
## With Cache Components
When [Cache Components](/docs/app/getting-started/caching) is enabled, `generateViewport` follows the same rules as other components. If viewport accesses runtime data (`cookies()`, `headers()`, `params`, `searchParams`) or performs uncached data fetching, it defers to request time.
Unlike metadata, viewport cannot be streamed because it affects initial page load UI. If `generateViewport` defers to request time, the page would need to block until resolved.
If viewport depends on external data but not runtime data, use `use cache`:
```tsx filename="app/layout.tsx" highlight={2}
export async function generateViewport() {
'use cache'
const { width, initialScale } = await db.query('viewport-size')
return { width, initialScale }
}
```
If viewport genuinely requires runtime data, wrap the document `<body>` in a Suspense boundary to signal that the entire route should be dynamic:
```tsx filename="app/layout.tsx" highlight={1,13,17}
import { Suspense } from 'react'
import { cookies } from 'next/headers'
export async function generateViewport() {
const cookieJar = await cookies()
return {
themeColor: cookieJar.get('theme-color')?.value,
}
}
export default function RootLayout({ children }) {
return (
<Suspense>
<html>
<body>{children}</body>
</html>
</Suspense>
)
}
```
Caching is preferred because it allows static shell generation. Wrapping the document `body` in Suspense means there is no static shell or content to immediately send when a request arrives, making the entire route block until ready on every request.
> **Good to know**: Use [multiple root layouts](/docs/app/api-reference/file-conventions/layout#root-layout) to isolate fully dynamic viewport to specific routes, while still letting other routes in your application generate a static shell.
## Types
You can add type safety to your viewport object by using the `Viewport` type. If you are using the [built-in TypeScript plugin](/docs/app/api-reference/config/typescript) in your IDE, you do not need to manually add the type, but you can still explicitly add it if you want.
### `viewport` object
```tsx
import type { Viewport } from 'next'
export const viewport: Viewport = {
themeColor: 'black',
}
```
### `generateViewport` function
#### Regular function
```tsx
import type { Viewport } from 'next'
export function generateViewport(): Viewport {
return {
themeColor: 'black',
}
}
```
#### With segment props
```tsx
import type { Viewport } from 'next'
type Props = {
params: Promise<{ id: string }>
searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}
export function generateViewport({ params, searchParams }: Props): Viewport {
return {
themeColor: 'black',
}
}
export default function Page({ params, searchParams }: Props) {}
```
#### JavaScript Projects
For JavaScript projects, you can use JSDoc to add type safety.
```js
/** @type {import("next").Viewport} */
export const viewport = {
themeColor: 'black',
}
```
## Version History
| Version | Changes |
| --------- | --------------------------------------------- |
| `v14.0.0` | `viewport` and `generateViewport` introduced. |