obsidian/wiki/payloadcms/admin-preview.md
2026-05-15 15:13:56 +01:00

4.2 KiB

title aliases tags sources created updated
Admin Preview & Draft Preview
payload-preview
draft-preview
payload-draft-mode
payloadcms
admin
preview
draft
nextjs
raw/admin__preview.md
2026-05-15 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 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:

export const Pages: CollectionConfig = {
  slug: 'pages',
  admin: {
    preview: ({ slug }) => `http://localhost:3000/${slug}`,
  },
}

Function Signature

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:

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

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

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

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:

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

Sources