145 lines
4.2 KiB
Markdown
145 lines
4.2 KiB
Markdown
---
|
|
title: "Admin Preview & Draft Preview"
|
|
aliases: [payload-preview, draft-preview, payload-draft-mode]
|
|
tags: [payloadcms, admin, preview, draft, nextjs]
|
|
sources: [raw/admin__preview.md]
|
|
created: 2026-05-15
|
|
updated: 2026-05-15
|
|
---
|
|
|
|
## Overview
|
|
|
|
Preview generates a direct link from the Admin Edit View to your front-end app. A **Preview** button appears in the edit view with an `href` pointing to the URL your function returns.
|
|
|
|
> **Not** [[wiki/payloadcms/admin-panel-overview|Live Preview]] — Live Preview embeds your app in an iframe inside the Admin Panel. Preview just navigates to an external URL.
|
|
|
|
## Setup
|
|
|
|
Add `admin.preview` to any Collection or Global config:
|
|
|
|
```ts
|
|
export const Pages: CollectionConfig = {
|
|
slug: 'pages',
|
|
admin: {
|
|
preview: ({ slug }) => `http://localhost:3000/${slug}`,
|
|
},
|
|
}
|
|
```
|
|
|
|
### Function Signature
|
|
|
|
```ts
|
|
preview: (doc, options) => string | null | Promise<string | null>
|
|
```
|
|
|
|
| Arg | Type | Description |
|
|
|-----|------|-------------|
|
|
| `doc` | object | Full document data including unsaved changes |
|
|
| `options.locale` | string | Current locale |
|
|
| `options.req` | PayloadRequest | Full request object |
|
|
| `options.token` | string | JWT of authenticated user |
|
|
|
|
Build absolute URLs using `req`:
|
|
```ts
|
|
preview: (doc, { req }) => `${req.protocol}//${req.host}/${doc.slug}`
|
|
```
|
|
|
|
## Draft Preview
|
|
|
|
Draft Preview allows editors to see unpublished content. Flow:
|
|
1. Admin clicks **Preview** → navigates to a custom endpoint on the front-end
|
|
2. Endpoint verifies a shared `PREVIEW_SECRET`, authenticates the user via `payload.auth()`
|
|
3. Endpoint enables Next.js Draft Mode (sets a cookie) and redirects to the page
|
|
4. Page query includes `draft: true` → Payload returns draft document
|
|
|
|
### Next.js Implementation (3 Steps)
|
|
|
|
#### Step 1 — Format Preview URL
|
|
|
|
```ts
|
|
preview: ({ slug }) => {
|
|
const params = new URLSearchParams({
|
|
slug,
|
|
collection: 'pages',
|
|
path: `/${slug}`,
|
|
previewSecret: process.env.PREVIEW_SECRET || '',
|
|
})
|
|
return `/preview?${params.toString()}`
|
|
}
|
|
```
|
|
|
|
#### Step 2 — Create `/app/preview/route.ts`
|
|
|
|
```ts
|
|
import { draftMode } from 'next/headers'
|
|
import { redirect } from 'next/navigation'
|
|
import { getPayload } from 'payload'
|
|
|
|
export async function GET(req) {
|
|
const payload = await getPayload({ config: configPromise })
|
|
const { searchParams } = new URL(req.url)
|
|
const path = searchParams.get('path')
|
|
const previewSecret = searchParams.get('previewSecret')
|
|
|
|
if (previewSecret !== process.env.PREVIEW_SECRET)
|
|
return new Response('Forbidden', { status: 403 })
|
|
|
|
if (!path?.startsWith('/'))
|
|
return new Response('Bad path', { status: 400 })
|
|
|
|
let user
|
|
try {
|
|
user = await payload.auth({ req, headers: req.headers })
|
|
} catch {
|
|
return new Response('Forbidden', { status: 403 })
|
|
}
|
|
|
|
const draft = await draftMode()
|
|
if (!user) { draft.disable(); return new Response('Forbidden', { status: 403 }) }
|
|
|
|
draft.enable()
|
|
redirect(path)
|
|
}
|
|
```
|
|
|
|
#### Step 3 — Query Draft Content
|
|
|
|
```ts
|
|
const { isEnabled: isDraftMode } = await draftMode()
|
|
|
|
const page = await payload.find({
|
|
collection: 'pages',
|
|
draft: isDraftMode,
|
|
overrideAccess: isDraftMode,
|
|
where: { slug: { equals: slug } },
|
|
limit: 1,
|
|
})?.then(({ docs }) => docs?.[0])
|
|
```
|
|
|
|
## Conditional Preview Button
|
|
|
|
Return `null` to hide the button:
|
|
|
|
```ts
|
|
preview: (doc) => doc?.enabled ? `http://localhost:3000/${doc.slug}` : null
|
|
```
|
|
|
|
Useful when you only want preview available for documents meeting certain criteria (e.g. has a slug, is a specific status).
|
|
|
|
## Key Takeaways
|
|
|
|
- `admin.preview` returns a URL string (or `null` to hide button) — can be async
|
|
- Preview ≠ Live Preview: Preview is a link, Live Preview is an embedded iframe
|
|
- Draft Preview requires 3 parts: preview URL → `/preview` route → draft-aware page query
|
|
- Always guard the preview route with a `PREVIEW_SECRET` env var + `payload.auth()` check
|
|
- Use `req.protocol + req.host` for absolute URLs (required for Vercel Preview Deployments)
|
|
- Return `null` conditionally to show/hide the preview button based on document state
|
|
|
|
## Related
|
|
|
|
- [[wiki/payloadcms/admin-panel-overview|Admin Panel Overview]]
|
|
- [[wiki/payloadcms/admin-preferences|Admin Preferences]]
|
|
|
|
## Sources
|
|
|
|
- `raw/admin__preview.md` — compiled from https://payloadcms.com/docs/admin/preview
|