mirror of
https://github.com/hanzoai/next.js.git
synced 2026-09-24 17:00:51 +00:00
docs: accuracy and pappercuts (#94299)
Exhaustive pass over the guides and getting started using audit-docs skill.
This commit is contained in:
@@ -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:
|
||||
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
```
|
||||
@@ -181,7 +187,8 @@ 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,
|
||||
@@ -189,7 +196,8 @@ export async function generateMetadata({ params }) {
|
||||
}
|
||||
|
||||
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>
|
||||
}
|
||||
```
|
||||
@@ -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(
|
||||
(
|
||||
@@ -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(
|
||||
(
|
||||
|
||||
@@ -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,
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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 {
|
||||
@@ -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 {
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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', {
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
...
|
||||
}
|
||||
```
|
||||
@@ -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
|
||||
@@ -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} />
|
||||
}
|
||||
@@ -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 />
|
||||
@@ -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) {
|
||||
@@ -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 />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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} */
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -367,6 +367,7 @@ Options to pass to the `<GoogleAnalytics>` component.
|
||||
| --------------- | -------- | ------------------------------------------------------------------------------------------------------ |
|
||||
| `gaId` | Required | Your [measurement ID](https://support.google.com/analytics/answer/12270356). Usually starts with `G-`. |
|
||||
| `dataLayerName` | Optional | Name of the data layer. Defaults to `dataLayer`. |
|
||||
| `debugMode` | Optional | Enable Google Analytics [debug mode](https://support.google.com/analytics/answer/7201382). |
|
||||
| `nonce` | Optional | A [nonce](/docs/app/guides/content-security-policy#nonces). |
|
||||
|
||||
### Google Maps Embed
|
||||
|
||||
@@ -374,7 +374,7 @@ export async function generateMetadata(props) {
|
||||
const query = searchParams.query
|
||||
}
|
||||
|
||||
export async function Page(props) {
|
||||
export default async function Page(props) {
|
||||
const params = await props.params
|
||||
const searchParams = await props.searchParams
|
||||
const slug = params.slug
|
||||
|
||||
Reference in New Issue
Block a user