
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跳转前端、校验身份并进入预览模式到前端以drafttrue拉取草稿内容、发布后按需重新生成静态页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进入管理面板使用邮箱demopayloadcms.com、密码demo登录。从 package.json 可以看到dev脚本实际是pnpm seed next dev即启动前会先执行 seed 脚本初始化数据库详见下文 Seed 一节seed脚本本身是payload migrate:fresh。该示例依赖payloadlatest、next^15.4.10、payloadcms/next、payloadcms/db-mongodb、payloadcms/richtext-slate以及payloadcms/admin-barNode 引擎要求^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 访问控制的两个函数publishedOrLoggedInaccess/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。loggedInaccess/loggedIn.ts则更简单直接return Boolean(user)用于 create/update/delete。3.4 前端如何拉取草稿文档README 给出的前端取数模式是进入预览模式后请求 Payload REST API 时带上drafttrue查询参数与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}depth1${preview ? drafttrue : } // 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.findconst 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: falseoverrideAccess: 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): PromiseResponse { 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(): PromiseResponse { 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 langen 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.tsimport type { CollectionAfterChangeHook } from payload import { revalidatePath } from next/cache export const revalidatePage: CollectionAfterChangeHookPage ({ 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在启动时为你搭好一个可直接体验的数据库创建演示用户demopayloadcms.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 PanelREADME 步骤在项目根目录执行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.tsPayload 总配置collections、cors、csrf、secret、dbcollections/Pages/index.tsPages 集合drafts: true、admin.preview、access、afterChangeaccess/publishedOrLoggedIn.ts只读访问控制未登录仅可见 publishedhooks/revalidatePage.ts发布后按需重新验证前端静态页app/(app)/preview/route.ts/preview/route.ts)secret JWT 双重校验开启 Draft Modeapp/(app)/exit-preview/route.ts/exit-preview/route.ts)关闭 Draft Modeapp/(app)/[slug]/page.tsx按 Draft Mode 状态取草稿/已发布文档app/(app)/layout.tsx/layout.tsx)把预览态传给 AdminBarmigrations/seed.ts播种演示用户与一发布一草稿的示例页package.jsondev先 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。【免费下载链接】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),仅供参考