### What?
This PR introduces complete trash (soft-delete) support. When a
collection is configured with `trash: true`, documents can now be
soft-deleted and restored via both the API and the admin panel.
```
import type { CollectionConfig } from 'payload'
const Posts: CollectionConfig = {
slug: 'posts',
trash: true, // <-- New collection config prop @default false
fields: [
{
name: 'title',
type: 'text',
},
// other fields...
],
}
```
### Why
Soft deletes allow developers and admins to safely remove documents
without losing data immediately. This enables workflows like reversible
deletions, trash views, and auditing—while preserving compatibility with
drafts, autosave, and version history.
### How?
#### Backend
- Adds new `trash: true` config option to collections.
- When enabled:
- A `deletedAt` timestamp is conditionally injected into the schema.
- Soft deletion is performed by setting `deletedAt` instead of removing
the document from the database.
- Extends all relevant API operations (`find`, `findByID`, `update`,
`delete`, `versions`, etc.) to support a new `trash` param:
- `trash: false` → excludes trashed documents (default)
- `trash: true` → includes both trashed and non-trashed documents
- To query **only trashed** documents: use `trash: true` with a `where`
clause like `{ deletedAt: { exists: true } }`
- Enforces delete access control before allowing a soft delete via
update or updateByID.
- Disables version restoring on trashed documents (must be restored
first).
#### Admin Panel
- Adds a dedicated **Trash view**: `/collections/:collectionSlug/trash`
- Default delete action now soft-deletes documents when `trash: true` is
set.
- **Delete confirmation modal** includes a checkbox to permanently
delete instead.
- Trashed documents:
- Displays UI banner for better clarity of trashed document edit view vs
non-trashed document edit view
- Render in a read-only edit view
- Still allow access to **Preview**, **API**, and **Versions** tabs
- Updated Status component:
- Displays “Previously published” or “Previously a draft” for trashed
documents.
- Disables status-changing actions when documents are in trash.
- Adds new **Restore** bulk action to clear the `deletedAt` timestamp.
- New `Restore` and `Permanently Delete` buttons for
single-trashed-document restore and permanent deletion.
- **Restore confirmation modal** includes a checkbox to restore as
`published`, defaults to `draft`.
- Adds **Empty Trash** and **Delete permanently** bulk actions.
#### Notes
- This feature is completely opt-in. Collections without trash: true
behave exactly as before.
https://github.com/user-attachments/assets/00b83f8a-0442-441e-a89e-d5dc1f49dd37
201 lines
7.2 KiB
Plaintext
201 lines
7.2 KiB
Plaintext
---
|
|
title: Server-side Live Preview
|
|
label: Server-side
|
|
order: 30
|
|
desc: Learn how to implement Live Preview in your server-side front-end application.
|
|
keywords: live preview, frontend, react, next.js, vue, nuxt.js, svelte, hook, useLivePreview
|
|
---
|
|
|
|
<Banner type="info">
|
|
Server-side Live Preview is only for front-end frameworks that support the
|
|
concept of Server Components, i.e. [React Server
|
|
Components](https://react.dev/reference/rsc/server-components). If your
|
|
front-end application is built with a client-side framework like the [Next.js
|
|
Pages Router](https://nextjs.org/docs/pages), [React
|
|
Router](https://reactrouter.com), [Vue 3](https://vuejs.org), etc., see
|
|
[client-side Live Preview](./client).
|
|
</Banner>
|
|
|
|
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](../local-api/overview).
|
|
|
|
<Banner type="warning">
|
|
It is recommended that you enable [Autosave](../versions/autosave) alongside
|
|
Live Preview to make the experience feel more responsive.
|
|
</Banner>
|
|
|
|
If your front-end application is built with [React](#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](#building-your-own-router-refresh-component) for more information.
|
|
|
|
## React
|
|
|
|
If your front-end application is built with server-side [React](https://react.dev) like [Next.js App Router](https://nextjs.org/docs/app), you can use the `RefreshRouteOnSave` component that Payload provides.
|
|
|
|
First, install the `@payloadcms/live-preview-react` package:
|
|
|
|
```bash
|
|
npm install @payloadcms/live-preview-react
|
|
```
|
|
|
|
Then, render the `RefreshRouteOnSave` component anywhere in your `page.tsx`. Here's an example:
|
|
|
|
`page.tsx`:
|
|
|
|
```tsx
|
|
import { RefreshRouteOnSave } from './RefreshRouteOnSave.tsx'
|
|
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,
|
|
trash: true, // add this if trash is enabled in your collection and want to preview trashed documents
|
|
})
|
|
|
|
return (
|
|
<Fragment>
|
|
<RefreshRouteOnSave />
|
|
<h1>{page.title}</h1>
|
|
</Fragment>
|
|
)
|
|
}
|
|
```
|
|
|
|
`RefreshRouteOnSave.tsx`:
|
|
|
|
```tsx
|
|
'use client'
|
|
import { RefreshRouteOnSave as PayloadLivePreview } from '@payloadcms/live-preview-react'
|
|
import { useRouter } from 'next/navigation.js'
|
|
import React from 'react'
|
|
|
|
export const RefreshRouteOnSave: React.FC = () => {
|
|
const router = useRouter()
|
|
|
|
return (
|
|
<PayloadLivePreview
|
|
refresh={() => router.refresh()}
|
|
serverURL={process.env.NEXT_PUBLIC_PAYLOAD_URL}
|
|
/>
|
|
)
|
|
}
|
|
```
|
|
|
|
## 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:
|
|
|
|
```bash
|
|
npm install @payloadcms/live-preview
|
|
```
|
|
|
|
This package provides the following functions:
|
|
|
|
| Path | Description |
|
|
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
|
| **`ready`** | Sends a `window.postMessage` event to the Admin Panel to indicate that the front-end is ready to receive messages. |
|
|
| **`isDocumentEvent`** | Checks 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:
|
|
|
|
```tsx
|
|
import { ready, isDocumentEvent } from '@payloadcms/live-preview'
|
|
|
|
// To build your own component:
|
|
// 1. Listen for document-level `window.postMessage` events sent from the Admin Panel
|
|
// 2. Tell the Admin Panel when it is ready to receive messages
|
|
// 3. Refresh the route every time a new document-level event is received
|
|
// 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:
|
|
|
|
```tsx
|
|
'use client'
|
|
|
|
import type React from 'react'
|
|
|
|
import { isDocumentEvent, ready } from '@payloadcms/live-preview'
|
|
import { useCallback, useEffect, useRef } from 'react'
|
|
|
|
export const RefreshRouteOnSave: React.FC<{
|
|
apiRoute?: string
|
|
depth?: number
|
|
refresh: () => void
|
|
serverURL: string
|
|
}> = (props) => {
|
|
const { apiRoute, depth, refresh, serverURL } = props
|
|
const hasSentReadyMessage = useRef<boolean>(false)
|
|
|
|
const onMessage = useCallback(
|
|
(event: MessageEvent) => {
|
|
if (isDocumentEvent(event, serverURL)) {
|
|
if (typeof refresh === 'function') {
|
|
refresh()
|
|
}
|
|
}
|
|
},
|
|
[refresh, serverURL],
|
|
)
|
|
|
|
useEffect(() => {
|
|
if (typeof window !== 'undefined') {
|
|
window.addEventListener('message', onMessage)
|
|
}
|
|
|
|
if (!hasSentReadyMessage.current) {
|
|
hasSentReadyMessage.current = true
|
|
|
|
ready({
|
|
serverURL,
|
|
})
|
|
}
|
|
|
|
return () => {
|
|
if (typeof window !== 'undefined') {
|
|
window.removeEventListener('message', onMessage)
|
|
}
|
|
}
|
|
}, [serverURL, onMessage, depth, apiRoute])
|
|
|
|
return null
|
|
}
|
|
```
|
|
|
|
## Example
|
|
|
|
For a working demonstration of this, check out the official [Live Preview Example](https://github.com/payloadcms/payload/tree/main/examples/live-preview). 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](../versions/autosave) to mimic this effect server-side. Try decreasing the value of `versions.autoSave.interval` to make the experience feel more responsive:
|
|
|
|
```ts
|
|
// collection.ts
|
|
{
|
|
versions: {
|
|
drafts: {
|
|
autosave: {
|
|
interval: 375,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
```
|
|
|
|
#### Iframe refuses to connect
|
|
|
|
If your front-end application has set a [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) (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:
|
|
|
|
```plaintext
|
|
frame-ancestors: "self" localhost:* https://your-site.com;
|
|
```
|