next 16.2.8 → 16.2.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/dist/.build-commit +1 -1
  2. package/dist/bin/next +2 -2
  3. package/dist/bin/next.map +0 -0
  4. package/dist/build/index.js +3 -3
  5. package/dist/build/swc/index.js +1 -1
  6. package/dist/build/turbopack-analyze/index.js +1 -1
  7. package/dist/build/turbopack-build/impl.js +1 -1
  8. package/dist/build/webpack-config.js +3 -3
  9. package/dist/bundle-analyzer/404.html +2 -2
  10. package/dist/bundle-analyzer/__next.__PAGE__.txt +1 -1
  11. package/dist/bundle-analyzer/__next._full.txt +1 -1
  12. package/dist/bundle-analyzer/__next._head.txt +1 -1
  13. package/dist/bundle-analyzer/__next._index.txt +1 -1
  14. package/dist/bundle-analyzer/__next._tree.txt +1 -1
  15. package/dist/bundle-analyzer/_not-found/__next._full.txt +1 -1
  16. package/dist/bundle-analyzer/_not-found/__next._head.txt +1 -1
  17. package/dist/bundle-analyzer/_not-found/__next._index.txt +1 -1
  18. package/dist/bundle-analyzer/_not-found/__next._not-found.__PAGE__.txt +1 -1
  19. package/dist/bundle-analyzer/_not-found/__next._not-found.txt +1 -1
  20. package/dist/bundle-analyzer/_not-found/__next._tree.txt +1 -1
  21. package/dist/bundle-analyzer/_not-found.html +2 -2
  22. package/dist/bundle-analyzer/_not-found.txt +1 -1
  23. package/dist/bundle-analyzer/index.html +2 -2
  24. package/dist/bundle-analyzer/index.txt +1 -1
  25. package/dist/client/app-bootstrap.js +1 -1
  26. package/dist/client/index.js +1 -1
  27. package/dist/compiled/next-server/pages-api-turbo.runtime.dev.js +1 -1
  28. package/dist/compiled/next-server/pages-api-turbo.runtime.dev.js.map +1 -1
  29. package/dist/compiled/next-server/pages-api.runtime.dev.js +1 -1
  30. package/dist/compiled/next-server/pages-api.runtime.dev.js.map +1 -1
  31. package/dist/compiled/next-server/pages-api.runtime.prod.js +1 -1
  32. package/dist/compiled/next-server/pages-api.runtime.prod.js.map +1 -1
  33. package/dist/compiled/next-server/pages-turbo.runtime.dev.js +1 -1
  34. package/dist/compiled/next-server/pages-turbo.runtime.dev.js.map +1 -1
  35. package/dist/compiled/next-server/pages.runtime.dev.js +1 -1
  36. package/dist/compiled/next-server/pages.runtime.dev.js.map +1 -1
  37. package/dist/compiled/next-server/pages.runtime.prod.js +1 -1
  38. package/dist/compiled/next-server/pages.runtime.prod.js.map +1 -1
  39. package/dist/compiled/next-server/server.runtime.prod.js +1 -1
  40. package/dist/compiled/next-server/server.runtime.prod.js.map +1 -1
  41. package/dist/docs/01-app/01-getting-started/02-project-structure.md +1 -1
  42. package/dist/docs/01-app/01-getting-started/03-layouts-and-pages.md +6 -4
  43. package/dist/docs/01-app/01-getting-started/04-linking-and-navigating.md +1 -1
  44. package/dist/docs/01-app/01-getting-started/05-server-and-client-components.md +1 -1
  45. package/dist/docs/01-app/01-getting-started/07-mutating-data.md +4 -3
  46. package/dist/docs/01-app/01-getting-started/09-revalidating.md +9 -8
  47. package/dist/docs/01-app/01-getting-started/13-fonts.md +1 -1
  48. package/dist/docs/01-app/01-getting-started/14-metadata-and-og-images.md +23 -9
  49. package/dist/docs/01-app/01-getting-started/17-deploying.md +1 -1
  50. package/dist/docs/01-app/02-guides/authentication.md +5 -5
  51. package/dist/docs/01-app/02-guides/backend-for-frontend.md +20 -8
  52. package/dist/docs/01-app/02-guides/cdn-caching.md +1 -1
  53. package/dist/docs/01-app/02-guides/custom-server.md +1 -1
  54. package/dist/docs/01-app/02-guides/data-security.md +19 -12
  55. package/dist/docs/01-app/02-guides/draft-mode.md +168 -105
  56. package/dist/docs/01-app/02-guides/forms.md +1 -1
  57. package/dist/docs/01-app/02-guides/how-revalidation-works.md +2 -2
  58. package/dist/docs/01-app/02-guides/mdx.md +1 -0
  59. package/dist/docs/01-app/02-guides/migrating-to-cache-components.md +533 -3
  60. package/dist/docs/01-app/02-guides/multi-zones.md +1 -1
  61. package/dist/docs/01-app/02-guides/package-bundling.md +1 -1
  62. package/dist/docs/01-app/02-guides/prefetching.md +5 -5
  63. package/dist/docs/01-app/02-guides/preventing-flash-before-hydration.md +3 -3
  64. package/dist/docs/01-app/02-guides/public-static-pages.md +1 -1
  65. package/dist/docs/01-app/02-guides/scripts.md +1 -1
  66. package/dist/docs/01-app/02-guides/server-actions.md +180 -0
  67. package/dist/docs/01-app/02-guides/streaming.md +0 -1
  68. package/dist/docs/01-app/02-guides/third-party-libraries.md +1 -0
  69. package/dist/docs/01-app/02-guides/upgrading/version-15.md +1 -1
  70. package/dist/docs/01-app/03-api-reference/01-directives/use-server.md +2 -0
  71. package/dist/docs/01-app/03-api-reference/03-file-conventions/dynamic-routes.md +4 -2
  72. package/dist/docs/01-app/03-api-reference/03-file-conventions/instrumentation.md +18 -4
  73. package/dist/docs/01-app/03-api-reference/05-config/01-next-config-js/rewrites.md +8 -6
  74. package/dist/docs/01-app/03-api-reference/05-config/01-next-config-js/serverActions.md +2 -0
  75. package/dist/docs/01-app/03-api-reference/05-config/01-next-config-js/transpilePackages.md +15 -3
  76. package/dist/docs/01-app/03-api-reference/05-config/02-typescript.md +1 -1
  77. package/dist/docs/01-app/04-glossary.md +1 -1
  78. package/dist/docs/02-pages/02-guides/upgrading/version-12.md +1 -1
  79. package/dist/docs/04-community/01-contribution-guide.md +2 -2
  80. package/dist/docs/index.md +0 -2
  81. package/dist/esm/build/index.js +3 -3
  82. package/dist/esm/build/swc/index.js +1 -1
  83. package/dist/esm/build/turbopack-analyze/index.js +1 -1
  84. package/dist/esm/build/turbopack-build/impl.js +1 -1
  85. package/dist/esm/build/webpack-config.js +3 -3
  86. package/dist/esm/client/app-bootstrap.js +1 -1
  87. package/dist/esm/client/index.js +1 -1
  88. package/dist/esm/lib/patch-incorrect-lockfile.js +3 -3
  89. package/dist/esm/server/config.js +1 -1
  90. package/dist/esm/server/dev/hot-reloader-turbopack.js +2 -2
  91. package/dist/esm/server/dev/hot-reloader-webpack.js +1 -1
  92. package/dist/esm/server/lib/app-info-log.js +1 -1
  93. package/dist/esm/server/lib/start-server.js +1 -1
  94. package/dist/esm/shared/lib/errors/canary-only-config-error.js +1 -1
  95. package/dist/lib/patch-incorrect-lockfile.js +3 -3
  96. package/dist/server/config.js +1 -1
  97. package/dist/server/dev/hot-reloader-turbopack.js +2 -2
  98. package/dist/server/dev/hot-reloader-webpack.js +1 -1
  99. package/dist/server/lib/app-info-log.js +1 -1
  100. package/dist/server/lib/start-server.js +1 -1
  101. package/dist/shared/lib/errors/canary-only-config-error.js +1 -1
  102. package/dist/telemetry/anonymous-meta.js +1 -1
  103. package/dist/telemetry/events/session-stopped.js +2 -2
  104. package/dist/telemetry/events/swc-load-failure.js +1 -1
  105. package/dist/telemetry/events/version.js +2 -2
  106. package/package.json +10 -252
  107. /package/dist/bundle-analyzer/_next/static/{ShlsHWUU3q2GhICNPxAIf → -wnBUGOTjuvnrsp1fJxyv}/_buildManifest.js +0 -0
  108. /package/dist/bundle-analyzer/_next/static/{ShlsHWUU3q2GhICNPxAIf → -wnBUGOTjuvnrsp1fJxyv}/_clientMiddlewareManifest.json +0 -0
  109. /package/dist/bundle-analyzer/_next/static/{ShlsHWUU3q2GhICNPxAIf → -wnBUGOTjuvnrsp1fJxyv}/_ssgManifest.js +0 -0
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  title: How to preview content with Draft Mode in Next.js
3
3
  nav_title: Draft Mode
4
- description: Next.js has draft mode to toggle between static and dynamic pages. You can learn how it works with App Router here.
4
+ description: Bypass Next.js caching for a request so editors can preview unpublished content from a headless CMS.
5
5
  related:
6
6
  title: Next Steps
7
7
  description: See the API reference for more information on how to use Draft Mode.
@@ -9,27 +9,46 @@ related:
9
9
  - app/api-reference/functions/draft-mode
10
10
  ---
11
11
 
12
- **Draft Mode** allows you to preview draft content from your headless CMS in your Next.js application. This is useful for static pages that are generated at build time as it allows you to switch to [dynamic rendering](/docs/app/glossary#dynamic-rendering) and see the draft changes without having to rebuild your entire site.
12
+ **Draft Mode** lets editors see how draft or in-progress content will render on your site, without waiting for revalidation. While an editor is in Draft Mode, cached or pre-rendered content is bypassed, and fetched from upstream sources directly. Other visitors continue to see the cached or pre-rendered version of the page.
13
13
 
14
- This page walks through how to enable and use Draft Mode.
14
+ Your data-fetching code does not need to change if your CMS serves draft and published content from the same URL. Otherwise, see [When your CMS uses a separate draft endpoint](#when-your-cms-uses-a-separate-draft-endpoint).
15
15
 
16
- ## Step 1: Create a Route Handler
16
+ ## What Draft Mode does
17
17
 
18
- Create a [Route Handler](/docs/app/api-reference/file-conventions/route). It can have any name, for example, `app/api/draft/route.ts`.
18
+ When Draft Mode is enabled for a request:
19
19
 
20
- ```ts filename="app/api/draft/route.ts" switcher
21
- export async function GET(request: Request) {
22
- return new Response('')
23
- }
24
- ```
20
+ - `fetch()` calls skip the Next.js fetch cache and hit the network directly.
21
+ - Components and functions inside [`'use cache'`](/docs/app/api-reference/directives/use-cache) re-execute on every request, and their results are not saved to the cache.
22
+ - [`unstable_cache`](/docs/app/api-reference/functions/unstable_cache) reads and writes are bypassed in the same way.
23
+ - The page is excluded from the ISR response cache and is served with `Cache-Control: private, no-cache, no-store, max-age=0, must-revalidate`.
25
24
 
26
- ```js filename="app/api/draft/route.js" switcher
27
- export async function GET() {
28
- return new Response('')
29
- }
30
- ```
25
+ The effect applies whether the page is statically generated, served from cache, or revalidated through ISR.
26
+
27
+ ## What this guide covers
28
+
29
+ This guide assumes:
30
+
31
+ - Your headless CMS supports configurable preview URLs (most do).
32
+ - The CMS opens a URL like `/api/draft?secret=XXX&slug=/posts/foo` in a new tab when an editor clicks "Preview". The secret is a shared token; the slug is the path to preview.
33
+ - Your Next.js app validates the secret, enables Draft Mode, and redirects to the slug.
31
34
 
32
- Then, import the [`draftMode`](/docs/app/api-reference/functions/draft-mode) function and call the `enable()` method.
35
+ With that contract in mind, the rest of this guide walks through:
36
+
37
+ 1. Creating a Route Handler that enables Draft Mode by setting a cookie.
38
+ 2. Securing that handler with the shared secret and slug from the CMS.
39
+ 3. Rendering pages that read the latest draft.
40
+ 4. Showing a preview banner with an exit form.
41
+
42
+ Then, depending on your setup:
43
+
44
+ - [Draft Mode with Cache Components](#draft-mode-with-cache-components) for surfacing the preview state from a `'use cache'` boundary.
45
+ - [When your CMS uses a separate draft endpoint](#when-your-cms-uses-a-separate-draft-endpoint) for branching the fetch URL on `isEnabled`.
46
+
47
+ > **Good to know:** `GET` is meant to be a safe, read-only method. Operations that affect future requests, like enabling Draft Mode via a cookie, should use `POST`. The entry handler uses `GET` because we're assuming a CMS preview integration: the CMS opens the URL in a new browser tab, which is a `GET` request. The exit flow in Step 4 uses `POST` (via [Server Action](/docs/app/getting-started/mutating-data) or `POST` Route Handler).
48
+
49
+ ## Step 1: Create a Route Handler
50
+
51
+ Create a [Route Handler](/docs/app/api-reference/file-conventions/route) that sets the Draft Mode cookie. It can have any name, for example, `app/api/draft/route.ts`.
33
52
 
34
53
  ```ts filename="app/api/draft/route.ts" switcher
35
54
  import { draftMode } from 'next/headers'
@@ -51,17 +70,19 @@ export async function GET(request) {
51
70
  }
52
71
  ```
53
72
 
54
- This will set a **cookie** to enable draft mode. Subsequent requests containing this cookie will trigger draft mode and change the behavior of statically generated pages.
73
+ `draft.enable()` sets a cookie named `__prerender_bypass`. Subsequent requests that carry this cookie skip every cache layer listed above.
74
+
75
+ You can test this manually by visiting `/api/draft` and looking at your browser's developer tools. Notice the `Set-Cookie` response header.
55
76
 
56
- You can test this manually by visiting `/api/draft` and looking at your browser’s developer tools. Notice the `Set-Cookie` response header with a cookie named `__prerender_bypass`.
77
+ As written, the handler is public: anyone who hits `/api/draft` enables Draft Mode for themselves. Step 2 closes that with a shared secret so only your CMS can call it.
57
78
 
58
- ## Step 2: Access the Route Handler from your Headless CMS
79
+ ## Step 2: Access the Route Handler from your headless CMS
59
80
 
60
- > These steps assume that the headless CMS youre using supports setting **custom draft URLs**. If it doesnt, you can still use this method to secure your draft URLs, but youll need to construct and access the draft URL manually. The specific steps will vary depending on which headless CMS youre using.
81
+ > These steps assume that the headless CMS you're using supports setting **custom draft URLs**. If it doesn't, you can still use this method to secure your draft URLs, but you'll need to construct and access the draft URL manually. The specific steps will vary depending on which headless CMS you're using.
61
82
 
62
83
  To securely access the Route Handler from your headless CMS:
63
84
 
64
- 1. Create a **secret token string** using a token generator of your choice. This secret will only be known by your Next.js app and your headless CMS.
85
+ 1. Create a **secret token string** using a token generator of your choice. This secret is only known to your Next.js app and your headless CMS.
65
86
  2. If your headless CMS supports setting custom draft URLs, specify a draft URL (this assumes that your Route Handler is located at `app/api/draft/route.ts`). For example:
66
87
 
67
88
  ```bash filename="Terminal"
@@ -72,41 +93,35 @@ https://<your-site>/api/draft?secret=<token>&slug=<path>
72
93
  > - `<token>` should be replaced with the secret token you generated.
73
94
  > - `<path>` should be the path for the page that you want to view. If you want to view `/posts/one`, then you should use `&slug=/posts/one`.
74
95
  >
75
- > Your headless CMS might allow you to include a variable in the draft URL so that `<path>` can be set dynamically based on the CMSs data like so: `&slug=/posts/{entry.fields.slug}`
96
+ > Your headless CMS might allow you to include a variable in the draft URL so that `<path>` can be set dynamically based on the CMS's data like so: `&slug=/posts/{entry.fields.slug}`
76
97
 
77
- 3. In your Route Handler, check that the secret matches and that the `slug` parameter exists (if not, the request should fail), call `draftMode.enable()` to set the cookie. Then, redirect the browser to the path specified by `slug`:
98
+ 3. In your Route Handler, check that the secret matches and that the `slug` parameter exists (if not, the request should fail), call `draft.enable()` to set the cookie, then redirect the browser to the path specified by `slug`:
78
99
 
79
100
  ```ts filename="app/api/draft/route.ts" switcher
80
101
  import { draftMode } from 'next/headers'
81
102
  import { redirect } from 'next/navigation'
82
103
 
83
104
  export async function GET(request: Request) {
84
- // Parse query string parameters
85
105
  const { searchParams } = new URL(request.url)
86
106
  const secret = searchParams.get('secret')
87
107
  const slug = searchParams.get('slug')
88
108
 
89
- // Check the secret and next parameters
90
109
  // This secret should only be known to this Route Handler and the CMS
91
110
  if (secret !== 'MY_SECRET_TOKEN' || !slug) {
92
111
  return new Response('Invalid token', { status: 401 })
93
112
  }
94
113
 
95
- // Fetch the headless CMS to check if the provided `slug` exists
96
- // getPostBySlug would implement the required fetching logic to the headless CMS
114
+ // Verify the slug exists in the CMS before enabling Draft Mode
97
115
  const post = await getPostBySlug(slug)
98
-
99
- // If the slug doesn't exist prevent draft mode from being enabled
100
116
  if (!post) {
101
117
  return new Response('Invalid slug', { status: 401 })
102
118
  }
103
119
 
104
- // Enable Draft Mode by setting the cookie
105
120
  const draft = await draftMode()
106
121
  draft.enable()
107
122
 
108
- // Redirect to the path from the fetched post
109
- // We don't redirect to searchParams.slug as that might lead to open redirect vulnerabilities
123
+ // Redirect to the path from the fetched post, not from searchParams,
124
+ // to avoid open redirect vulnerabilities
110
125
  redirect(post.slug)
111
126
  }
112
127
  ```
@@ -116,132 +131,152 @@ import { draftMode } from 'next/headers'
116
131
  import { redirect } from 'next/navigation'
117
132
 
118
133
  export async function GET(request) {
119
- // Parse query string parameters
120
134
  const { searchParams } = new URL(request.url)
121
135
  const secret = searchParams.get('secret')
122
136
  const slug = searchParams.get('slug')
123
137
 
124
- // Check the secret and next parameters
125
- // This secret should only be known to this Route Handler and the CMS
126
138
  if (secret !== 'MY_SECRET_TOKEN' || !slug) {
127
139
  return new Response('Invalid token', { status: 401 })
128
140
  }
129
141
 
130
- // Fetch the headless CMS to check if the provided `slug` exists
131
- // getPostBySlug would implement the required fetching logic to the headless CMS
132
142
  const post = await getPostBySlug(slug)
133
-
134
- // If the slug doesn't exist prevent draft mode from being enabled
135
143
  if (!post) {
136
144
  return new Response('Invalid slug', { status: 401 })
137
145
  }
138
146
 
139
- // Enable Draft Mode by setting the cookie
140
147
  const draft = await draftMode()
141
148
  draft.enable()
142
149
 
143
- // Redirect to the path from the fetched post
144
- // We don't redirect to searchParams.slug as that might lead to open redirect vulnerabilities
145
150
  redirect(post.slug)
146
151
  }
147
152
  ```
148
153
 
149
- If it succeeds, then the browser will be redirected to the path you want to view with the draft mode cookie.
150
-
151
- ## Step 3: Preview the Draft Content
152
-
153
- The next step is to update your page to check the value of `draftMode().isEnabled`.
154
+ If it succeeds, the browser is redirected to the target path with the Draft Mode cookie set.
154
155
 
155
- If you request a page which has the cookie set, then data will be fetched at **request time** (instead of at build time).
156
+ ## Step 3: Preview the draft content
156
157
 
157
- Furthermore, the value of `isEnabled` will be `true`.
158
+ Because Draft Mode bypasses the cache automatically, your page does not need to know whether Draft Mode is on to receive fresh content. Fetch as you normally would:
158
159
 
159
- ```tsx filename="app/page.tsx" switcher
160
- // page that fetches data
161
- import { draftMode } from 'next/headers'
162
-
163
- async function getData() {
164
- const { isEnabled } = await draftMode()
160
+ ```tsx filename="app/posts/[slug]/page.tsx" switcher
161
+ async function getPost(slug: string) {
162
+ const res = await fetch(`https://cms.example.com/posts/${slug}`)
163
+ return res.json()
164
+ }
165
165
 
166
- const url = isEnabled
167
- ? 'https://draft.example.com'
168
- : 'https://production.example.com'
166
+ export default async function Page({ params }: PageProps<'/posts/[slug]'>) {
167
+ const { slug } = await params
168
+ const post = await getPost(slug)
169
169
 
170
- const res = await fetch(url)
170
+ return (
171
+ <main>
172
+ <h1>{post.title}</h1>
173
+ <article>{post.content}</article>
174
+ </main>
175
+ )
176
+ }
177
+ ```
171
178
 
179
+ ```jsx filename="app/posts/[slug]/page.js" switcher
180
+ async function getPost(slug) {
181
+ const res = await fetch(`https://cms.example.com/posts/${slug}`)
172
182
  return res.json()
173
183
  }
174
184
 
175
- export default async function Page() {
176
- const { title, desc } = await getData()
185
+ export default async function Page({ params }) {
186
+ const { slug } = await params
187
+ const post = await getPost(slug)
177
188
 
178
189
  return (
179
190
  <main>
180
- <h1>{title}</h1>
181
- <p>{desc}</p>
191
+ <h1>{post.title}</h1>
192
+ <article>{post.content}</article>
182
193
  </main>
183
194
  )
184
195
  }
185
196
  ```
186
197
 
187
- ```jsx filename="app/page.js" switcher
188
- // page that fetches data
189
- import { draftMode } from 'next/headers'
198
+ When the Draft Mode cookie is present, the `fetch` above skips the Next.js fetch cache and hits your CMS for the current draft. When it is not, the same request can be served from cache as usual.
190
199
 
191
- async function getData() {
192
- const { isEnabled } = await draftMode()
200
+ If your CMS uses a different URL for drafts rather than serving them from the same endpoint, see [When your CMS uses a separate draft endpoint](#when-your-cms-uses-a-separate-draft-endpoint).
193
201
 
194
- const url = isEnabled
195
- ? 'https://draft.example.com'
196
- : 'https://production.example.com'
202
+ ## Step 4: Show a preview indicator
197
203
 
198
- const res = await fetch(url)
204
+ `isEnabled` is most useful as a signal to the editor: a banner that confirms they are looking at draft content, plus a way to exit. Render an indicator from your root layout so it appears on every preview page.
199
205
 
200
- return res.json()
206
+ ```tsx filename="app/preview-banner.tsx" switcher
207
+ import { draftMode } from 'next/headers'
208
+ import { redirect } from 'next/navigation'
209
+
210
+ async function exitPreview() {
211
+ 'use server'
212
+ const draft = await draftMode()
213
+ draft.disable()
214
+ redirect('/')
201
215
  }
202
216
 
203
- export default async function Page() {
204
- const { title, desc } = await getData()
217
+ export async function PreviewBanner() {
218
+ const { isEnabled } = await draftMode()
219
+ if (!isEnabled) return null
205
220
 
206
221
  return (
207
- <main>
208
- <h1>{title}</h1>
209
- <p>{desc}</p>
210
- </main>
222
+ <aside role="status">
223
+ Preview mode is on.{' '}
224
+ <form action={exitPreview}>
225
+ <button type="submit">Exit preview</button>
226
+ </form>
227
+ </aside>
211
228
  )
212
229
  }
213
230
  ```
214
231
 
215
- If you access the draft Route Handler (with `secret` and `slug`) from your headless CMS or manually using the URL, you should now be able to see the draft content. And, if you update your draft without publishing, you should be able to view the draft.
232
+ ```jsx filename="app/preview-banner.js" switcher
233
+ import { draftMode } from 'next/headers'
234
+ import { redirect } from 'next/navigation'
216
235
 
217
- ## Draft Mode with Cache Components
236
+ async function exitPreview() {
237
+ 'use server'
238
+ const draft = await draftMode()
239
+ draft.disable()
240
+ redirect('/')
241
+ }
218
242
 
219
- When using [caching directives](/docs/app/api-reference/directives/use-cache), Draft Mode forces all cached functions and components to produce fresh results on every request. You do not need to add any special handling to your caching code.
243
+ export async function PreviewBanner() {
244
+ const { isEnabled } = await draftMode()
245
+ if (!isEnabled) return null
220
246
 
221
- When Draft Mode is enabled:
247
+ return (
248
+ <aside role="status">
249
+ Preview mode is on.{' '}
250
+ <form action={exitPreview}>
251
+ <button type="submit">Exit preview</button>
252
+ </form>
253
+ </aside>
254
+ )
255
+ }
256
+ ```
257
+
258
+ Exiting Draft Mode also works with a `GET` Route Handler, but a `POST` is semantically more correct, for example via a form submitted through a [Server Action](/docs/app/getting-started/mutating-data) or to a `POST` Route Handler.
222
259
 
223
- - All functions and components under a caching directive scope re-execute on every request instead of serving from cache.
224
- - Results are not saved to the cache, so draft requests do not pollute cached content.
225
- - `fetch()` calls inside cached scopes use the original `fetch` implementation directly, without the Next.js fetch cache.
226
- - Response headers are set to `private, no-cache, no-store, max-age=0, must-revalidate`.
260
+ If you do use a `GET` Route Handler, trigger it from a `<form method="GET">` rather than a [`<Link>`](/docs/app/api-reference/components/link). Next.js prefetches `<Link>` components by default, which would clear the cookie before the editor clicks. Forms are not prefetched, regardless of method.
227
261
 
228
- You can read if Draft Mode is enabled inside a caching directive scope, so you can conditionally fetch draft or published content:
262
+ ## Draft Mode with Cache Components
263
+
264
+ You can read `isEnabled` inside a [`'use cache'`](/docs/app/api-reference/directives/use-cache) scope to render a preview indicator from a cached component. The cache bypass still applies, so the component re-executes with fresh data on every draft request.
229
265
 
230
- ```tsx filename="app/post/[slug]/page.tsx" switcher
266
+ ```tsx filename="app/posts/[slug]/page.tsx" switcher
231
267
  import { draftMode } from 'next/headers'
232
268
 
233
269
  async function Post({ slug }: { slug: string }) {
234
270
  'use cache'
235
271
 
272
+ const post = await fetch(`https://cms.example.com/posts/${slug}`).then((r) =>
273
+ r.json()
274
+ )
236
275
  const { isEnabled } = await draftMode()
237
276
 
238
- // Fetch draft or published content based on draft mode status
239
- const post = isEnabled
240
- ? await fetchDraftPost(slug)
241
- : await fetchPublishedPost(slug)
242
-
243
277
  return (
244
278
  <article>
279
+ {isEnabled && <p role="status">Draft preview</p>}
245
280
  <h1>{post.title}</h1>
246
281
  <div>{post.content}</div>
247
282
  </article>
@@ -249,21 +284,20 @@ async function Post({ slug }: { slug: string }) {
249
284
  }
250
285
  ```
251
286
 
252
- ```jsx filename="app/post/[slug]/page.js" switcher
287
+ ```jsx filename="app/posts/[slug]/page.js" switcher
253
288
  import { draftMode } from 'next/headers'
254
289
 
255
290
  async function Post({ slug }) {
256
291
  'use cache'
257
292
 
293
+ const post = await fetch(`https://cms.example.com/posts/${slug}`).then((r) =>
294
+ r.json()
295
+ )
258
296
  const { isEnabled } = await draftMode()
259
297
 
260
- // Fetch draft or published content based on draft mode status
261
- const post = isEnabled
262
- ? await fetchDraftPost(slug)
263
- : await fetchPublishedPost(slug)
264
-
265
298
  return (
266
299
  <article>
300
+ {isEnabled && <p role="status">Draft preview</p>}
267
301
  <h1>{post.title}</h1>
268
302
  <div>{post.content}</div>
269
303
  </article>
@@ -271,9 +305,38 @@ async function Post({ slug }) {
271
305
  }
272
306
  ```
273
307
 
274
- > **Good to know:**
275
- >
276
- > - You can await the `draftMode()` promise inside a caching directive scope to read `isEnabled`, but other runtime APIs like `cookies()` and `headers()` are not allowed and will throw an error, even when Draft Mode is active. (The [`"use cache: private"`](/docs/app/api-reference/directives/use-cache-private) directive does allow access to `cookies()` and `headers()`.)
277
- > - You cannot call `draftMode().enable()` or `draftMode().disable()` inside a caching directive scope. Draft Mode can only be toggled in [Route Handlers](/docs/app/api-reference/file-conventions/route) or [Server Actions](/docs/app/getting-started/mutating-data).
308
+ > **Good to know:** `draftMode().enable()` and `draftMode().disable()` cannot be called inside a caching directive scope; toggle Draft Mode from a [Route Handler](/docs/app/api-reference/file-conventions/route) or [Server Action](/docs/app/getting-started/mutating-data) instead.
309
+
310
+ ## When your CMS uses a separate draft endpoint
311
+
312
+ If your CMS exposes draft content at a different URL or requires different credentials, branch your fetch on `isEnabled`:
313
+
314
+ ```tsx filename="app/posts/[slug]/page.tsx" switcher
315
+ import { draftMode } from 'next/headers'
316
+
317
+ async function getPost(slug: string) {
318
+ const { isEnabled } = await draftMode()
319
+ const baseUrl = isEnabled
320
+ ? 'https://cms.example.com/preview'
321
+ : 'https://cms.example.com/published'
322
+
323
+ const res = await fetch(`${baseUrl}/posts/${slug}`)
324
+ return res.json()
325
+ }
326
+ ```
327
+
328
+ ```jsx filename="app/posts/[slug]/page.js" switcher
329
+ import { draftMode } from 'next/headers'
330
+
331
+ async function getPost(slug) {
332
+ const { isEnabled } = await draftMode()
333
+ const baseUrl = isEnabled
334
+ ? 'https://cms.example.com/preview'
335
+ : 'https://cms.example.com/published'
336
+
337
+ const res = await fetch(`${baseUrl}/posts/${slug}`)
338
+ return res.json()
339
+ }
340
+ ```
278
341
 
279
- If your cached function needs values from `cookies()` or `headers()`, read them outside the caching directive scope and [pass them as arguments](/docs/app/getting-started/caching#passing-runtime-values-to-cached-functions).
342
+ The cache bypass still applies to both branches; the fork only chooses where to read from.
@@ -4,7 +4,7 @@ nav_title: Forms
4
4
  description: Learn how to create forms in Next.js with React Server Actions.
5
5
  ---
6
6
 
7
- React Server Actions are [Server Functions](https://react.dev/reference/rsc/server-functions) that execute on the server. They can be called in Server and Client Components to handle form submissions. This guide will walk you through how to create forms in Next.js with Server Actions.
7
+ React Server Actions are [Server Functions](https://react.dev/reference/rsc/server-functions) that execute on the server. They can be called in Server and Client Components to handle form submissions. This guide will walk you through how to create forms in Next.js with Server Actions. For Server Action behaviors beyond forms (single-roundtrip response, sequential dispatch, security, deployment), see [Server Actions and Mutations](/docs/app/guides/server-actions).
8
8
 
9
9
  > [!WARNING]
10
10
  > Always verify [authentication and authorization](/docs/app/guides/authentication) inside each Server Action, even if the form is only rendered on an authenticated page. See the [Data Security guide](/docs/app/guides/data-security) for more details.
@@ -50,7 +50,7 @@ Soft tags are automatically generated by Next.js based on the route path, prefix
50
50
 
51
51
  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`).
52
52
 
53
- 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.
53
+ 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.
54
54
 
55
55
  ## Multi-Instance Considerations
56
56
 
@@ -89,7 +89,7 @@ If a CDN caches Next.js responses, it should respect the `Vary` header and the `
89
89
  The revalidation system prioritizes availability over strict consistency. Content is always served, even when infrastructure guarantees cannot be fully met:
90
90
 
91
91
  - **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.
92
- - **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.
92
+ - **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.
93
93
  - **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.
94
94
  - **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.
95
95
 
@@ -810,6 +810,7 @@ module.exports = withMDX({
810
810
  mdxRs: {
811
811
  jsxRuntime?: string // Custom jsx runtime
812
812
  jsxImportSource?: string // Custom jsx import source,
813
+ providerImportSource?: string // Module providing a `useMDXComponents` context
813
814
  mdxType?: 'gfm' | 'commonmark' // Configure what kind of mdx syntax will be used to parse & transform
814
815
  },
815
816
  },