Live Preview in Payload CMS
Live Preview allows editors to see content changes in real-time directly in the admin panel, without publishing. Payload sends draft data to the frontend via postMessage — the Next.js app renders the page with this data in an iframe.
Configuration in Payload
// payload.config.ts
export default buildConfig({
admin: {
livePreview: {
// Breakpoints for preview
breakpoints: [
{ label: 'Mobile', name: 'mobile', width: 375, height: 667 },
{ label: 'Tablet', name: 'tablet', width: 768, height: 1024 },
{ label: 'Desktop', name: 'desktop', width: 1440, height: 900 },
],
},
},
})
// collections/Posts.ts — URL for specific collection
const Posts: CollectionConfig = {
slug: 'posts',
admin: {
livePreview: {
url: ({ data, locale }) => {
const baseURL = process.env.NEXT_PUBLIC_FRONTEND_URL
const slug = data?.slug || 'preview'
const localeParam = locale?.code ? `?locale=${locale.code}` : ''
return `${baseURL}/posts/${slug}${localeParam}`
},
},
},
}
// globals/HomePage.ts
const HomePage: GlobalConfig = {
slug: 'home-page',
admin: {
livePreview: {
url: () => process.env.NEXT_PUBLIC_FRONTEND_URL || 'http://localhost:3000',
},
},
}
Next.js: useLivePreview Hook
npm install @payloadcms/live-preview-react
// app/(frontend)/posts/[slug]/page.tsx
import { LivePreviewListener } from './LivePreviewListener'
export default async function PostPage({ params, searchParams }: {
params: { slug: string }
searchParams: { locale?: string }
}) {
const payload = await getPayload({ config })
const { isEnabled } = draftMode()
const result = await payload.find({
collection: 'posts',
where: { slug: { equals: params.slug } },
draft: isEnabled,
overrideAccess: isEnabled,
locale: searchParams.locale as any || 'ru',
})
const post = result.docs[0]
if (!post) notFound()
return (
<>
{isEnabled && <LivePreviewListener initialData={post} />}
<Article post={post} />
</>
)
}
// app/(frontend)/posts/[slug]/LivePreviewListener.tsx
'use client'
import { useLivePreview } from '@payloadcms/live-preview-react'
import { useRouter } from 'next/navigation'
import type { Post } from '@/payload-types'
export const LivePreviewListener = ({ initialData }: { initialData: Post }) => {
const { data } = useLivePreview<Post>({
initialData,
serverURL: process.env.NEXT_PUBLIC_SERVER_URL!,
depth: 2,
})
// Update page title when data changes
if (typeof document !== 'undefined' && data.title) {
document.title = data.title
}
return null // data updates through initialData prop of parent
}
Full Component with Live Data
// Component using live data directly
'use client'
import { useLivePreview } from '@payloadcms/live-preview-react'
import type { Post } from '@/payload-types'
export const PostPreview = ({ initialPost }: { initialPost: Post }) => {
const { data: post } = useLivePreview<Post>({
initialData: initialPost,
serverURL: process.env.NEXT_PUBLIC_SERVER_URL!,
depth: 2,
})
return (
<article>
<h1>{post.title}</h1>
{post.featuredImage && typeof post.featuredImage !== 'string' && (
<img src={post.featuredImage.url!} alt={post.featuredImage.alt} />
)}
<RichText content={post.content} />
</article>
)
}
Enabling Draft Mode in Next.js
// app/api/draft/route.ts
import { draftMode } from 'next/headers'
import { NextRequest, NextResponse } from 'next/server'
export async function GET(req: NextRequest) {
const secret = req.nextUrl.searchParams.get('secret')
const slug = req.nextUrl.searchParams.get('slug')
const collection = req.nextUrl.searchParams.get('collection')
if (secret !== process.env.PAYLOAD_DRAFT_SECRET) {
return NextResponse.json({ error: 'Invalid token' }, { status: 401 })
}
draftMode().enable()
const redirectMap: Record<string, string> = {
posts: `/posts/${slug}`,
pages: `/${slug}`,
}
return NextResponse.redirect(
new URL(redirectMap[collection!] || '/', req.url)
)
}
Timeline
Setting up Live Preview for 2–3 content types with mobile and desktop breakpoints takes 1 day.







