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

4.8 KiB

title aliases tags sources created updated
Live Preview — Server-Side
server-live-preview
RefreshRouteOnSave
server-components-live-preview
payloadcms
live-preview
nextjs
react-server-components
drafts
raw/live-preview__server.md
2026-05-15 2026-05-15

Overview

Server-side Live Preview is for frameworks that support React Server Components (e.g. Next.js App Router). It works differently from wiki/payloadcms/live-preview-client: instead of streaming form-state diffs, it triggers a full server roundtrip on every document save (draft save, autosave, or publish).

Flow: Admin Panel emits window.postMessage → front-end catches it → calls router.refresh() → Next.js re-fetches fresh data from the wiki/payloadcms/local-api.

Not applicable to: Next.js Pages Router, React Router, Vue 3, Nuxt.js, Svelte (use wiki/payloadcms/live-preview-client instead).


React / Next.js App Router

Install

npm install @payloadcms/live-preview-react

Usage pattern

Two files: a server page component + a thin 'use client' wrapper.

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.js'

export const RefreshRouteOnSave = () => {
  const router = useRouter()
  return (
    <PayloadLivePreview
      refresh={() => router.refresh()}
      serverURL={process.env.NEXT_PUBLIC_PAYLOAD_URL}
    />
  )
}

The RefreshRouteOnSave component must be a Client Component because it subscribes to window.postMessage.


Building Your Own Refresh Component

Install the base package (framework-agnostic):

npm install @payloadcms/live-preview
Export Purpose
ready({ serverURL }) Tells Admin Panel the front-end is ready to receive messages
isDocumentEvent(event, serverURL) Checks if a MessageEvent is a document-level event from the Admin Panel

Minimal implementation

import { isDocumentEvent, ready } from '@payloadcms/live-preview'
import { useCallback, useEffect, useRef } from 'react'

export const RefreshRouteOnSave = ({ refresh, serverURL }) => {
  const hasSentReadyMessage = useRef(false)

  const onMessage = useCallback((event) => {
    if (isDocumentEvent(event, serverURL)) refresh()
  }, [refresh, serverURL])

  useEffect(() => {
    window.addEventListener('message', onMessage)
    if (!hasSentReadyMessage.current) {
      hasSentReadyMessage.current = true
      ready({ serverURL })
    }
    return () => window.removeEventListener('message', onMessage)
  }, [serverURL, onMessage])

  return null
}

Key Takeaways

  • Server-side only for RSC frameworks — requires router.refresh() (Next.js App Router pattern); not for SPA-style routing
  • Roundtrip on save — less real-time than client-side (useLivePreview); updates appear only after a document save event
  • Enable Autosave — set versions.drafts.autosave.interval: 375 to make server-side feel responsive
  • Split Client/ServerRefreshRouteOnSave must be 'use client'; the page component stays a Server Component
  • Base package@payloadcms/live-preview provides ready + isDocumentEvent for custom non-React implementations
  • CSP gotcha — if your front-end sets Content-Security-Policy, add frame-ancestors: "self" localhost:* https://your-site.com to allow the Admin Panel iframe

Troubleshooting

Symptom Fix
Updates lag compared to client-side Enable Autosave with low interval (375ms)
Iframe refuses to connect Add Admin Panel domain to CSP frame-ancestors directive


Sources