Payload Draft Preview 实践:基于 Versions、Drafts 与 Next.js Draft Mode 的内容预发布预览方案
本文围绕 Payload 官方示例 examples/draft-preview 展开,讲解 Draft Preview(草稿预览)的完整实现链路:从后台点击 Preview 按钮、携带 secret 跳转前端、校验身份并进入预览模式,到前端以 draft=true 拉取草稿内容、发布后按需重新生成静态页(On-demand Revalidation)。读完本文,你将能够基于该示例在自己的 Payload + Next.js 项目中落地一套"发布前先预览"的工作流,并理解其中访问控制、CORS/CSRF 安全配置与 revalidation 钩子的具体实现细节。
一、Draft Preview 是什么
Draft Preview 是 Payload 管理面板提供的一项能力:开启 Versions(版本管理)中的 Drafts(草稿)后,编辑人员在后台保存的文档可以是 draft 状态,未发布前公众不可见。通过 Draft Preview,你可以从后台的 "Preview" 按钮直接跳转到自己的前端站点并进入 "draft mode",此时查询会被修改为拉取草稿内容而非已发布内容,从而在发布前看到内容在前端上的真实渲染效果。
整个机制的核心思想可以概括为一句话:用户带着自己的 http-only cookie(身份凭证)和一个 secret(一次性校验凭证)被重定向到前端;前端 API 路由校验两者后进入预览模式;此后前端即可携带 Authorization 头安全地请求 Payload 中的草稿文档。
该示例基于 Next.js App Router 实现,相关概念在仓库文档中有对应说明:草稿预览概述、版本管理、Drafts。
二、Quick Start:把示例跑起来
以下是示例 README 给出的完整启动步骤,可直接复制执行:
-
用脚手架基于该示例创建项目:
npx create-payload-app --example draft-preview -
复制环境变量模板:
cp .env.example .env -
确保 MongoDB 已运行,并将
DATABASE_URL指向它,例如:mongodb://127.0.0.1/payload-example-draft-preview -
启动开发服务器(三者任选其一):
pnpm dev # 或 yarn dev / npm run dev -
打开
http://localhost:3000/admin进入管理面板; -
使用邮箱
demo@payloadcms.com、密码demo登录。
从 package.json 可以看到,dev 脚本实际是 pnpm seed && next dev,即启动前会先执行 seed 脚本初始化数据库(详见下文 Seed 一节);seed 脚本本身是 payload migrate:fresh。该示例依赖 payload@latest、next@^15.4.10、@payloadcms/next、@payloadcms/db-mongodb、@payloadcms/richtext-slate 以及 @payloadcms/admin-bar,Node 引擎要求 ^18.20.2 || >=20.9.0。
三、集合设计:Users 与 Pages
示例的 Payload 配置入口是 payload.config.ts,其中注册了两个集合与一个全局文档:
export default buildConfig({
collections: [Pages, Users],
cors: [process.env.NEXT_PUBLIC_SERVER_URL || ''].filter(Boolean),
csrf: [process.env.NEXT_PUBLIC_SERVER_URL || ''].filter(Boolean),
db: mongooseAdapter({
url: process.env.DATABASE_URL || '',
}),
editor: slateEditor({}),
globals: [MainMenu],
secret: process.env.PAYLOAD_SECRET || '',
// ...
})
3.1 Users 集合:预览时的身份来源
users 集合启用了 auth,提供管理面板登录能力。关键在于:在前端预览文档时,使用的是当前登录用户的 JWT 来通过 Payload 的鉴权——这正是草稿访问控制能被安全"绕过"的前提。鉴权细节可参考仓库文档 Authentication 概述 或官方 Auth 示例。
3.2 Pages 集合:drafts 开启 + 访问控制
Pages 集合(src/collections/Pages/index.ts)是 Draft Preview 的核心载体,其配置要点如下:
export const Pages: CollectionConfig = {
slug: 'pages',
access: {
create: loggedIn,
delete: loggedIn,
read: publishedOrLoggedIn, // 只读操作:已发布,或已登录
update: loggedIn,
},
admin: {
defaultColumns: ['title', 'slug', 'updatedAt'],
preview: ({ slug, collection }: { slug: string; collection: CollectionSlug }) => {
const encodedParams = new URLSearchParams({
path: `/${slug}`,
previewSecret: process.env.PREVIEW_SECRET || '',
} satisfies PreviewSearchParams)
return `${process.env.NEXT_PUBLIC_SERVER_URL}/preview?${encodedParams.toString()}`
},
useAsTitle: 'title',
},
fields: [
{ name: 'title', type: 'text', required: true },
{
name: 'slug',
type: 'text',
admin: { position: 'sidebar' },
hooks: { beforeValidate: [formatSlug('title')] },
index: true,
label: 'Slug',
},
richText(),
],
hooks: {
afterChange: [revalidatePage], // 按需重新验证(见第六节)
},
versions: {
drafts: true, // 开启草稿
},
}
四个要点:
versions: { drafts: true }:开启后该集合的文档拥有_status字段(draft/published),后台会出现 "Save Draft"、"Publish"、"Preview" 等按钮。admin.preview函数:即文档中所说的 preview function。它拼出前端的预览路由 URL,并把path(该文档在前端的相对路径/${slug})与previewSecret(环境变量PREVIEW_SECRET)作为 query 参数传给前端。- 访问控制:
read使用publishedOrLoggedIn,这是防止未登录用户读到草稿的关键。 afterChange钩子revalidatePage:发布后触发前端静态页重新生成,详见第六节。
3.3 访问控制的两个函数
publishedOrLoggedIn(access/publishedOrLoggedIn.ts)的实现是"返回访问控制对象"的典型案例——它没有简单返回布尔值,而是返回一个 where 查询来追加过滤条件:
import type { Access } from 'payload'
export const publishedOrLoggedIn: Access = ({ req: { user } }) => {
if (user) {
return true
}
return {
or: [
{
_status: {
equals: 'published',
},
},
],
}
}
含义是:请求方已登录则放行全部(包括 draft);否则强制在查询条件中附加 _status = published。loggedIn(access/loggedIn.ts)则更简单,直接 return Boolean(user),用于 create/update/delete。
3.4 前端如何拉取草稿文档
README 给出的前端取数模式是:进入预览模式后,请求 Payload REST API 时带上 draft=true 查询参数与 Authorization 头(值为当前用户的 Payload JWT),以通过上文的草稿访问控制:
const preview = true // set this based on your own front-end environment (see `Preview Mode` below)
const pageSlug = 'example-page' // same here
const searchParams = `?where[slug][equals]=${pageSlug}&depth=1${preview ? `&draft=true` : ''}`
// when previewing, send the payload token to bypass draft access control
const pageReq = await fetch(`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/api/pages${searchParams}`, {
headers: {
...(preview
? {
Authorization: `JWT ${payloadToken}`,
}
: {}),
},
})
在本示例中,前端是 Next.js App Router 的服务端组件,取数逻辑写在 app/(app)/[slug]/page.tsx 中,通过 Local API 完成同样的事情——用 draftMode() 判断是否处于预览态,然后把 draft 与 overrideAccess 一并传给 payload.find:
const queryPageBySlug = cache(async ({ slug }: { slug: string }) => {
const { isEnabled: draft } = await draftMode()
const payload = await getPayload({ config })
const result = await payload.find({
collection: 'pages',
draft, // 预览模式下拉取最新版本(草稿)
limit: 1,
overrideAccess: draft, // 预览模式下跳过访问控制
where: {
slug: {
equals: slug,
},
},
})
return result.docs?.[0] || null
})
同一文件中,generateStaticParams 在构建期用 draft: false + overrideAccess: false 拉取所有已发布页面(排除 home),生成静态路由——也就是说构建产物天然只包含公开内容,草稿绝不会泄漏进静态 HTML。
四、Preview Mode 的完整链路
README 对 Preview Mode 的描述是:用户先至少保存一份草稿文档,然后在管理面板点击 "Preview" 按钮;Payload 调用 admin.preview 函数生成的 URL 把用户路由到前端,URL 上带有 secret,同时浏览器带着用户的 http-only cookie;前端的 API 路由校验 secret 与 token 后进入预览模式。下面按请求顺序拆解本示例中的实现。
4.1 入口校验:/preview 路由
app/(app)/preview/route.ts/preview/route.ts) 是整个安全模型的关键,逻辑分五步:
export async function GET(req: NextRequest): Promise<Response> {
const payload = await getPayload({ config: configPromise })
const { searchParams } = new URL(req.url)
const path = searchParams.get('path')
const previewSecret = searchParams.get('previewSecret')
// 1. 校验 secret 是否与后端 PREVIEW_SECRET 一致
if (previewSecret !== process.env.PREVIEW_SECRET) {
return new Response('You are not allowed to preview this page', { status: 403 })
}
// 2. 必须有 path
if (!path) {
return new Response('Insufficient search params', { status: 404 })
}
// 3. path 必须是站内相对路径,防止开放重定向
if (!path.startsWith('/')) {
return new Response('This endpoint can only be used for relative previews', { status: 500 })
}
// 4. 用 http-only cookie 中的 JWT 验证用户身份
let user
try {
user = await payload.auth({
req: req as unknown as PayloadRequest,
headers: req.headers,
})
} catch (error) {
payload.logger.error({ err: error }, 'Error verifying token for live preview')
return new Response('You are not allowed to preview this page', { status: 403 })
}
if (!user) {
draft.disable()
return new Response('You are not allowed to preview this page', { status: 403 })
}
// 5. 校验通过,开启 Next.js Draft Mode 并重定向到目标页
draft.enable()
redirect(path)
}
其中 draftMode() 来自 next/headers。README 特别指出:"Preview mode" 的具体形态因框架而异。在 Next.js 中,Draft Mode 允许你在浏览器中设置 cookie,使内容按草稿展示;换到其他前端框架时(如 TanStack、Remix、Astro),这一段需要按各框架的机制自行实现,但"secret + 身份 cookie 双重校验"的思路可以复用。
4.2 退出预览:/exit-preview 路由
退出同样是一个 API 路由,调用 draftMode() 的 disable() 清除 Draft Mode 的 cookie:
// src/app/(app)/exit-preview/route.ts
import { draftMode } from 'next/headers'
export async function GET(): Promise<Response> {
const draft = await draftMode()
draft.disable()
return new Response('Draft mode is disabled')
}
4.3 预览态贯穿全局:AdminBar 的 preview 属性
根布局 app/(app)/layout.tsx/layout.tsx) 在每次渲染时读取 draftMode() 的状态,并把它传给自定义 AdminBar 组件:
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const { isEnabled } = await draftMode()
return (
<html lang="en">
<body>
<AdminBar
adminBarProps={{
preview: isEnabled,
}}
/>
<Header />
{children}
</body>
</html>
)
}
preview 为 true 时,AdminBar 上会出现退出预览的入口,方便编辑人员在"后台编辑 ↔ 前端预览"之间快速往返。
五、Admin Bar:后台与前端的快速通道
README 建议在前端渲染一条 admin bar,让登录中的用户在前端与 Payload 管理面板之间快速导航。示例的做法是:
- React 应用直接使用官方 Payload Admin Bar 包(示例依赖中即有
@payloadcms/admin-bar),本示例在其基础上做了 自定义封装,并把 Draft Mode 状态通过preview属性透传进去; - 非 React 框架的替代方案:带
credentials: 'include'请求 Payload 的/me路由,若返回已登录则自行渲染一条 admin bar。
六、On-demand Revalidation:发布即重新生成页面
如果前端是静态生成的,只靠 Draft Mode 预览还不够——页面发布后还需要"按需重新验证"(On-demand Revalidation):每次文档更新时单独重新生成对应页面的 HTML,避免为一次内容变更而整站重建。
README 给出的方案是:给集合添加 afterChange 钩子,在文档每次更新时向前端发一个后台请求,由前端处理该请求来 revalidate 对应页面的 HTML。示例中的实现是 hooks/revalidatePage.ts:
import type { CollectionAfterChangeHook } from 'payload'
import { revalidatePath } from 'next/cache'
export const revalidatePage: CollectionAfterChangeHook<Page> = ({ doc, previousDoc, req }) => {
if (req.context.skipRevalidate) {
return doc
}
// 文档变为已发布:revalidate 新路径
if (doc._status === 'published') {
const path = doc.slug === 'home' ? '/' : `/${doc.slug}`
req.payload.logger.info(`Revalidating page at path: ${path}`)
revalidatePath(path)
}
// 之前是已发布、现在不再是:revalidate 旧路径(让旧页面回到 404/重建)
if (previousDoc?._status === 'published' && doc._status !== 'published') {
const oldPath = previousDoc.slug === 'home' ? '/' : `/${previousDoc.slug}`
req.payload.logger.info(`Revalidating old page at path: ${oldPath}`)
revalidatePath(oldPath)
}
return doc
}
两个实现细节值得注意:
homeslug 特判:首页在前端路由是/,所以 revalidate 的路径做了doc.slug === 'home' ? '/' : \/${doc.slug}`` 的映射;req.context.skipRevalidate逃生口:seed 脚本在创建/更新数据时通过context: { skipRevalidate: true }(见 migrations/seed.ts)跳过 revalidation,避免初始化数据库时对空站做无意义的路径重建;全局文档MainMenu也有同款钩子 revalidateMainMenu.ts。
同理,README 也提醒:按需重新验证的行为因框架而异(Next.js App Router 提供针对特定页面的 on-demand revalidation),移植到其他框架时需要对应变通。
七、CORS / CSRF / Cookies:跨域安全配置
本示例中前端与后台是同域同端口部署在 Next.js 中,但配置上仍按"前后端可能分离"的安全基线来写。payload.config.ts 中:
cors: [process.env.NEXT_PUBLIC_SERVER_URL || ''].filter(Boolean),
csrf: [process.env.NEXT_PUBLIC_SERVER_URL || ''].filter(Boolean),
cors、csrf、cookies 三项设置的目的是确保管理面板与前端之间能安全地跨域通信:CORS 限定允许的来源,CSRF 校验同源/可信来源,cookies 配置保证 http-only 会话 cookie 在跨域场景下可被正确携带。README 的说明是:如果你把前端和管理面板合并进同一个共享端口与域的应用,可以移除这些设置以简化配置。相关背景见仓库文档 CORS/CSRF 防护 与 Cookie 配置。
八、Seed:开箱即用的演示数据
示例内置 seed 脚本(migrations/seed.ts),在启动时为你搭好一个可直接体验的数据库:
- 创建演示用户:
demo@payloadcms.com/demo; - 创建首页(
home); - 创建一个
example-page,并故意造出两个版本:先用 published 数据创建(seed/page.ts),再以draft: true更新出一份草稿(seed/pageDraft.ts)——这正是 Draft Preview 演示所需的"一已发布一草稿"状态; - 同步初始化
main-menu全局文档,把首页与示例页挂进导航。
README 给出的管理方式与注意事项:
dev脚本中的pnpm seed会在每次启动前执行;不需要该行为可从 package.json 的dev脚本中移除;- 任意时刻手动执行
pnpm seed可重新播种; - 注意:seed 是破坏性操作——它会 drop 当前数据库并从模板重新填充。只在启动新项目或可接受丢失现有数据时执行。
九、Production 构建与部署
生产环境运行需要构建并启动 Admin Panel(README 步骤):
- 在项目根目录执行
pnpm build或npm run build,调用next build,生成包含生产可用 admin bundle 的.next目录; - 执行
pnpm start或npm run start,以生产模式运行 Node,从.build目录对外提供 Payload 服务。
部署方面,README 提到最省事的方式是使用 Payload Cloud 一键托管;自托管则参考仓库文档 Deployment。生产化前建议同时阅读 防止滥用(CORS/CSRF) 与 Rotating Secret 相关文档,确保 PAYLOAD_SECRET、PREVIEW_SECRET 等机密通过环境变量注入而非硬编码。
十、示例文件地图
| 文件 | 作用 |
|---|---|
| payload.config.ts | Payload 总配置:collections、cors、csrf、secret、db |
| collections/Pages/index.ts | Pages 集合:drafts: true、admin.preview、access、afterChange |
| access/publishedOrLoggedIn.ts | 只读访问控制:未登录仅可见 published |
| hooks/revalidatePage.ts | 发布后按需重新验证前端静态页 |
| app/(app)/preview/route.ts/preview/route.ts) | secret + JWT 双重校验,开启 Draft Mode |
| app/(app)/exit-preview/route.ts/exit-preview/route.ts) | 关闭 Draft Mode |
| app/(app)/[slug]/page.tsx | 按 Draft Mode 状态取草稿/已发布文档 |
| app/(app)/layout.tsx/layout.tsx) | 把预览态传给 AdminBar |
| migrations/seed.ts | 播种演示用户与"一发布一草稿"的示例页 |
| package.json | dev(先 seed)、seed、build、start 脚本 |
十一、小结
回到 README 的一句话定义:Draft Preview 让用户带着 secret 与 http-only cookie 跳进前端的 "draft mode",此后查询被改写为拉取草稿内容。本示例把这句话落成了四段可验证的代码:
- 数据层:
versions.drafts提供_status与版本能力,publishedOrLoggedIn保证草稿对公众不可见; - 入口层:
admin.preview生成带path与previewSecret的跳转 URL,/preview路由完成 secret 比对、相对路径校验、payload.auth身份验证三步后才draft.enable(); - 渲染层:页面组件读取
draftMode()决定payload.find的draft/overrideAccess参数,构建期静态参数只收录 published 文档; - 发布层:
afterChange钩子在状态切换到 published 时调用revalidatePath,实现发布即更新静态页。
这四段各自独立、可单独移植,组合起来就构成了一套完整的内容预发布预览工作流;若要扩展更多字段或行为,可进一步参考仓库文档 Collections 配置、Drafts 与 Access Control。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00