8.2 KiB
8.2 KiB
| title | aliases | tags | sources | created | updated | ||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
| Live Preview — Overview |
|
|
2026-05-15 | 2026-05-15 |
PayloadCMS — Live Preview
Overview
- Renders your frontend inside an iframe directly in the Admin Panel
- Changes propagate in real-time via
window.postMessage— no save required - Two modes: server-side (React Server Components / Next.js App Router) and client-side (Pages Router, Vue, SPA)
- Server-side is recommended when available: simpler setup, better performance
- Client-side fires on every form-state change; server-side fires on save/autosave/publish
Setup
Payload config (global or per-collection)
import { buildConfig } from 'payload'
export default buildConfig({
admin: {
livePreview: {
url: 'http://localhost:3000',
collections: ['pages'],
globals: ['header'],
},
},
})
- Can also be set on individual wiki/payloadcms/configuration or Global admin configs; those settings are merged as overrides
- Once configured, a "Live Preview" toggle appears at the top of each enabled document
Config Options
| Option | Type | Description |
|---|---|---|
url |
string | function |
iframe src; can be dynamic (see below) |
breakpoints |
Breakpoint[] |
Device-size presets shown in the toolbar |
collections |
string[] |
Collection slugs to enable Live Preview on |
globals |
string[] |
Global slugs to enable Live Preview on |
Dynamic URL function
url: ({ data, collectionConfig, locale, req }) =>
`${data.tenant.url}/${data.slug}${locale ? `?locale=${locale.code}` : ''}`
URL function args:
| Arg | Description |
|---|---|
data |
Document data including unsaved changes |
locale |
Current locale being edited (if localization enabled) |
collectionConfig |
Collection admin config of the document |
globalConfig |
Global admin config of the document |
req |
Payload Request object (use for fully-qualified URLs: ${req.protocol}//${req.host}/${data.slug}) |
- Return
undefinedornullto conditionally hide Live Preview (acts as access control) - Return a relative path — Payload auto-injects protocol/host from the browser window
- Useful for unknown preview URLs at build time (e.g. Vercel preview deployments)
Conditional Rendering
Return null/undefined to hide the Live Preview button for certain users or documents:
url: ({ req }) => (req.user?.role === 'admin' ? '/hello-world' : null)
Breakpoints
breakpoints: [
{ label: 'Mobile', name: 'mobile', width: 375, height: 667 },
{ label: 'Tablet', name: 'tablet', width: 768, height: 1024 },
]
Breakpoint options (* = required):
| Option | Description |
|---|---|
label * |
Label shown in the toolbar dropdown |
name * |
Internal identifier |
width * |
iframe width in px |
height * |
iframe height in px |
- "Responsive" (100% width/height) is always available and is the default — no config needed
- Toolbar has manual width/height inputs that temporarily switch to "Custom" mode
- "Open in new window" button closes the iframe and opens a resizable browser window; closing it re-opens the iframe
Code Examples
Server-side (Next.js App Router)
Install:
npm install @payloadcms/live-preview-react
page.tsx (Server Component):
import { RefreshRouteOnSave } from './RefreshRouteOnSave'
import { getPayload } from 'payload'
import config from '../payload.config'
export default async function Page() {
const payload = await getPayload({ config })
const page = await payload.findByID({ collection: 'pages', id: '123', draft: true })
return (
<>
<RefreshRouteOnSave />
<h1>{page.title}</h1>
</>
)
}
RefreshRouteOnSave.tsx (Client Component):
'use client'
import { RefreshRouteOnSave as PayloadLivePreview } from '@payloadcms/live-preview-react'
import { useRouter } from 'next/navigation'
export const RefreshRouteOnSave = () => {
const router = useRouter()
return (
<PayloadLivePreview
refresh={() => router.refresh()}
serverURL={process.env.NEXT_PUBLIC_PAYLOAD_URL}
/>
)
}
Client-side React (Pages Router / SPA)
'use client'
import { useLivePreview } from '@payloadcms/live-preview-react'
import type { Page as PageType } from '@/payload-types'
export const PageClient: React.FC<{ page: PageType }> = ({ page: initialPage }) => {
const { data, isLoading } = useLivePreview<PageType>({
initialData: initialPage,
serverURL: process.env.NEXT_PUBLIC_PAYLOAD_URL,
depth: 2, // must match your initial fetch depth
})
return <h1>{data.title}</h1>
}
Client-side Vue 3 / Nuxt 3
npm install @payloadcms/live-preview-vue
<script setup lang="ts">
import { useLivePreview } from '@payloadcms/live-preview-vue'
const props = defineProps<{ initialData: PageData }>()
const { data } = useLivePreview<PageData>({
initialData: props.initialData,
serverURL: '<PAYLOAD_SERVER_URL>',
depth: 2,
})
</script>
<template><h1>{{ data.title }}</h1></template>
Custom hook (framework-agnostic)
npm install @payloadcms/live-preview
import { subscribe, unsubscribe, ready } from '@payloadcms/live-preview'
// 1. subscribe() — listens to postMessage, merges data, populates relationships, calls callback
// 2. ready() — signals Admin Panel the frontend is ready
// 3. unsubscribe() — clean up on unmount
For server-side custom: use ready + isDocumentEvent from the same package to trigger router.refresh().
Gotchas
- Depth mismatch —
depthinuseLivePreviewmust exactly match the depth used in the initial page fetch; otherwise relationships disappear during editing - CSP iframe block — add
frame-ancestors: "self" localhost:* https://your-cms.comto your frontend's Content Security Policy - CORS/CSRF — if frontend and Payload run on different domains/ports, configure both
corsandcsrfinpayload.config.ts - Server-side feels slower — it refreshes on save, not on keystroke. Fix: enable wiki/payloadcms/versions with a short interval (e.g.
375ms) - RSC + client hook — do not use
useLivePreviewin Server Components; wrap in a'use client'component - Conditional rendering — returning
nullfromurlfunction hides the Live Preview button; use it for role-based access
Key Takeaways
- Live Preview renders your frontend in an iframe inside the Admin Panel; no save required for client-side mode
- Uses
window.postMessagefor real-time sync between Admin Panel and frontend urlcan be a dynamic function — use it for multi-tenant, localization, unknown preview URLs, or conditional access- Return
null/undefinedfromurlto hide Live Preview for specific users or documents (role-based access control) - Depth mismatch in
useLivePreviewvs initial fetch is the most common bug — always match them - Server-side mode (Next.js App Router) = simpler, but fires on save; client-side = fires on every keystroke
- "Responsive" breakpoint is always available; "Open in new window" for free resize
Related
- wiki/payloadcms/live-preview-client —
useLivePreviewhook, Vue, framework-agnostic API - wiki/payloadcms/live-preview-frontend — server-side vs client-side decision guide
- wiki/payloadcms/document-views —
livePreviewdocument view key and tab config - wiki/payloadcms/configuration —
admin.livePreviewroot config - wiki/payloadcms/localization —
localearg in dynamic URL function