obsidian/wiki/payloadcms/live-preview.md
2026-05-15 16:14:29 +01:00

8.2 KiB

title aliases tags sources created updated
Live Preview — Overview
live-preview
payload-live-preview
visual-editing
payloadcms
tech-patterns
live-preview
raw/live-preview__overview.md
https://payloadcms.com/docs/live-preview/overview
https://payloadcms.com/docs/live-preview/server
https://payloadcms.com/docs/live-preview/client
https://payloadcms.com/docs/live-preview/frontend
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 undefined or null to 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 mismatchdepth in useLivePreview must 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.com to your frontend's Content Security Policy
  • CORS/CSRF — if frontend and Payload run on different domains/ports, configure both cors and csrf in payload.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 useLivePreview in Server Components; wrap in a 'use client' component
  • Conditional rendering — returning null from url function 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.postMessage for real-time sync between Admin Panel and frontend
  • url can be a dynamic function — use it for multi-tenant, localization, unknown preview URLs, or conditional access
  • Return null/undefined from url to hide Live Preview for specific users or documents (role-based access control)
  • Depth mismatch in useLivePreview vs 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