Server-side Live Preview

Server-side Live Preview works by making a roundtrip to the server every time your document is saved, i.e. draft save, autosave, or publish. While using Live Preview, the Admin Panel emits a new window.postMessage event which your front-end application can use to invoke this process. In Next.js, this means simply calling router.refresh() which will hydrate the HTML using new data straight from the Local API.

If your front-end application is built with React, you can use the RefreshRouteOnChange function that Payload provides. In the future, all other major frameworks like Vue and Svelte will be officially supported. If you are using any of these frameworks today, you can still integrate with Live Preview yourself using the underlying tooling that Payload provides. See building your own router refresh component for more information.

React

If your front-end application is built with server-side React like Next.js App Router, you can use the RefreshRouteOnSave component that Payload provides.

First, install the @payloadcms/live-preview-react package:

1
npm install @payloadcms/live-preview-react

Then, render the RefreshRouteOnSave component anywhere in your page.tsx. Here's an example:

page.tsx:

1
import { RefreshRouteOnSave } from './RefreshRouteOnSave.tsx'
2
import { getPayloadHMR } from '@payloadcms/next'
3
import config from '../payload.config'
4
5
export default async function Page() {
6
const payload = await getPayloadHMR({ config })
7
8
const page = await payload.findByID({
9
collection: 'pages',
10
id: '123',
11
draft: true
12
})
13
14
return (
15
<Fragment>
16
<RefreshRouteOnSave />
17
<h1>{page.title}</h1>
18
</Fragment>
19
)
20
}

RefreshRouteOnSave.tsx:

1
'use client'
2
import { RefreshRouteOnSave as PayloadLivePreview } from '@payloadcms/live-preview-react'
3
import { useRouter } from 'next/navigation.js'
4
import React from 'react'
5
6
export const RefreshRouteOnSave: React.FC = () => {
7
const router = useRouter()
8
9
return (
10
<PayloadLivePreview
11
refresh={() => router.refresh()}
12
serverURL={process.env.NEXT_PUBLIC_PAYLOAD_URL}
13
/>
14
)
15
}

Building your own router refresh component

No matter what front-end framework you are using, you can build your own component using the same underlying tooling that Payload provides.

First, install the base @payloadcms/live-preview package:

1
npm install @payloadcms/live-preview

This package provides the following functions:

PathDescription
readySends a window.postMessage event to the Admin Panel to indicate that the front-end is ready to receive messages.
isDocumentEventChecks if a MessageEvent originates from the Admin Panel and is a document-level event, i.e. draft save, autosave, publish, etc.

With these functions, you can build your own hook using your front-end framework of choice:

1
import { ready, isDocumentEvent } from '@payloadcms/live-preview'
2
3
// To build your own component:
4
// 1. Listen for document-level `window.postMessage` events sent from the Admin Panel
5
// 2. Tell the Admin Panel when it is ready to receive messages
6
// 3. Refresh the route every time a new document-level event is received
7
// 4. Unsubscribe from the `window.postMessage` events when it unmounts

Here is an example of what the same RefreshRouteOnSave React component from above looks like under the hood:

1
'use client'
2
3
import type React from 'react'
4
5
import { isDocumentEvent, ready } from '@payloadcms/live-preview'
6
import { useCallback, useEffect, useRef } from 'react'
7
8
export const RefreshRouteOnSave: React.FC<{
9
apiRoute?: string
10
depth?: number
11
refresh: () => void
12
serverURL: string
13
}> = (props) => {
14
const { apiRoute, depth, refresh, serverURL } = props
15
const hasSentReadyMessage = useRef<boolean>(false)
16
17
const onMessage = useCallback(
18
(event: MessageEvent) => {
19
if (isDocumentEvent(event, serverURL)) {
20
if (typeof refresh === 'function') {
21
refresh()
22
}
23
}
24
},
25
[refresh, serverURL],
26
)
27
28
useEffect(() => {
29
if (typeof window !== 'undefined') {
30
window.addEventListener('message', onMessage)
31
}
32
33
if (!hasSentReadyMessage.current) {
34
hasSentReadyMessage.current = true
35
36
ready({
37
serverURL,
38
})
39
}
40
41
return () => {
42
if (typeof window !== 'undefined') {
43
window.removeEventListener('message', onMessage)
44
}
45
}
46
}, [serverURL, onMessage, depth, apiRoute])
47
48
return null
49
}

Example

For a working demonstration of this, check out the official Live Preview Example. There you will find a fully working example of how to implement Live Preview in your Next.js App Router application.

Troubleshooting

Updates do not appear as fast as client-side Live Preview

If you are noticing that updates feel less snappy than client-side Live Preview (i.e. the useLivePreview hook), this is because of how the two differ in how they work—instead of emitting events against form state, server-side Live Preview refreshes the route after a new document is saved.

Use Autosave to mimic this effect server-side. Try decreasing the value of versions.autoSave.interval to make the experience feel more responsive:

1
// collection.ts
2
{
3
versions: {
4
drafts: {
5
autosave: {
6
interval: 375,
7
},
8
},
9
},
10
}

Iframe refuses to connect

If your front-end application has set a Content Security Policy (CSP) that blocks the Admin Panel from loading your front-end application, the iframe will not be able to load your site. To resolve this, you can whitelist the Admin Panel's domain in your CSP by setting the frame-ancestors directive:

1
frame-ancestors: "self" localhost:* https://your-site.com;
Next

Client-side Live Preview