Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions docs/01-app/01-getting-started/09-revalidating.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,15 @@ export async function getProducts() {

`cacheLife` accepts a profile name or a custom configuration object:

| Profile | `stale` | `revalidate` | `expire` |
| --------- | ------- | ------------ | ----------- |
| `seconds` | 0 | 1s | 60s |
| `minutes` | 5m | 1m | 1h |
| `hours` | 5m | 1h | 1d |
| `days` | 5m | 1d | 1w |
| `weeks` | 5m | 1w | 30d |
| `max` | 5m | 30d | ~indefinite |
| Profile | `stale` | `revalidate` | `expire` |
| --------- | ------- | ------------ | -------- |
| `default` | 5m | 15m | never |
| `seconds` | 30s | 1s | 60s |
| `minutes` | 5m | 1m | 1h |
| `hours` | 5m | 1h | 1d |
| `days` | 5m | 1d | 1w |
| `weeks` | 5m | 1w | 30d |
| `max` | 5m | 30d | 1y |

For fine-grained control, pass an object:

Expand Down
32 changes: 23 additions & 9 deletions docs/01-app/01-getting-started/14-metadata-and-og-images.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -162,17 +162,23 @@ import { getPost } from '@/app/lib/data'
export async function generateMetadata({
params,
}: {
params: { slug: string }
params: Promise<{ slug: string }>
}) {
const post = await getPost(params.slug)
const { slug } = await params
const post = await getPost(slug)
return {
title: post.title,
description: post.description,
}
}

export default async function Page({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug)
export default async function Page({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
const post = await getPost(slug)
return <div>{post.title}</div>
}
```
Expand All @@ -181,15 +187,17 @@ export default async function Page({ params }: { params: { slug: string } }) {
import { getPost } from '@/app/lib/data'

export async function generateMetadata({ params }) {
const post = await getPost(params.slug)
const { slug } = await params
const post = await getPost(slug)
return {
title: post.title,
description: post.description,
}
}

export default async function Page({ params }) {
const post = await getPost(params.slug)
const { slug } = await params
const post = await getPost(slug)
return <div>{post.title}</div>
}
```
Expand Down Expand Up @@ -264,8 +272,13 @@ export const size = {
export const contentType = 'image/png'

// Image generation
export default async function Image({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug)
export default async function Image({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
const post = await getPost(slug)

return new ImageResponse(
(
Expand Down Expand Up @@ -302,7 +315,8 @@ export const contentType = 'image/png'

// Image generation
export default async function Image({ params }) {
const post = await getPost(params.slug)
const { slug } = await params
const post = await getPost(slug)

return new ImageResponse(
(
Expand Down
10 changes: 5 additions & 5 deletions docs/01-app/02-guides/authentication.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -749,16 +749,16 @@ import { cookies } from 'next/headers'
import { decrypt } from '@/app/lib/session'

export async function updateSession() {
const session = (await cookies()).get('session')?.value
const cookieStore = await cookies()
const session = cookieStore.get('session')?.value
const payload = await decrypt(session)

if (!session || !payload) {
return null
}

const expires = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)(
await cookies()
).set('session', session, {
const expires = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
cookieStore.set('session', session, {
httpOnly: true,
secure: true,
expires: expires,
Expand Down Expand Up @@ -1120,7 +1120,7 @@ While Proxy can be useful for initial checks, it should not be your only line of

> **Tips**:
>
> - In Proxy, you can also read cookies using `req.cookies.get('session').value`.
> - In Proxy, you can also read cookies using `req.cookies.get('session')?.value`.
> - Proxy uses the Node.js runtime, check if your Auth library and session management library are compatible.
> - You can use the `matcher` property in the Proxy to specify which routes Proxy should run on. Although, for auth, it's recommended Proxy runs on all routes.

Expand Down
28 changes: 20 additions & 8 deletions docs/01-app/02-guides/backend-for-frontend.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -351,9 +351,9 @@ export async function POST(request: Request) {
try {
const clonedRequest = request.clone()

await request.body()
await clonedRequest.body()
await request.body() // Throws error
await request.text()
await clonedRequest.text()
await request.text() // Throws error

return new Response(null, { status: 204 })
} catch {
Expand All @@ -367,9 +367,9 @@ export async function POST(request) {
try {
const clonedRequest = request.clone()

await request.body()
await clonedRequest.body()
await request.body() // Throws error
await request.text()
await clonedRequest.text()
await request.text() // Throws error

return new Response(null, { status: 204 })
} catch {
Expand Down Expand Up @@ -612,7 +612,13 @@ export async function GET(request: NextRequest) {
const token = request.nextUrl.searchParams.get('session_token')
const redirectUrl = request.nextUrl.searchParams.get('redirect_url')

const response = NextResponse.redirect(new URL(redirectUrl, request.url))
const destination = new URL(redirectUrl ?? '/', request.url)
// Prevent open redirects: only allow same-origin destinations
if (destination.origin !== request.nextUrl.origin) {
return new Response('Invalid redirect', { status: 400 })
}

const response = NextResponse.redirect(destination)

response.cookies.set({
value: token,
Expand All @@ -634,7 +640,13 @@ export async function GET(request) {
const token = request.nextUrl.searchParams.get('session_token')
const redirectUrl = request.nextUrl.searchParams.get('redirect_url')

const response = NextResponse.redirect(new URL(redirectUrl, request.url))
const destination = new URL(redirectUrl ?? '/', request.url)
// Prevent open redirects: only allow same-origin destinations
if (destination.origin !== request.nextUrl.origin) {
return new Response('Invalid redirect', { status: 400 })
}

const response = NextResponse.redirect(destination)

response.cookies.set({
value: token,
Expand Down
2 changes: 1 addition & 1 deletion docs/01-app/02-guides/cdn-caching.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ CDNs that respect `s-maxage` and `stale-while-revalidate` can cache static and I

### Static assets

Static assets (JavaScript, CSS, images, fonts) served from `/_next/static/` include content hashes in their filenames and have a 1 year `max-age` and `immutable` directive: `public,max-age=31536000,immutable`
Static assets (JavaScript, CSS, images, fonts) served from `/_next/static/` include content hashes in their filenames and have a 1 year `max-age` and `immutable` directive: `public, max-age=31536000, immutable`

You can use [`assetPrefix`](/docs/app/api-reference/config/next-config-js/assetPrefix) to serve static assets from a different domain or CDN origin.

Expand Down
30 changes: 18 additions & 12 deletions docs/01-app/02-guides/data-security.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ You should follow a **Zero Trust** model when adopting Server Components in an e
import { cookies } from 'next/headers'

export default async function Page() {
const cookieStore = cookies()
const cookieStore = await cookies()
const token = cookieStore.get('AUTH_TOKEN')?.value

const res = await fetch('https://api.example.com/profile', {
Expand Down Expand Up @@ -73,7 +73,8 @@ import { cookies } from 'next/headers'
// Component to Server Component which minimizes risk of passing it to a Client
// Component.
export const getCurrentUser = cache(async () => {
const token = cookies().get('AUTH_TOKEN')
const cookieStore = await cookies()
const token = cookieStore.get('AUTH_TOKEN')
const decodedToken = await decryptAndValidate(token)
// Don't include secret tokens or private information as public fields.
// Use classes to avoid accidentally passing the whole object to the client.
Expand Down Expand Up @@ -116,12 +117,13 @@ export async function getProfileDTO(slug: string) {
```

```tsx filename="app/page.tsx"
import { getProfile } from '../../data/user'
import { getProfileDTO } from '../../data/user-dto'

export async function Page({ params: { slug } }) {
export default async function Page({ params }) {
const { slug } = await params
// This page can now safely pass around this profile knowing
// that it shouldn't contain anything sensitive.
const profile = await getProfile(slug);
const profile = await getProfileDTO(slug)
...
}
```
Expand All @@ -137,7 +139,8 @@ This approach, however, makes it easier to accidentally expose private data to t
```tsx filename="app/page.tsx"
import Profile from './components/profile.tsx'

export async function Page({ params: { slug } }) {
export default async function Page({ params }) {
const { slug } = await params
const [rows] = await sql`SELECT * FROM user WHERE slug = ${slug}`
const userData = rows[0]
// EXPOSED: This exposes all the fields in userData to the client because
Expand Down Expand Up @@ -184,10 +187,11 @@ import { getUser } from '../data/user'
import Profile from './ui/profile'

export default async function Page({
params: { slug },
params,
}: {
params: { slug: string }
params: Promise<{ slug: string }>
}) {
const { slug } = await params
const publicProfile = await getUser(slug)
return <Profile user={publicProfile} />
}
Expand Down Expand Up @@ -307,7 +311,7 @@ You should always validate input from client, as they can be easily modified. Fo
```tsx filename="app/page.tsx"
// BAD: Trusting searchParams directly
export default async function Page({ searchParams }) {
const isAdmin = searchParams.get('isAdmin')
const isAdmin = (await searchParams).isAdmin
if (isAdmin === 'true') {
// Vulnerable: relies on untrusted client data
return <AdminPanel />
Expand All @@ -319,7 +323,8 @@ import { cookies } from 'next/headers'
import { verifyAdmin } from './auth'

export default async function Page() {
const token = cookies().get('AUTH_TOKEN')
const cookieStore = await cookies()
const token = cookieStore.get('AUTH_TOKEN')
const isAdmin = await verifyAdmin(token)

if (isAdmin) {
Expand Down Expand Up @@ -565,8 +570,9 @@ Mutations (e.g. logging out users, updating databases, invalidating caches) shou
```tsx filename="app/page.tsx"
// BAD: Triggering a mutation during rendering
export default async function Page({ searchParams }) {
if (searchParams.get('logout')) {
cookies().delete('AUTH_TOKEN')
if ((await searchParams).logout) {
const cookieStore = await cookies()
cookieStore.delete('AUTH_TOKEN')
}

return <UserProfile />
Expand Down
4 changes: 2 additions & 2 deletions docs/01-app/02-guides/how-revalidation-works.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ Soft tags are automatically generated by Next.js based on the route path, prefix

Soft tags enable [`revalidatePath()`](/docs/app/api-reference/functions/revalidatePath) to work through the same tag-based system. When `revalidatePath('/blog/hello')` is called, it invalidates cache entries associated with that path's leaf route tag and its ancestor layout soft tags (for example `_N_T_/layout`, `_N_T_/blog/layout`, `_N_T_/blog/hello/layout`, and `_N_T_/blog/hello`).

In the [cache handler API](/docs/app/api-reference/config/next-config-js/cacheHandlers), soft tags are passed to the `get()` method as the `softTags` parameter. Your handler should check whether any soft tag has been invalidated after the cache entry's timestamp. The `getExpiration()` method returns the most recent revalidation timestamp across all provided tags, or `0` if none have been revalidated. Your handler should treat an entry as stale if the returned timestamp is newer than the entry's own timestamp. See the [cache handler API reference](/docs/app/api-reference/config/next-config-js/cacheHandlers#getexpiration) for the full semantics.
In the [cache handler API](/docs/app/api-reference/config/next-config-js/cacheHandlers), soft tags are passed to the `get()` method as the `softTags` parameter. Your handler should check whether any soft tag has been invalidated after the cache entry's timestamp. The `getExpiration()` method returns the most recent revalidation timestamp across all provided tags, or `0` if none have been revalidated. It can also return `Infinity` to signal that the soft tags should instead be passed to `get()` and checked for expiration there. Your handler should treat an entry as stale if the returned timestamp is newer than the entry's own timestamp. See the [cache handler API reference](/docs/app/api-reference/config/next-config-js/cacheHandlers#getexpiration) for the full semantics.

## Multi-Instance Considerations

Expand Down Expand Up @@ -89,7 +89,7 @@ If a CDN caches Next.js responses, it should respect the `Vary` header and the `
The revalidation system prioritizes availability over strict consistency. Content is always served, even when infrastructure guarantees cannot be fully met:

- **Cache write failure**: the response is still served to the user because writes are asynchronous. The cache entry is lost, and the next request triggers a fresh render.
- **Cache read failure**: your handler should catch internal errors and return `undefined` (the cache miss signal). The route is then server-rendered fresh. The framework does not wrap `get()` in a try/catch, so unhandled exceptions will propagate as render errors.
- **Cache read failure**: your handler should catch internal errors and return `undefined` (the cache miss signal). The route is then server-rendered fresh. A thrown error is not treated as a cache miss; it propagates as a render error, so always return `undefined` to signal a miss.
- **HTML/RSC cache inconsistency**: if a CDN caches HTML and RSC responses with different TTLs or invalidation timing, users may see mismatched content during client-side navigation. Cache them together and respect the `Vary` header to avoid this.
- **Cross-deployment skew**: during rolling deployments, configure [`deploymentId`](/docs/app/api-reference/config/next-config-js/deploymentId) so that a build ID change triggers a hard navigation to fetch consistent content.

Expand Down
2 changes: 1 addition & 1 deletion docs/01-app/02-guides/instant-navigation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ test('product title appears instantly', async ({ page }) => {

Inside the `instant()` callback, only the static shell is visible. After the callback finishes, dynamic content streams in and you can assert on the full page.

There is no need to write an `instant()` test for every navigation. Use `instant()` for the user flows that matter most. In the future build-time `instant` validaiton will be available to cover a broader set of navigation cases.
There is no need to write an `instant()` test for every navigation. Use `instant()` for the user flows that matter most. In the future build-time `instant` validation will be available to cover a broader set of navigation cases.

## Fixing a navigation that blocks

Expand Down
1 change: 1 addition & 0 deletions docs/01-app/02-guides/mdx.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -810,6 +810,7 @@ module.exports = withMDX({
mdxRs: {
jsxRuntime?: string // Custom jsx runtime
jsxImportSource?: string // Custom jsx import source,
providerImportSource?: string // Module providing a `useMDXComponents` context
mdxType?: 'gfm' | 'commonmark' // Configure what kind of mdx syntax will be used to parse & transform
},
},
Expand Down
2 changes: 1 addition & 1 deletion docs/01-app/02-guides/multi-zones.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export async function proxy(request) {

## Linking between zones

Links to paths in a different zone should use an `a` tag instead of the Next.js [`<Link>`](/docs/pages/api-reference/components/link) component. This is because Next.js will try to prefetch and soft navigate to any relative path in `<Link>` component, which will not work across zones.
Links to paths in a different zone should use an `a` tag instead of the Next.js [`<Link>`](/docs/app/api-reference/components/link) component. This is because Next.js will try to prefetch and soft navigate to any relative path in `<Link>` component, which will not work across zones.

## Sharing code

Expand Down
2 changes: 1 addition & 1 deletion docs/01-app/02-guides/package-bundling.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ const nextConfig = {
module.exports = nextConfig
```

To automatically bundle all packages, you can use the [`bundlePagesRouterDependencies`](/docs/pages/api-reference/config/next-config-js/bundlePagesRouterDependencies) option in your `next.config.js`.
To automatically bundle all packages, you can use the [`bundlePagesRouterDependencies`](/docs/pages/api-reference/config/next-config-js/bundlePagesRouterDependencies) option in your `next.config.js`. This option defaults to `false`.

```js filename="next.config.js"
/** @type {import('next').NextConfig} */
Expand Down
10 changes: 5 additions & 5 deletions docs/01-app/02-guides/prefetching.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,10 @@ export default function NavLink() {
}
```

| **Context** | **Prefetched payload** | **Client Cache TTL** |
| ----------------- | -------------------------------- | ------------------------------------------------------------------------------ |
| No `loading.js` | Entire page | Until app reload |
| With `loading.js` | Layout to first loading boundary | 30s ([configurable](/docs/app/api-reference/config/next-config-js/staleTimes)) |
| **Context** | **Prefetched payload** | **Client Cache TTL** |
| ----------------- | -------------------------------- | ------------------------------------------------------------------------------------------------- |
| No `loading.js` | Entire page | 5 min ([`staleTimes.static`](/docs/app/api-reference/config/next-config-js/staleTimes)) |
| With `loading.js` | Layout to first loading boundary | Off by default ([`staleTimes.dynamic`](/docs/app/api-reference/config/next-config-js/staleTimes)) |

Automatic prefetching runs only in production. Disable with `prefetch={false}` or use the wrapper in [Disabled Prefetch](#disabled-prefetch).

Expand All @@ -78,7 +78,7 @@ export function PricingCard() {
}
```

If the intent is to prefetch a URL when a component loads, see the extending or rejecting a link [example].
To prefetch a URL when a component loads, see [Extending or ejecting link](#extending-or-ejecting-link).

## Hover-triggered prefetch

Expand Down
2 changes: 1 addition & 1 deletion docs/01-app/02-guides/public-static-pages.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ However, if this component is rendered at request time, fetching its data will d

Even though the header is rendered instantly, it can't be sent to the browser until the product list has finished fetching.

To protect us from this performance cliff, Next.js will show us a [warning](/docs/messages/blocking-route) the first time we **await** data: `Blocking data was accessed outside of Suspense`
To protect us from this performance cliff, the first time we **await** this uncached data Next.js shows a [warning](/docs/messages/blocking-route): accessing uncached data outside of `<Suspense>` prevents the route from being prerendered.

At this point, we have to decide how to **unblock** the response. Either:

Expand Down
2 changes: 1 addition & 1 deletion docs/01-app/02-guides/scripts.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ Although the `worker` strategy does not require any additional configuration to

If you would like to add additional configuration options, you can include it within the `<Head />` component used in a [custom `_document.js`](/docs/pages/building-your-application/routing/custom-document):

```jsx filename="_pages/document.jsx"
```jsx filename="pages/_document.js"
import { Html, Head, Main, NextScript } from 'next/document'

export default function Document() {
Expand Down
1 change: 0 additions & 1 deletion docs/01-app/02-guides/streaming.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,6 @@ Prefer explicit `<Suspense>` boundaries close to the dynamic access. When the pr

### Error handling mid-stream

{/* TODO: catchError semantics - not landed on stable yet */}
If a component throws an error after streaming has started, the nearest [`error.js`](/docs/app/api-reference/file-conventions/error) boundary catches it and renders the error UI in place of the failed component. The rest of the page remains intact, only the section that errored is replaced.

Because the HTTP status code (`200 OK`) has already been sent with the first chunk, it cannot be changed to a `4xx` or `5xx`. The error is handled entirely within the streamed HTML. See [The HTTP contract](#the-http-contract) for more on this constraint.
Expand Down
Loading
Loading