docs: accuracy and pappercuts (#94299)

Exhaustive pass over the guides and getting started using audit-docs
skill.
This commit is contained in:
Joseph
2026-06-01 22:28:44 +02:00
committed by GitHub
parent d897911814
commit bc384860d5
17 changed files with 91 additions and 57 deletions
@@ -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(
(
+5 -5
View File
@@ -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.
+20 -8
View File
@@ -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,
+1 -1
View File
@@ -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.
+18 -12
View File
@@ -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.
+1 -1
View File
@@ -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
+1
View File
@@ -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
},
},
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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} */
+5 -5
View File
@@ -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:
+1 -1
View File
@@ -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() {
-1
View File
@@ -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