Skip to content

Conversation

@AlessioGr
Copy link
Member

@AlessioGr AlessioGr commented Aug 27, 2025

Alternative solution to #11104. Big thanks to @andershermansen and @GermanJablo for kickstarting work on a solution and bringing this to our attention. This PR copies over the live-preview test suite example from his PR.

Fixes #5285, #6071 and #8277. Potentially fixes #11801

This PR completely gets rid of our client-side live preview field traversal + population and all logic related to it, and instead lets the findByID endpoint handle it.

The data sent through the live preview message event is now passed to findByID via the newly added data attribute. The findByID endpoint will then use this data and run hooks on it (which run population), instead of fetching the data from the database.

This new API basically behaves like a /api/populate?data= endpoint, with the benefit that it runs all the hooks. Another use-case for it will be rendering lexical data. Sometimes you may only have unpopulated data available. This functionality allows you to then populate the lexical portion of it on-the-fly, so that you can properly render it to JSX while displaying images.

Benefits

  • a lot less code to maintain. No duplicative population logic
  • much faster - one single API request instead of one request per relationship to populate
  • all payload features are now correctly supported (population and hooks)
    • since hooks are now running for client-side live preview, this means the lexicalHTML field is now supported! This was a long-running issue
  • this fixes a lot of population inconsistencies that we previously did not know of. For example, it previously populated lexical and slate relationships even if the data was saved in an incorrect format

Method Override (POST) change

The population request to the findByID endpoint is sent as a post request, so that we can pass through the data without having to squeeze it into the url params. To do that, it uses the X-Payload-HTTP-Method-Override header.

Previously, this functionality still expected the data to be sent through as URL search params - just passed to the body instead of the URL. In this PR, I made it possible to pass it as JSON instead. This means:

  • the receiving endpoint will receive the data under req.data and is not able to read it from the search params
  • this means existing endpoints won't support this functionality unless they also attempt to read from req.data.
  • for the purpose of this PR, the findByID endpoint was modified to support this behavior. This functionality is documented as it can be useful for user-defined endpoints as well.

Passing data as json has the following benefits:

  • it's more performant - no need to serialize and deserialize data to search params via qs-esm. This is especially important here, as we are passing large amounts of json data
  • the current implementation was serializing the data incorrectly, leading to incorrect data within nested lexical nodes

Note for people passing their own live preview requestHandler: instead of sending a GET request to populate documents, you will now need to send a POST request to the findByID endpoint and pass additional headers. Additionally, you will need to send through the arguments as JSON instead of search params and include data as an argument. Here is the updated defaultRequestHandler for reference:

const defaultRequestHandler: CollectionPopulationRequestHandler = ({
  apiPath,
  data,
  endpoint,
  serverURL,
}) => {
  const url = `${serverURL}${apiPath}/${endpoint}`

  return fetch(url, {
    body: JSON.stringify(data),
    credentials: 'include',
    headers: {
      'Content-Type': 'application/json',
      'X-Payload-HTTP-Method-Override': 'GET',
    },
    method: 'POST',
  })
}

@github-actions
Copy link
Contributor

github-actions bot commented Aug 27, 2025

📦 esbuild Bundle Analysis for payload

This analysis was generated by esbuild-bundle-analyzer. 🤖

Meta File Out File Size (raw) Note
packages/next/meta_index.json esbuild/index.js 754.07 KB ✅ No change
packages/payload/meta_index.json esbuild/index.js 1.21 MB ⚠️ +364 B (+0.0%)
packages/payload/meta_shared.json esbuild/exports/shared.js 161.04 KB ✅ -941 B (-0.6%)
packages/richtext-lexical/meta_client.json esbuild/exports/client_optimized/index.js 258.87 KB ✅ No change
packages/ui/meta_client.json esbuild/exports/client_optimized/index.js 1.15 MB ✅ -184 B (-0.0%)
packages/ui/meta_shared.json esbuild/exports/shared_optimized/index.js 14.20 KB ✅ No change
Largest paths These visualization shows top 20 largest paths in the bundle.

Meta file: packages/next/meta_index.json, Out file: esbuild/index.js

Path Size
../../node_modules ${{\color{Goldenrod}{ ████████████████████ }}}$ 80.3%, 601.88 KB
dist/views/Version ${{\color{Goldenrod}{ █▋ }}}$ 6.7%, 50.09 KB
dist/views/Document ${{\color{Goldenrod}{ ▌ }}}$ 2.0%, 14.91 KB
dist/views/List ${{\color{Goldenrod}{ ▎ }}}$ 1.2%, 9.16 KB
dist/views/Root ${{\color{Goldenrod}{ ▏ }}}$ 0.9%, 7.08 KB
dist/views/Versions ${{\color{Goldenrod}{ ▏ }}}$ 0.8%, 6.30 KB
dist/views/API ${{\color{Goldenrod}{ ▏ }}}$ 0.8%, 5.98 KB
dist/views/Account ${{\color{Goldenrod}{ ▏ }}}$ 0.7%, 5.32 KB
dist/elements/Nav ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 4.84 KB
dist/elements/DocumentHeader ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 4.81 KB
dist/views/Login ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 4.39 KB
dist/views/Dashboard ${{\color{Goldenrod}{ ▏ }}}$ 0.5%, 3.68 KB
dist/views/ForgotPassword ${{\color{Goldenrod}{ }}}$ 0.4%, 3.09 KB
dist/layouts/Root ${{\color{Goldenrod}{ }}}$ 0.4%, 3.09 KB
dist/utilities/initPage ${{\color{Goldenrod}{ }}}$ 0.4%, 3.06 KB
dist/templates/Default ${{\color{Goldenrod}{ }}}$ 0.4%, 2.83 KB
dist/views/CreateFirstUser ${{\color{Goldenrod}{ }}}$ 0.4%, 2.74 KB
dist/views/BrowseByFolder ${{\color{Goldenrod}{ }}}$ 0.3%, 2.60 KB
dist/views/CollectionFolders ${{\color{Goldenrod}{ }}}$ 0.3%, 2.46 KB
dist/views/ResetPassword ${{\color{Goldenrod}{ }}}$ 0.3%, 2.41 KB
(other) ${{\color{Goldenrod}{ ████▉ }}}$ 19.7%, 147.51 KB

Meta file: packages/payload/meta_index.json, Out file: esbuild/index.js

Path Size
../../node_modules ${{\color{Goldenrod}{ █████████████████▍ }}}$ 69.7%, 841.07 KB
dist/fields/hooks ${{\color{Goldenrod}{ ▊ }}}$ 3.3%, 40.21 KB
dist/collections/operations ${{\color{Goldenrod}{ ▊ }}}$ 3.0%, 36.05 KB
dist/auth/operations ${{\color{Goldenrod}{ ▎ }}}$ 1.3%, 15.30 KB
dist/queues/operations ${{\color{Goldenrod}{ ▎ }}}$ 1.0%, 12.00 KB
dist/utilities/configToJSONSchema.js ${{\color{Goldenrod}{ ▎ }}}$ 1.0%, 11.54 KB
dist/globals/operations ${{\color{Goldenrod}{ ▏ }}}$ 0.9%, 11.13 KB
dist/fields/config ${{\color{Goldenrod}{ ▏ }}}$ 0.9%, 10.82 KB
dist/fields/validations.js ${{\color{Goldenrod}{ ▏ }}}$ 0.8%, 9.38 KB
dist/bin/generateImportMap ${{\color{Goldenrod}{ ▏ }}}$ 0.7%, 8.27 KB
dist/database/migrations ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 7.79 KB
dist/collections/config ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 7.79 KB
dist/uploads/fetchAPI-multipart ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 7.74 KB
dist/index.js ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 7.37 KB
dist/collections/endpoints ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 7.29 KB
dist/config/orderable ${{\color{Goldenrod}{ ▏ }}}$ 0.5%, 6.25 KB
dist/auth/strategies ${{\color{Goldenrod}{ ▏ }}}$ 0.5%, 5.50 KB
dist/config/sanitize.js ${{\color{Goldenrod}{ ▏ }}}$ 0.5%, 5.44 KB
dist/auth/endpoints ${{\color{Goldenrod}{ }}}$ 0.4%, 5.41 KB
dist/utilities/telemetry ${{\color{Goldenrod}{ }}}$ 0.4%, 5.31 KB
(other) ${{\color{Goldenrod}{ ███████▌ }}}$ 30.3%, 365.96 KB

Meta file: packages/payload/meta_shared.json, Out file: esbuild/exports/shared.js

Path Size
../../node_modules ${{\color{Goldenrod}{ ████████████████████ }}}$ 80.4%, 126.93 KB
dist/fields/validations.js ${{\color{Goldenrod}{ █▍ }}}$ 5.9%, 9.38 KB
dist/fields/baseFields ${{\color{Goldenrod}{ ▍ }}}$ 1.8%, 2.79 KB
dist/utilities/deepCopyObject.js ${{\color{Goldenrod}{ ▍ }}}$ 1.6%, 2.48 KB
dist/auth/cookies.js ${{\color{Goldenrod}{ ▎ }}}$ 1.0%, 1.55 KB
dist/utilities/flattenTopLevelFields.js ${{\color{Goldenrod}{ ▏ }}}$ 0.9%, 1.41 KB
dist/fields/config ${{\color{Goldenrod}{ ▏ }}}$ 0.8%, 1.28 KB
dist/utilities/flattenAllFields.js ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 943 B
dist/folders/utils ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 916 B
dist/utilities/unflatten.js ${{\color{Goldenrod}{ ▏ }}}$ 0.5%, 779 B
dist/utilities/sanitizeUserDataForEmail.js ${{\color{Goldenrod}{ ▏ }}}$ 0.5%, 713 B
dist/collections/config ${{\color{Goldenrod}{ }}}$ 0.4%, 570 B
dist/bin/generateImportMap ${{\color{Goldenrod}{ }}}$ 0.4%, 559 B
dist/auth/sessions.js ${{\color{Goldenrod}{ }}}$ 0.3%, 545 B
dist/utilities/getSafeRedirect.js ${{\color{Goldenrod}{ }}}$ 0.3%, 423 B
dist/utilities/deepMerge.js ${{\color{Goldenrod}{ }}}$ 0.3%, 413 B
dist/utilities/getFieldPermissions.js ${{\color{Goldenrod}{ }}}$ 0.2%, 391 B
dist/utilities/formatLabels.js ${{\color{Goldenrod}{ }}}$ 0.2%, 380 B
dist/utilities/transformColumnPreferences.js ${{\color{Goldenrod}{ }}}$ 0.2%, 348 B
dist/utilities/mergeListSearchAndWhere.js ${{\color{Goldenrod}{ }}}$ 0.2%, 344 B
(other) ${{\color{Goldenrod}{ ████▉ }}}$ 19.6%, 30.90 KB

Meta file: packages/richtext-lexical/meta_client.json, Out file: esbuild/exports/client_optimized/index.js

Path Size
dist/lexical/plugins ${{\color{Goldenrod}{ ███ }}}$ 12.1%, 30.95 KB
dist/features/experimental_table ${{\color{Goldenrod}{ ██▎ }}}$ 9.2%, 23.66 KB
dist/lexical/ui ${{\color{Goldenrod}{ ██▎ }}}$ 9.2%, 23.47 KB
dist/features/blocks ${{\color{Goldenrod}{ ██▏ }}}$ 8.8%, 22.42 KB
dist/packages/@lexical ${{\color{Goldenrod}{ █▊ }}}$ 7.4%, 18.99 KB
dist/features/link ${{\color{Goldenrod}{ █▊ }}}$ 7.0%, 17.96 KB
dist/features/toolbars ${{\color{Goldenrod}{ █▋ }}}$ 6.9%, 17.68 KB
dist/features/textState ${{\color{Goldenrod}{ █ }}}$ 4.3%, 11.02 KB
dist/features/upload ${{\color{Goldenrod}{ ▉ }}}$ 3.8%, 9.60 KB
dist/features/relationship ${{\color{Goldenrod}{ ▉ }}}$ 3.7%, 9.41 KB
dist/lexical/utils ${{\color{Goldenrod}{ ▊ }}}$ 3.2%, 8.08 KB
dist/features/debug ${{\color{Goldenrod}{ ▋ }}}$ 2.9%, 7.39 KB
dist/features/converters ${{\color{Goldenrod}{ ▋ }}}$ 2.8%, 7.04 KB
dist/utilities/fieldsDrawer ${{\color{Goldenrod}{ ▋ }}}$ 2.7%, 7.01 KB
dist/lexical/config ${{\color{Goldenrod}{ ▌ }}}$ 2.0%, 5.10 KB
dist/features/lists ${{\color{Goldenrod}{ ▍ }}}$ 1.9%, 4.95 KB
dist/lexical/theme ${{\color{Goldenrod}{ ▍ }}}$ 1.6%, 4.01 KB
dist/features/format ${{\color{Goldenrod}{ ▎ }}}$ 1.4%, 3.46 KB
dist/lexical/LexicalEditor.js ${{\color{Goldenrod}{ ▎ }}}$ 1.4%, 3.46 KB
dist/features/indent ${{\color{Goldenrod}{ ▎ }}}$ 1.0%, 2.50 KB
(other) ${{\color{Goldenrod}{ █████████████████████▉ }}}$ 87.9%, 224.89 KB

Meta file: packages/ui/meta_client.json, Out file: esbuild/exports/client_optimized/index.js

Path Size
../../node_modules ${{\color{Goldenrod}{ ████████████▌ }}}$ 50.2%, 572.93 KB
dist/elements/FolderView ${{\color{Goldenrod}{ ▋ }}}$ 2.7%, 30.37 KB
dist/elements/BulkUpload ${{\color{Goldenrod}{ ▌ }}}$ 2.2%, 24.57 KB
dist/elements/WhereBuilder ${{\color{Goldenrod}{ ▎ }}}$ 1.4%, 16.26 KB
dist/fields/Relationship ${{\color{Goldenrod}{ ▎ }}}$ 1.4%, 16.11 KB
dist/views/Edit ${{\color{Goldenrod}{ ▎ }}}$ 1.4%, 15.43 KB
dist/elements/Table ${{\color{Goldenrod}{ ▎ }}}$ 1.3%, 15.26 KB
dist/forms/Form ${{\color{Goldenrod}{ ▎ }}}$ 1.3%, 14.77 KB
dist/fields/Blocks ${{\color{Goldenrod}{ ▎ }}}$ 1.1%, 12.43 KB
dist/fields/Upload ${{\color{Goldenrod}{ ▎ }}}$ 1.0%, 11.94 KB
dist/fields/Array ${{\color{Goldenrod}{ ▏ }}}$ 0.8%, 9.30 KB
dist/elements/QueryPresets ${{\color{Goldenrod}{ ▏ }}}$ 0.8%, 9.08 KB
dist/elements/PublishButton ${{\color{Goldenrod}{ ▏ }}}$ 0.8%, 8.72 KB
dist/providers/Folders ${{\color{Goldenrod}{ ▏ }}}$ 0.7%, 8.38 KB
dist/elements/LivePreview ${{\color{Goldenrod}{ ▏ }}}$ 0.7%, 8.26 KB
dist/elements/ListHeader ${{\color{Goldenrod}{ ▏ }}}$ 0.7%, 7.98 KB
dist/views/CollectionFolder ${{\color{Goldenrod}{ ▏ }}}$ 0.7%, 7.89 KB
dist/elements/HTMLDiff ${{\color{Goldenrod}{ ▏ }}}$ 0.7%, 7.81 KB
dist/views/List ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 7.18 KB
dist/elements/ReactSelect ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 6.91 KB
(other) ${{\color{Goldenrod}{ ████████████▍ }}}$ 49.8%, 567.92 KB

Meta file: packages/ui/meta_shared.json, Out file: esbuild/exports/shared_optimized/index.js

Path Size
dist/graphics/Logo ${{\color{Goldenrod}{ █████▋ }}}$ 22.9%, 3.12 KB
../../node_modules ${{\color{Goldenrod}{ ████▉ }}}$ 19.5%, 2.65 KB
dist/graphics/Icon ${{\color{Goldenrod}{ ██▊ }}}$ 11.2%, 1.52 KB
dist/utilities/formatDocTitle ${{\color{Goldenrod}{ ██▍ }}}$ 9.7%, 1.32 KB
dist/providers/TableColumns ${{\color{Goldenrod}{ █▌ }}}$ 6.3%, 862 B
dist/utilities/groupNavItems.js ${{\color{Goldenrod}{ █▌ }}}$ 6.0%, 814 B
dist/utilities/api.js ${{\color{Goldenrod}{ █▍ }}}$ 5.6%, 756 B
dist/elements/Translation ${{\color{Goldenrod}{ ▉ }}}$ 3.6%, 493 B
dist/elements/withMergedProps ${{\color{Goldenrod}{ ▋ }}}$ 2.5%, 339 B
dist/utilities/handleTakeOver.js ${{\color{Goldenrod}{ ▍ }}}$ 1.8%, 251 B
dist/elements/WithServerSideProps ${{\color{Goldenrod}{ ▍ }}}$ 1.7%, 232 B
dist/utilities/handleGoBack.js ${{\color{Goldenrod}{ ▎ }}}$ 1.2%, 168 B
dist/fields/mergeFieldStyles.js ${{\color{Goldenrod}{ ▎ }}}$ 1.2%, 159 B
dist/forms/Form ${{\color{Goldenrod}{ ▎ }}}$ 1.1%, 147 B
dist/utilities/abortAndIgnore.js ${{\color{Goldenrod}{ ▎ }}}$ 1.1%, 146 B
dist/utilities/hasSavePermission.js ${{\color{Goldenrod}{ ▎ }}}$ 1.0%, 136 B
dist/utilities/handleBackToDashboard.js ${{\color{Goldenrod}{ ▏ }}}$ 0.9%, 129 B
dist/utilities/findLocaleFromCode.js ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 84 B
dist/utilities/sanitizeID.js ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 77 B
dist/utilities/isEditing.js ${{\color{Goldenrod}{ }}}$ 0.4%, 59 B
(other) ${{\color{Goldenrod}{ ███████████████████▎ }}}$ 77.1%, 10.49 KB
Details

Next to the size is how much the size has increased or decreased compared with the base branch of this PR.

  • ‼️: Size increased by 20% or more. Special attention should be given to this.
  • ⚠️: Size increased in acceptable range (lower than 20%).
  • ✅: No change or even downsized.
  • 🗑️: The out file is deleted: not found in base branch.
  • 🆕: The out file is newly found: will be added to base branch.

@andershermansen
Copy link
Contributor

Thanks for looking into this @AlessioGr, this simplifies things a lot 🤩
You then don't event need the fieldsSchemaJSON anymore 👍

@AlessioGr AlessioGr marked this pull request as ready for review September 4, 2025 18:37
@andershermansen
Copy link
Contributor

@AlessioGr I tried this change in one of my projects 👀
You tried to hide it from me, but I tracked down your secret stash: release 3.55.0-internal.df60e35 🕵️‍♂️🐛✨

It seems to work great with populating data in my blocks inside lexical 🤩

One tiny issue is that the new header has to be part of CORS. So I had to add:

    cors: {
        origins: [process.env.WEBSITE_URL!],
        headers: ['x-payload-http-method-override'],
    },

to my payload config before I got it working. Since my website and cms is running in different projects.

I think that x-payload-http-method-override should be part of defaultAllowedHeaders so that this configuration is not necessary.

const defaultAllowedHeaders = [
'Origin',
'X-Requested-With',
'Content-Type',
'Accept',
'Authorization',
'Content-Encoding',
'x-apollo-tracing',
]

@AlessioGr AlessioGr enabled auto-merge (squash) September 5, 2025 18:40
Copy link
Member

@jacobsfletch jacobsfletch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is so good it makes me sick

@AlessioGr AlessioGr merged commit 6e203db into main Sep 5, 2025
163 of 166 checks passed
@AlessioGr AlessioGr deleted the refactor/simpler-live-preview branch September 5, 2025 18:40
AlessioGr pushed a commit that referenced this pull request Sep 5, 2025
#13717)

### What?

Set  X-Payload-HTTP-Method-Override as allowed cross origin header

### Why?

As of #13619 the live preview uses POST method to retrieve updated data.
When running cms and website on different origins, the cross origin
requires X-Payload-HTTP-Method-Override header to be allowed

### How?

Adding X-Payload-HTTP-Method-Override as a default allowed header
@github-actions
Copy link
Contributor

github-actions bot commented Sep 9, 2025

🚀 This is included in version v3.55.0

@notflip
Copy link

notflip commented Sep 9, 2025

What does this mean for the implementation of Live Preview? Does this change anything for developers?

@AlessioGr
Copy link
Member Author

What does this mean for the implementation of Live Preview? Does this change anything for developers?

Only if you pass your own requestHandler or call mergeData directly. This change should not change anything for 99.999% of developers:

  • none of our templates or e2e tests required adjusting
  • none of our first-party framework packages (live preview react and vue) required adjusting - thus there's unlikely to be any breakage for custom implementations for different frameworks

@codeflorist
Copy link
Contributor

codeflorist commented Sep 17, 2025

@AlessioGr It seems that before this change an updated version was sent immediately to the client window on each keypress even without autosave enabled, which was great and imho how a live preview should work.

Now this doesn't seem to work anymore. 🫤 On the one hand one has to enable autosave, on the other hand, each keypress has to wait until autosave is completed and until the frontend fetches the new draft version from the server, which is a significant delay.

Unfortunately this is quite the decrease in user experience. 🫤

EDIT: The Live Preview docs also doesn't mention anywhere, that this only works with autosave enabled, which seems to be the case now. So this is kind-of a feature-regression. Is there any way to get the old behaviour back?

@andershermansen
Copy link
Contributor

@codeflorist For us it works this way still, so I think this is something project specific on your end. Are you able to create an issue with a minimal reproduction?

@codeflorist
Copy link
Contributor

@codeflorist For us it works this way still, so I think this is something project specific on your end. Are you able to create an issue with a minimal reproduction?

I'm one of the lucky 0.001% of developers, who is using his own requestHandler.

I've just spent about a week to get live preview to work with my Nuxt frontend, now everything is broken again. 😖

When my requestHandler sends it's post request, i get a 403.

When i send the post request directly, it works, when only the X-Payload-HTTP-Method-Override: GET header is present, once i add Content-Type: application/json, it results in a 403. 🤡

Oh well, here goes another week...

@andershermansen
Copy link
Contributor

When my requestHandler sends it's post request, i get a 403.

credentials: 'include' is missing?

@codeflorist
Copy link
Contributor

When my requestHandler sends it's post request, i get a 403.

credentials: 'include' is missing?

@andershermansen Thanks for your help!

I basically got it working again, so it gets updated on keypress. I forgot to add &draft=true.

But it does not work for rich-text (inside a block). Does this work for you?

The rich-text field is completely missing from the object returned by the X-Payload-HTTP-Method-Override call, and thus is removed from the preview-page completely.

@andershermansen
Copy link
Contributor

But it does not work for rich-text (inside a block). Does this work for you?

I use blocks inside rich-text, not richtext inside blocks. But I tested this in the payload monorepo now, the live-preview test already contains this setup. And it works fine there:

Skjermbilde 2025-09-17 kl  15 09 38

@codeflorist
Copy link
Contributor

But it does not work for rich-text (inside a block). Does this work for you?

I use blocks inside rich-text, not richtext inside blocks. But I tested this in the payload monorepo now, the live-preview test already contains this setup. And it works fine there.

It was this issue, which only affected localized rich-text fields: #13756

Upgrading to the just now released v3.56.0 fixes the problem.

Many thanks again @andershermansen for your efforts!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Virtual Fields do not update in Live Preview mode Live preview does not correctly find relations when using blocks inside the lexical rte.

5 participants