fix(live-preview): reset cache per subscription and ignore invalid preview messages (#13793)

### What?

Fix two live preview issues affecting client-side navigation:
1. Stale preview data leaking between pages using `useLivePreview`.
2. Erroneous fetches to `/api/undefined` and incorrect content rendering
when preview events lack slugs.

### Why?

The live-preview module cached merged preview data globally, which
persisted across route changes, causing a new page to render with the
previous page’s data.

The client attempted to merge when preview events didn’t specify
collectionSlug or globalSlug, producing an endpoint of undefined and
triggering requests to /api/undefined, sometimes overwriting state with
mismatched content.

### How?

Clear the internal cache at the time of `subscribe()` so each page using
`useLivePreview` starts from a clean slate.

In `handleMessage`, only call `mergeData` when `collectionSlug` or
`globalSlug` is present; otherwise return `initialData` and perform no
request.

Fixes #13792
This commit is contained in:
Anders Semb Hermansen
2025-09-12 20:40:24 +02:00
committed by GitHub
parent dfb0021545
commit b62a30a8dc
5 changed files with 58 additions and 3 deletions

View File

@@ -14,6 +14,12 @@ const _payloadLivePreview: {
previousData: undefined,
}
// Reset the internal cached merged data. This is useful when navigating
// between routes where a new subscription should not inherit prior data.
export const resetCache = (): void => {
_payloadLivePreview.previousData = undefined
}
export const handleMessage = async <T extends Record<string, any>>(args: {
apiRoute?: string
depth?: number
@@ -27,6 +33,12 @@ export const handleMessage = async <T extends Record<string, any>>(args: {
if (isLivePreviewEvent(event, serverURL)) {
const { collectionSlug, data, globalSlug, locale } = event.data
// Only attempt to merge when we have a clear target
// Either a collectionSlug or a globalSlug must be present
if (!collectionSlug && !globalSlug) {
return initialData
}
const mergedData = await mergeData<T>({
apiRoute,
collectionSlug,

View File

@@ -1,6 +1,6 @@
import type { CollectionPopulationRequestHandler } from './types.js'
import { handleMessage } from './handleMessage.js'
import { handleMessage, resetCache } from './handleMessage.js'
export const subscribe = <T extends Record<string, any>>(args: {
apiRoute?: string
@@ -12,6 +12,10 @@ export const subscribe = <T extends Record<string, any>>(args: {
}): ((event: MessageEvent) => Promise<void> | void) => {
const { apiRoute, callback, depth, initialData, requestHandler, serverURL } = args
// Ensure previous subscription state does not leak across navigations
// by clearing the internal cached data before subscribing.
resetCache()
const onMessage = async (event: MessageEvent) => {
const mergedData = await handleMessage<T>({
apiRoute,

View File

@@ -36,7 +36,7 @@ export const CMSLink: React.FC<CMSLinkType> = ({
}) => {
const href =
type === 'reference' && typeof reference?.value === 'object' && reference.value.slug
? `/live-preview/${reference.value.slug}`
? `/live-preview${reference.relationTo === 'posts' ? '/posts' : ''}/${reference.value.slug}`
: url
if (!href) {

View File

@@ -13,5 +13,38 @@ export const header: Partial<Header> = {
label: 'Posts',
},
},
{
link: {
type: 'reference',
url: '',
reference: {
relationTo: 'posts',
value: '{{POST_1_ID}}',
},
label: 'Post 1',
},
},
{
link: {
type: 'reference',
url: '',
reference: {
relationTo: 'posts',
value: '{{POST_2_ID}}',
},
label: 'Post 2',
},
},
{
link: {
type: 'reference',
url: '',
reference: {
relationTo: 'posts',
value: '{{POST_3_ID}}',
},
label: 'Post 3',
},
},
],
}

View File

@@ -168,7 +168,13 @@ export const seed: Config['onInit'] = async (payload) => {
await payload.updateGlobal({
slug: 'header',
data: JSON.parse(JSON.stringify(header).replace(/"\{\{POSTS_PAGE_ID\}\}"/g, postsPageDocID)),
data: JSON.parse(
JSON.stringify(header)
.replace(/"\{\{POSTS_PAGE_ID\}\}"/g, postsPageDocID)
.replace(/"\{\{POST_1_ID\}\}"/g, post1DocID)
.replace(/"\{\{POST_2_ID\}\}"/g, post2DocID)
.replace(/"\{\{POST_3_ID\}\}"/g, post3DocID),
),
})
await payload.updateGlobal({