Payload Draft Preview 实践:基于 Versions、Drafts 与 Next.js Draft Mode 的内容预发布预览方案
2026/9/6 23:13:50 网站建设 项目流程

Payload Draft Preview 实践:基于 Versions、Drafts 与 Next.js Draft Mode 的内容预发布预览方案

【免费下载链接】payloadPayload is the open-source, fullstack Next.js framework, giving you instant backend superpowers. Get a full TypeScript backend and admin panel instantly. Use Payload as a headless CMS or for building powerful applications.项目地址: https://gitcode.com/GitHub_Trending/pa/payload

本文围绕 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 给出的完整启动步骤,可直接复制执行:

  1. 用脚手架基于该示例创建项目:

    npx create-payload-app --example draft-preview
  2. 复制环境变量模板:

    cp .env.example .env
  3. 确保 MongoDB 已运行,并将DATABASE_URL指向它,例如:

    mongodb://127.0.0.1/payload-example-draft-preview
  4. 启动开发服务器(三者任选其一):

    pnpm dev # 或 yarn dev / npm run dev
  5. 打开http://localhost:3000/admin进入管理面板;

  6. 使用邮箱demo@payloadcms.com、密码demo登录。

从 package.json 可以看到,dev脚本实际是pnpm seed && next dev,即启动前会先执行 seed 脚本初始化数据库(详见下文 Seed 一节);seed脚本本身是payload migrate:fresh。该示例依赖payload@latestnext@^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 = publishedloggedIn(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()判断是否处于预览态,然后把draftoverrideAccess一并传给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),

corscsrfcookies三项设置的目的是确保管理面板与前端之间能安全地跨域通信: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 步骤):

  1. 在项目根目录执行pnpm buildnpm run build,调用next build,生成包含生产可用 admin bundle 的.next目录;
  2. 执行pnpm startnpm run start,以生产模式运行 Node,从.build目录对外提供 Payload 服务。

部署方面,README 提到最省事的方式是使用 Payload Cloud 一键托管;自托管则参考仓库文档 Deployment。生产化前建议同时阅读 防止滥用(CORS/CSRF) 与 Rotating Secret 相关文档,确保PAYLOAD_SECRETPREVIEW_SECRET等机密通过环境变量注入而非硬编码。

十、示例文件地图

文件作用
payload.config.tsPayload 总配置:collections、cors、csrf、secret、db
collections/Pages/index.tsPages 集合:drafts: trueadmin.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.jsondev(先 seed)、seedbuildstart脚本

十一、小结

回到 README 的一句话定义:Draft Preview 让用户带着 secret 与 http-only cookie 跳进前端的 "draft mode",此后查询被改写为拉取草稿内容。本示例把这句话落成了四段可验证的代码:

  1. 数据层versions.drafts提供_status与版本能力,publishedOrLoggedIn保证草稿对公众不可见;
  2. 入口层admin.preview生成带pathpreviewSecret的跳转 URL,/preview路由完成 secret 比对、相对路径校验、payload.auth身份验证三步后才draft.enable()
  3. 渲染层:页面组件读取draftMode()决定payload.finddraft/overrideAccess参数,构建期静态参数只收录 published 文档;
  4. 发布层afterChange钩子在状态切换到 published 时调用revalidatePath,实现发布即更新静态页。

这四段各自独立、可单独移植,组合起来就构成了一套完整的内容预发布预览工作流;若要扩展更多字段或行为,可进一步参考仓库文档 Collections 配置、Drafts 与 Access Control。

【免费下载链接】payloadPayload is the open-source, fullstack Next.js framework, giving you instant backend superpowers. Get a full TypeScript backend and admin panel instantly. Use Payload as a headless CMS or for building powerful applications.项目地址: https://gitcode.com/GitHub_Trending/pa/payload

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询