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.
- package/dist/.build-commit +1 -1
- package/dist/bin/next +2 -2
- package/dist/bin/next.map +0 -0
- package/dist/build/index.js +3 -3
- package/dist/build/swc/index.js +1 -1
- package/dist/build/turbopack-analyze/index.js +1 -1
- package/dist/build/turbopack-build/impl.js +1 -1
- package/dist/build/webpack-config.js +3 -3
- package/dist/bundle-analyzer/404.html +2 -2
- package/dist/bundle-analyzer/__next.__PAGE__.txt +1 -1
- package/dist/bundle-analyzer/__next._full.txt +1 -1
- package/dist/bundle-analyzer/__next._head.txt +1 -1
- package/dist/bundle-analyzer/__next._index.txt +1 -1
- package/dist/bundle-analyzer/__next._tree.txt +1 -1
- package/dist/bundle-analyzer/_not-found/__next._full.txt +1 -1
- package/dist/bundle-analyzer/_not-found/__next._head.txt +1 -1
- package/dist/bundle-analyzer/_not-found/__next._index.txt +1 -1
- package/dist/bundle-analyzer/_not-found/__next._not-found.__PAGE__.txt +1 -1
- package/dist/bundle-analyzer/_not-found/__next._not-found.txt +1 -1
- package/dist/bundle-analyzer/_not-found/__next._tree.txt +1 -1
- package/dist/bundle-analyzer/_not-found.html +2 -2
- package/dist/bundle-analyzer/_not-found.txt +1 -1
- package/dist/bundle-analyzer/index.html +2 -2
- package/dist/bundle-analyzer/index.txt +1 -1
- package/dist/client/app-bootstrap.js +1 -1
- package/dist/client/index.js +1 -1
- package/dist/compiled/next-server/pages-api-turbo.runtime.dev.js +1 -1
- package/dist/compiled/next-server/pages-api-turbo.runtime.dev.js.map +1 -1
- package/dist/compiled/next-server/pages-api.runtime.dev.js +1 -1
- package/dist/compiled/next-server/pages-api.runtime.dev.js.map +1 -1
- package/dist/compiled/next-server/pages-api.runtime.prod.js +1 -1
- package/dist/compiled/next-server/pages-api.runtime.prod.js.map +1 -1
- package/dist/compiled/next-server/pages-turbo.runtime.dev.js +1 -1
- package/dist/compiled/next-server/pages-turbo.runtime.dev.js.map +1 -1
- package/dist/compiled/next-server/pages.runtime.dev.js +1 -1
- package/dist/compiled/next-server/pages.runtime.dev.js.map +1 -1
- package/dist/compiled/next-server/pages.runtime.prod.js +1 -1
- package/dist/compiled/next-server/pages.runtime.prod.js.map +1 -1
- package/dist/compiled/next-server/server.runtime.prod.js +1 -1
- package/dist/compiled/next-server/server.runtime.prod.js.map +1 -1
- package/dist/docs/01-app/01-getting-started/02-project-structure.md +1 -1
- package/dist/docs/01-app/01-getting-started/03-layouts-and-pages.md +6 -4
- package/dist/docs/01-app/01-getting-started/04-linking-and-navigating.md +1 -1
- package/dist/docs/01-app/01-getting-started/05-server-and-client-components.md +1 -1
- package/dist/docs/01-app/01-getting-started/07-mutating-data.md +4 -3
- package/dist/docs/01-app/01-getting-started/09-revalidating.md +9 -8
- package/dist/docs/01-app/01-getting-started/13-fonts.md +1 -1
- package/dist/docs/01-app/01-getting-started/14-metadata-and-og-images.md +23 -9
- package/dist/docs/01-app/01-getting-started/17-deploying.md +1 -1
- package/dist/docs/01-app/02-guides/authentication.md +5 -5
- package/dist/docs/01-app/02-guides/backend-for-frontend.md +20 -8
- package/dist/docs/01-app/02-guides/cdn-caching.md +1 -1
- package/dist/docs/01-app/02-guides/custom-server.md +1 -1
- package/dist/docs/01-app/02-guides/data-security.md +19 -12
- package/dist/docs/01-app/02-guides/draft-mode.md +168 -105
- package/dist/docs/01-app/02-guides/forms.md +1 -1
- package/dist/docs/01-app/02-guides/how-revalidation-works.md +2 -2
- package/dist/docs/01-app/02-guides/mdx.md +1 -0
- package/dist/docs/01-app/02-guides/migrating-to-cache-components.md +533 -3
- package/dist/docs/01-app/02-guides/multi-zones.md +1 -1
- package/dist/docs/01-app/02-guides/package-bundling.md +1 -1
- package/dist/docs/01-app/02-guides/prefetching.md +5 -5
- package/dist/docs/01-app/02-guides/preventing-flash-before-hydration.md +3 -3
- package/dist/docs/01-app/02-guides/public-static-pages.md +1 -1
- package/dist/docs/01-app/02-guides/scripts.md +1 -1
- package/dist/docs/01-app/02-guides/server-actions.md +180 -0
- package/dist/docs/01-app/02-guides/streaming.md +0 -1
- package/dist/docs/01-app/02-guides/third-party-libraries.md +1 -0
- package/dist/docs/01-app/02-guides/upgrading/version-15.md +1 -1
- package/dist/docs/01-app/03-api-reference/01-directives/use-server.md +2 -0
- package/dist/docs/01-app/03-api-reference/03-file-conventions/dynamic-routes.md +4 -2
- package/dist/docs/01-app/03-api-reference/03-file-conventions/instrumentation.md +18 -4
- package/dist/docs/01-app/03-api-reference/05-config/01-next-config-js/rewrites.md +8 -6
- package/dist/docs/01-app/03-api-reference/05-config/01-next-config-js/serverActions.md +2 -0
- package/dist/docs/01-app/03-api-reference/05-config/01-next-config-js/transpilePackages.md +15 -3
- package/dist/docs/01-app/03-api-reference/05-config/02-typescript.md +1 -1
- package/dist/docs/01-app/04-glossary.md +1 -1
- package/dist/docs/02-pages/02-guides/upgrading/version-12.md +1 -1
- package/dist/docs/04-community/01-contribution-guide.md +2 -2
- package/dist/docs/index.md +0 -2
- package/dist/esm/build/index.js +3 -3
- package/dist/esm/build/swc/index.js +1 -1
- package/dist/esm/build/turbopack-analyze/index.js +1 -1
- package/dist/esm/build/turbopack-build/impl.js +1 -1
- package/dist/esm/build/webpack-config.js +3 -3
- package/dist/esm/client/app-bootstrap.js +1 -1
- package/dist/esm/client/index.js +1 -1
- package/dist/esm/lib/patch-incorrect-lockfile.js +3 -3
- package/dist/esm/server/config.js +1 -1
- package/dist/esm/server/dev/hot-reloader-turbopack.js +2 -2
- package/dist/esm/server/dev/hot-reloader-webpack.js +1 -1
- package/dist/esm/server/lib/app-info-log.js +1 -1
- package/dist/esm/server/lib/start-server.js +1 -1
- package/dist/esm/shared/lib/errors/canary-only-config-error.js +1 -1
- package/dist/lib/patch-incorrect-lockfile.js +3 -3
- package/dist/server/config.js +1 -1
- package/dist/server/dev/hot-reloader-turbopack.js +2 -2
- package/dist/server/dev/hot-reloader-webpack.js +1 -1
- package/dist/server/lib/app-info-log.js +1 -1
- package/dist/server/lib/start-server.js +1 -1
- package/dist/shared/lib/errors/canary-only-config-error.js +1 -1
- package/dist/telemetry/anonymous-meta.js +1 -1
- package/dist/telemetry/events/session-stopped.js +2 -2
- package/dist/telemetry/events/swc-load-failure.js +1 -1
- package/dist/telemetry/events/version.js +2 -2
- package/package.json +10 -252
- /package/dist/bundle-analyzer/_next/static/{ShlsHWUU3q2GhICNPxAIf → -wnBUGOTjuvnrsp1fJxyv}/_buildManifest.js +0 -0
- /package/dist/bundle-analyzer/_next/static/{ShlsHWUU3q2GhICNPxAIf → -wnBUGOTjuvnrsp1fJxyv}/_clientMiddlewareManifest.json +0 -0
- /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
|
|
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**
|
|
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
|
-
|
|
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
|
-
##
|
|
16
|
+
## What Draft Mode does
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
When Draft Mode is enabled for a request:
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
79
|
+
## Step 2: Access the Route Handler from your headless CMS
|
|
59
80
|
|
|
60
|
-
> These steps assume that the headless CMS you
|
|
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
|
|
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 CMS
|
|
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 `
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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,
|
|
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
|
-
|
|
156
|
+
## Step 3: Preview the draft content
|
|
156
157
|
|
|
157
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
166
|
+
export default async function Page({ params }: PageProps<'/posts/[slug]'>) {
|
|
167
|
+
const { slug } = await params
|
|
168
|
+
const post = await getPost(slug)
|
|
169
169
|
|
|
170
|
-
|
|
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 {
|
|
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
|
-
<
|
|
191
|
+
<h1>{post.title}</h1>
|
|
192
|
+
<article>{post.content}</article>
|
|
182
193
|
</main>
|
|
183
194
|
)
|
|
184
195
|
}
|
|
185
196
|
```
|
|
186
197
|
|
|
187
|
-
|
|
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
|
-
|
|
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
|
-
|
|
195
|
-
? 'https://draft.example.com'
|
|
196
|
-
: 'https://production.example.com'
|
|
202
|
+
## Step 4: Show a preview indicator
|
|
197
203
|
|
|
198
|
-
|
|
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
|
-
|
|
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
|
|
204
|
-
const {
|
|
217
|
+
export async function PreviewBanner() {
|
|
218
|
+
const { isEnabled } = await draftMode()
|
|
219
|
+
if (!isEnabled) return null
|
|
205
220
|
|
|
206
221
|
return (
|
|
207
|
-
<
|
|
208
|
-
|
|
209
|
-
<
|
|
210
|
-
|
|
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
|
-
|
|
232
|
+
```jsx filename="app/preview-banner.js" switcher
|
|
233
|
+
import { draftMode } from 'next/headers'
|
|
234
|
+
import { redirect } from 'next/navigation'
|
|
216
235
|
|
|
217
|
-
|
|
236
|
+
async function exitPreview() {
|
|
237
|
+
'use server'
|
|
238
|
+
const draft = await draftMode()
|
|
239
|
+
draft.disable()
|
|
240
|
+
redirect('/')
|
|
241
|
+
}
|
|
218
242
|
|
|
219
|
-
|
|
243
|
+
export async function PreviewBanner() {
|
|
244
|
+
const { isEnabled } = await draftMode()
|
|
245
|
+
if (!isEnabled) return null
|
|
220
246
|
|
|
221
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
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/
|
|
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
|
-
|
|
277
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
},
|