在 Next.js 中使用草稿模式预览内容
草稿模式(Draft Mode)让编辑者无需等待重新验证,就能查看草稿或编辑中内容在网站上的呈现效果。编辑者处于草稿模式时,缓存或预渲染内容会被绕过,系统直接从上游来源获取内容。其他访客仍然看到缓存或预渲染后的页面。 如果 CMS 使用同一 URL 提供草稿和已发布内容,数据获取代码无需修改。否则,请参阅下文“CMS 使用独立草稿端点时”。 ## 草稿模式会做什么
请求启用草稿模式后: – fetch() 调用跳过 Next.js 的 fetch 缓存,直接访问网络。 – 'use cache' 内的组件和函数会在每次请求时重新执行,结果不写入缓存。 – 同样绕过 unstable_cache 的读写。 – 页面不进入 ISR 响应缓存,并带有 Cache-Control: private, no-cache, no-store, max-age=0, must-revalidate 响应头。 无论页面是静态生成、从缓存提供,还是经 ISR 重新验证,上述效果都适用。 ## 本指南的范围
本指南假设:
- 无头 CMS 支持配置预览 URL,大多数 CMS 都支持。
- 编辑者点击“预览”时,CMS 在新标签页打开类似
/api/draft?secret=XXX&slug=/posts/foo的 URL。secret是共享令牌,slug是要预览的路径。 - Next.js 应用验证令牌、启用草稿模式,然后重定向到该路径。
基于这个约定,本指南依次介绍: 1. 创建通过设置 cookie 启用草稿模式的 Route Handler。 2. 使用 CMS 传来的共享令牌与 slug 保护处理器。 3. 渲染读取最新草稿的页面。 4. 显示带退出表单的预览横幅。
然后根据项目配置,继续查看: – “草稿模式与缓存组件”:从 'use cache' 边界内展示预览状态。 – “CMS 使用独立草稿端点时”:根据 isEnabled 选择获取内容的 URL。 > 提示:GET 的语义应是安全、只读。通过 cookie 启用草稿模式这类会影响后续请求的操作,应使用 POST。本例入口处理器使用 GET,因为假设采用 CMS 预览集成:CMS 在新标签页打开 URL,由此产生 GET 请求。步骤 4 的退出流程通过 Server Action 或 POST Route Handler 使用 POST。 ## 步骤 1:创建 Route Handler
创建一个设置草稿模式 cookie 的 Route Handler。名称任意,例如 app/api/draft/route.ts。
文件:app/api/draft/route.ts
import { draftMode } from 'next/headers'
export async function GET(request: Request) {
const draft = await draftMode()
draft.enable()
return new Response('Draft mode is enabled')
}
文件:app/api/draft/route.js
import { draftMode } from 'next/headers'
export async function GET(request) {
const draft = await draftMode()
draft.enable()
return new Response('Draft mode is enabled')
}
draft.enable() 会设置名为 __prerender_bypass 的 cookie。后续携带该 cookie 的请求会跳过上面列出的每一层缓存。 可以访问 /api/draft,然后在浏览器开发者工具中检查 Set-Cookie 响应头,手动验证。
按目前写法,处理器是公开的:任何访问 /api/draft 的人都能为自己启用草稿模式。步骤 2 会加入共享令牌,使其仅供 CMS 调用。 ## 步骤 2:从无头 CMS 访问 Route Handler
以下步骤假设 CMS 支持设置自定义草稿 URL。如果不支持,仍可用这种方式保护草稿 URL,但需要手动构造并访问。具体步骤取决于所用 CMS。
若要从无头 CMS 安全地访问 Route Handler: 1. 使用你选择的令牌生成器生成一个秘密令牌字符串。只有 Next.js 应用与无头 CMS 知道它。 2. 如果 CMS 支持自定义草稿 URL,指定该 URL。假设 Route Handler 位于 app/api/draft/route.ts,例如:
文件:Terminal
https://<your-site>/api/draft?secret=<token>&slug=<path>
<your-site>是部署域名。<token>替换为刚生成的秘密令牌。<path>是要查看的页面路径。若查看/posts/one,使用&slug=/posts/one。CMS 可能允许在草稿 URL 中插入变量,从而根据 CMS 数据动态设置
<path>,例如&slug=/posts/{entry.fields.slug}。 3. 在 Route Handler 中,检查令牌是否匹配、slug参数是否存在;不满足则使请求失败。随后调用draft.enable()设置 cookie,再将浏览器重定向到对应路径:
文件:app/api/draft/route.ts
import { draftMode } from 'next/headers'
import { redirect } from 'next/navigation'
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const secret = searchParams.get('secret')
const slug = searchParams.get('slug')
// This secret should only be known to this Route Handler and the CMS
if (secret !== 'MY_SECRET_TOKEN' || !slug) {
return new Response('Invalid token', { status: 401 })
}
// Verify the slug exists in the CMS before enabling Draft Mode
const post = await getPostBySlug(slug)
if (!post) {
return new Response('Invalid slug', { status: 401 })
}
const draft = await draftMode()
draft.enable()
// Redirect to the path from the fetched post, not from searchParams,
// to avoid open redirect vulnerabilities
redirect(post.slug)
}
文件:app/api/draft/route.js
import { draftMode } from 'next/headers'
import { redirect } from 'next/navigation'
export async function GET(request) {
const { searchParams } = new URL(request.url)
const secret = searchParams.get('secret')
const slug = searchParams.get('slug')
if (secret !== 'MY_SECRET_TOKEN' || !slug) {
return new Response('Invalid token', { status: 401 })
}
const post = await getPostBySlug(slug)
if (!post) {
return new Response('Invalid slug', { status: 401 })
}
const draft = await draftMode()
draft.enable()
redirect(post.slug)
}
成功后,浏览器会携带已设置的草稿模式 cookie,重定向到目标路径。 ## 步骤 3:预览草稿内容
草稿模式会自动绕过缓存,因此页面无需判断模式是否开启,也能接收最新内容。照常获取数据即可:
文件:app/posts/[slug]/page.tsx
async function getPost(slug: string) {
const res = await fetch(`https://cms.example.com/posts/${slug}`)
return res.json()
}
export default async function Page({ params }: PageProps<'/posts/[slug]'>) {
const { slug } = await params
const post = await getPost(slug)
return (
<main>
<h1>{post.title}</h1>
<article>{post.content}</article>
</main>
)
}
文件:app/posts/[slug]/page.js
async function getPost(slug) {
const res = await fetch(`https://cms.example.com/posts/${slug}`)
return res.json()
}
export default async function Page({ params }) {
const { slug } = await params
const post = await getPost(slug)
return (
<main>
<h1>{post.title}</h1>
<article>{post.content}</article>
</main>
)
}
存在草稿模式 cookie 时,上面的 fetch 会跳过 Next.js fetch 缓存,向 CMS 获取当前草稿。没有 cookie 时,同一个请求可以照常由缓存提供。 如果 CMS 为草稿使用不同 URL,而非同一端点,请参阅下文“CMS 使用独立草稿端点时”。 ## 步骤 4:显示预览状态
isEnabled 最适合用来给编辑者提示:通过横幅确认当前显示的是草稿,并提供退出方式。在根布局中渲染提示,使每个预览页面都能显示。
文件:app/preview-banner.tsx
import { draftMode } from 'next/headers'
import { redirect } from 'next/navigation'
async function exitPreview() {
'use server'
const draft = await draftMode()
draft.disable()
redirect('/')
}
export async function PreviewBanner() {
const { isEnabled } = await draftMode()
if (!isEnabled) return null
return (
<aside role="status">
Preview mode is on.{' '}
<form action={exitPreview}>
<button type="submit">Exit preview</button>
</form>
</aside>
)
}
文件:app/preview-banner.js
import { draftMode } from 'next/headers'
import { redirect } from 'next/navigation'
async function exitPreview() {
'use server'
const draft = await draftMode()
draft.disable()
redirect('/')
}
export async function PreviewBanner() {
const { isEnabled } = await draftMode()
if (!isEnabled) return null
return (
<aside role="status">
Preview mode is on.{' '}
<form action={exitPreview}>
<button type="submit">Exit preview</button>
</form>
</aside>
)
}
也可以通过 GET Route Handler 退出草稿模式,但 POST 的语义更恰当,例如通过 Server Action 提交表单,或提交到 POST Route Handler。 如果确实使用 GET Route Handler,应通过 <form method="GET"> 触发,而不是 <Link>。Next.js 默认预取 <Link>,这可能在编辑者点击之前就清除 cookie。无论使用什么方法,表单都不会被预取。 ## 草稿模式与缓存组件
可以在 'use cache' 作用域中读取 isEnabled,由缓存组件渲染预览提示。绕过缓存的规则仍然适用,因此组件会在每个草稿请求中用最新数据重新执行。
文件:app/posts/[slug]/page.tsx
import { draftMode } from 'next/headers'
async function Post({ slug }: { slug: string }) {
'use cache'
const post = await fetch(`https://cms.example.com/posts/${slug}`).then((r) =>
r.json()
)
const { isEnabled } = await draftMode()
return (
<article>
{isEnabled && <p role="status">Draft preview</p>}
<h1>{post.title}</h1>
<div>{post.content}</div>
</article>
)
}
文件:app/posts/[slug]/page.js
import { draftMode } from 'next/headers'
async function Post({ slug }) {
'use cache'
const post = await fetch(`https://cms.example.com/posts/${slug}`).then((r) =>
r.json()
)
const { isEnabled } = await draftMode()
return (
<article>
{isEnabled && <p role="status">Draft preview</p>}
<h1>{post.title}</h1>
<div>{post.content}</div>
</article>
)
}
提示:不能在缓存指令作用域内调用
draftMode().enable()或draftMode().disable();应从 Route Handler 或 Server Action 切换草稿模式。 ## CMS 使用独立草稿端点时
如果 CMS 在不同 URL 提供草稿,或要求不同凭据,可根据 isEnabled 为获取逻辑选择分支:
文件:app/posts/[slug]/page.tsx
import { draftMode } from 'next/headers'
async function getPost(slug: string) {
const { isEnabled } = await draftMode()
const baseUrl = isEnabled
? 'https://cms.example.com/preview'
: 'https://cms.example.com/published'
const res = await fetch(`${baseUrl}/posts/${slug}`)
return res.json()
}
文件:app/posts/[slug]/page.js
import { draftMode } from 'next/headers'
async function getPost(slug) {
const { isEnabled } = await draftMode()
const baseUrl = isEnabled
? 'https://cms.example.com/preview'
: 'https://cms.example.com/published'
const res = await fetch(`${baseUrl}/posts/${slug}`)
return res.json()
}
绕过缓存的规则仍适用于两个分支;这里的分支只决定从哪里读取数据。
下一步
参阅 draftMode API。
来源与许可
Next.js 官方指南,原页更新于 2026-06-02。Copyright (c) 2025 Vercel, Inc.,MIT。中文翻译与静态排版转换于 2026-10-03 完成,保留 TypeScript 和 JavaScript 两组原始示例。代码未执行。
完整许可证原文
The MIT License (MIT)
Copyright (c) 2025 Vercel, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.











暂无评论内容