mirror of
https://github.com/hanzoai/next.js.git
synced 2026-10-30 14:46:38 +00:00
<!-- 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> <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> </p> --------- Co-authored-by: Cursor Agent <[email protected]> Co-authored-by: Joseph <[email protected]>
286 lines
8.1 KiB
Plaintext
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. |
|