payload-live-preview 2.1.1 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +170 -55
- package/dist/adapters/astro/index.d.ts +7 -7
- package/dist/adapters/astro/index.js +1 -1
- package/dist/adapters/astro/index.js.map +1 -1
- package/dist/adapters/astro/middleware-entry.d.ts +3 -3
- package/dist/adapters/astro/middleware-entry.js +1 -1
- package/dist/adapters/astro/middleware-entry.js.map +1 -1
- package/dist/adapters/nextjs/index.d.ts +2 -2
- package/dist/adapters/nextjs/index.js +1 -1
- package/dist/adapters/nextjs/index.js.map +1 -1
- package/dist/adapters/nuxt/index.d.ts +4 -4
- package/dist/adapters/nuxt/index.js +1 -1
- package/dist/adapters/nuxt/index.js.map +1 -1
- package/dist/adapters/nuxt/module.d.ts +1 -1
- package/dist/adapters/sveltekit/index.d.ts +5 -5
- package/dist/adapters/sveltekit/index.js +1 -1
- package/dist/adapters/sveltekit/index.js.map +1 -1
- package/dist/client.cjs +1 -1
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +2 -2
- package/dist/client.d.ts +2 -2
- package/dist/client.js +1 -1
- package/dist/client.js.map +1 -1
- package/dist/core.cjs +1 -1
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.cts +2 -2
- package/dist/core.d.ts +2 -2
- package/dist/core.js +1 -1
- package/dist/core.js.map +1 -1
- package/dist/doctor-cli.d.ts +1 -1
- package/dist/doctor-cli.js +1 -1
- package/dist/doctor-cli.js.map +1 -1
- package/dist/doctor.d.ts +15 -2
- package/dist/doctor.js +1 -1
- package/dist/doctor.js.map +1 -1
- package/dist/{fragment-endpoint-B2qcRw5T.d.ts → fragment-endpoint-DGGLIijM.d.ts} +1 -1
- package/dist/fragment.cjs.map +1 -1
- package/dist/fragment.d.cts +2 -2
- package/dist/fragment.d.ts +2 -2
- package/dist/fragment.js.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/lean.cjs +1 -1
- package/dist/lean.cjs.map +1 -1
- package/dist/lean.js +1 -1
- package/dist/lean.js.map +1 -1
- package/dist/{locals-B1mF3aTb.d.ts → locals-zz2gi9dj.d.ts} +1 -1
- package/dist/{middleware-BGobLN9P.d.ts → middleware-CI-UTf6R.d.ts} +2 -2
- package/dist/{options-Cm8fC7dj.d.ts → options-CnYybG51.d.ts} +8 -0
- package/dist/plugins.cjs +1 -1
- package/dist/plugins.cjs.map +1 -1
- package/dist/plugins.d.cts +2 -2
- package/dist/plugins.d.ts +2 -2
- package/dist/plugins.js +1 -1
- package/dist/plugins.js.map +1 -1
- package/dist/{probe-DGPDsnbj.d.ts → probe-DpMZ4fOS.d.ts} +2 -2
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[](https://github.com/relative23/payload-live-preview/actions/workflows/ci.yml)
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
[](package.json)
|
|
6
|
-
[](
|
|
6
|
+
[](#compatibility)
|
|
7
7
|
[](https://www.npmjs.com/package/payload-live-preview)
|
|
8
8
|
|
|
9
9
|

|
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
|
|
13
13
|
Payload publishes the framework-independent `@payloadcms/live-preview` client and the official `@payloadcms/live-preview-react` and `@payloadcms/live-preview-vue` wrappers. Those hooks feed a hydrated component tree; they do not own server markup outside that tree. This package covers that markup. Mark one boundary per server-rendered component, or individual fields on a static page, and edits reach the iframe as the editor types. Scalar patches and successful fragment renders need neither a rebuild nor a full-page reload; a route-level change uses the configured route refresh. Client-rendered apps can instead use this package's React or Vue entry. Its v2 sanitizer and request-time policy defaults are strict; static delivery has no request to authorize. The package has no direct dependencies; framework entries use optional peers.
|
|
14
14
|
|
|
15
|
-
One runtime serves every framework, and
|
|
15
|
+
One runtime serves every framework. The Astro, Next.js, SvelteKit and Nuxt adapters authorize a preview request, merge `frame-ancestors` into your CSP and render fragment boundaries on your server, and each adapter's example app runs end to end against a real Payload admin in Chromium, Firefox and WebKit on every push to `main` and on pull requests. Where the adapters differ, the difference comes from the framework: [Frameworks](#frameworks) lists it.
|
|
16
16
|
|
|
17
|
-
**New here
|
|
17
|
+
**New here?** Configure Payload, then add your framework's adapter under [Quick start](#quick-start). The [documentation index](docs/README.md) has the reading path.
|
|
18
18
|
|
|
19
19
|
## Highlights
|
|
20
20
|
|
|
@@ -22,7 +22,7 @@ One runtime serves every framework, and every framework adapter's example app ru
|
|
|
22
22
|
- **Patch, fragment, route.** A binding is patched in place; a `data-payload-fragment` boundary is rendered again by your server from the unsaved form state; a patch that fell short, or a change nothing binds, goes to the fragment render or a route refresh when the page has one ([below](#patch-fragment-route)).
|
|
23
23
|
- **Delivery.** Inline, or a bootstrap that fetches the content-hashed, SRI-verified runtime only inside a preview (`delivery: 'asset'`; Astro's static equivalent is `mode: 'loader'`). Where something decides per request — Astro's middleware, `await LivePreviewScript(…)` in Next.js, the SvelteKit handle, the Nuxt Nitro plugin — a public visitor receives no script at all ([docs/deployment.md](docs/deployment.md#what-a-public-visitor-pays)).
|
|
24
24
|
- **Payload 3.x native.** `serverURL` re-fetches the populated document, like the official client, so relationship and upload fields render as content rather than as IDs — but only when an edit needs one: in the DOM runtime, typing into a text field costs no request, and a burst on a relationship costs two requests, at its start and its end, rather than one per keystroke.
|
|
25
|
-
- **Lexical renderer.** 20 node types including `upload`, `relationship`, `block`, `autolink`, tabs, indent and RTL, with rich-text detection from the value shape
|
|
25
|
+
- **Lexical renderer.** 20 node types including `upload`, `relationship`, `block`, `autolink`, tabs, indent and RTL, with rich-text detection from the value shape, which also decides when Payload is asked to fill in uploads and relationships. An unknown node renders only its children; a block with no registered renderer becomes an empty placeholder, and the live write keeps the markup your server rendered in its place when the two trees line up. The prebuilt inline runtime cannot register block renderers.
|
|
26
26
|
- **Hooks for client-rendered apps.** `useLivePreviewDocument()` from `payload-live-preview/react` or `/vue` hands the merged document to your component tree ([docs/react.md](docs/react.md), [docs/vue.md](docs/vue.md)).
|
|
27
27
|
- **Tooling.** `pll doctor <url>` requests a page as a visitor and as the admin's iframe and reports the difference; `pll migrate` rewrites 1.x APIs to their 2.0 names; `pll-codegen` generates types from the Payload config, and `pll-codegen annotate` finds the bindings a template can take unambiguously.
|
|
28
28
|
- **Authorization before anything privileged.** Where an adapter decides per request with `authorizePreview`, draft reads, runtime injection, CSP changes and binding attributes follow one verified decision, and the client-controlled intent signals unlock nothing. The setups that decide less are named under [Security model](#security-model).
|
|
@@ -31,19 +31,29 @@ One runtime serves every framework, and every framework adapter's example app ru
|
|
|
31
31
|
|
|
32
32
|
## Compatibility
|
|
33
33
|
|
|
34
|
-
|
|
|
35
|
-
|
|
|
36
|
-
|
|
|
37
|
-
|
|
|
38
|
-
|
|
|
39
|
-
|
|
34
|
+
| Payload | Status | Checked by |
|
|
35
|
+
| ------- | ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
36
|
+
| 2.x | supported in 2.x; ends with 3.0 | the 2.32.3 wire corpus replayed in CI; real-admin release checks of the DOM runtime and the React and Vue hooks ([docs/testing.md](docs/testing.md#local-release-checks)) |
|
|
37
|
+
| 3.x | supported | the real admin on every push (3.92.0, three engines); the 3.85, 3.88 and 3.89 wire corpora replayed in CI |
|
|
38
|
+
| 4.x | pre-release, the direction of this package | the 4.0.0-beta.4 wire corpus replayed in CI; the real-admin suite against `payload@beta` every night, as an early warning; a 2.2 release candidate on a site running 4.0.0-beta.4 with Astro 7.4.2 |
|
|
39
|
+
|
|
40
|
+
**Payload 4** is where this package is headed. Its pre-releases are followed as they appear, and support becomes official with the stable 4.0 release; until then the nightly run against `payload@beta` warns early and does not gate a release. The Payload 4 pre-release captured so far passes the same protocol checks as the 3.x corpora. As of 4.0.0-beta.4, Payload 4 needs Node 24.22.1 or later, and its admin Next.js 16.4 or later. Payload 2.x support ends with the next major release of this package: 3.0 supports Payload 3.x and 4.x, and every 2.x release keeps supporting Payload 2 ([ADR 0007 ledger, entry 25](docs/architecture/0007-v2-defaults-and-renames-ledger.md#ledger), [Preparing for 3.0](docs/migration.md#preparing-for-30)). Payload 3.x stays supported. The manifest declares `payload` as an optional peer, `^2.0.0 || ^3.0.0 || ^4.0.0-0`: npm never installs it, and a project that has Payload outside that range gets npm's `ERESOLVE` error.
|
|
41
|
+
|
|
42
|
+
What differs between Payload 2, 3 and 4:
|
|
43
|
+
|
|
44
|
+
| | Payload 2.x | Payload 3.x | Payload 4.x (measured on 4.0.0-beta.4) |
|
|
45
|
+
| ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | ----------------------------------------------------------------------- |
|
|
46
|
+
| Scalar field updates | ✅ | ✅ | ✅ |
|
|
47
|
+
| Rich text (Lexical) | ✅ | ✅ | ✅ through a fragment render; the client-side renderer not measured yet |
|
|
48
|
+
| Relationship / upload population | Collection GETs from the accepted schema with `serverURL` and matching depth (relationships measured against a real 2.32.3 admin, uploads by unit tests) | ✅ with `serverURL` (REST merge) | not measured yet |
|
|
49
|
+
| Schema-driven field typing (`fieldSchemaJSON`) | ✅ | — (3.x removed it; DOM heuristics + Lexical auto-detection take over) | — (as in 3.x) |
|
|
40
50
|
|
|
41
51
|
<!-- compat-matrix:start -->
|
|
42
52
|
|
|
43
53
|
| Framework | Supported | Tested in CI on every push (version, browsers) |
|
|
44
54
|
| --------- | ---------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
|
|
45
|
-
| Astro | >=4.0.0 <8.0.0 | 7.
|
|
46
|
-
| Next.js | App Router, 15.5 and 16 | 16.4.
|
|
55
|
+
| Astro | >=4.0.0 <8.0.0 | 7.4.2 (chromium, firefox, webkit); 6.x (chromium); 5.x (chromium); 4.x (chromium) |
|
|
56
|
+
| Next.js | App Router, 15.5 and 16 | 16.4.3 (chromium, firefox, webkit); 15.x (chromium) |
|
|
47
57
|
| SvelteKit | 2.x | 2.70.2 (chromium, firefox, webkit) |
|
|
48
58
|
| Nuxt | 3.x (end of life since 31 July 2026: kept working, not officially supported) | 3.21.11 (chromium, firefox, webkit) |
|
|
49
59
|
|
|
@@ -57,35 +67,20 @@ The framework hooks and fragment renderers run twice on every push: at the floor
|
|
|
57
67
|
- Payload 3.85.0: wire corpus captured from a real admin, replayed in tests/integration/wire-corpus.test.ts.
|
|
58
68
|
- Payload 3.88.0: wire corpus captured from a real admin, replayed in tests/integration/wire-corpus.test.ts.
|
|
59
69
|
- Payload 3.89.0: wire corpus captured from a real admin, replayed in tests/integration/wire-corpus.test.ts.
|
|
60
|
-
- Payload 3.
|
|
61
|
-
- Payload 4.0.0-
|
|
62
|
-
- Payload 4.0.0-beta.4 (pre-release): measured once on 2026-10-24, outside CI, in a consumer project that runs 2.1.0 from npm (Next 16, Node 24, Postgres 18, the real admin driven by headless Chromium). The config plugin type-checks against Payload's declarations and passes its config sanitizing. For six collection documents in two locales, the admin's preview frame loads the URL a hand-written callback produced, apart from the order of the query parameters, with the same breakpoint menu and no page errors, and the locale reaches the path resolvers. Against an admin with a `csrf` list, the `payload-session` strategy authorizes once its `origin` option is set. That run did not cover globals, the create screen, drafts and autosave, other plugins that write a Live Preview URL, or replacing existing breakpoints
|
|
70
|
+
- Payload 3.92.0: real admin E2E fixture (examples/payload-backend), run by CI on every push.
|
|
71
|
+
- Payload 4.0.0-beta.4: wire corpus captured on 2026-10-26 from a real Payload 4 admin (examples/payload-backend with payload@4.0.0-beta.4 installed over it, Node 24.22.1), replayed in tests/integration/wire-corpus.test.ts; the fixture itself stays on 3.x.
|
|
72
|
+
- Payload 4.0.0-beta.4 (pre-release): measured once on 2026-10-24, outside CI, in a consumer project that runs 2.1.0 from npm (Next 16, Node 24, Postgres 18, the real admin driven by headless Chromium). The config plugin type-checks against Payload's declarations and passes its config sanitizing. For six collection documents in two locales, the admin's preview frame loads the URL a hand-written callback produced, apart from the order of the query parameters, with the same breakpoint menu and no page errors, and the locale reaches the path resolvers. Against an admin with a `csrf` list, the `payload-session` strategy authorizes once its `origin` option is set. That run did not cover globals, the create screen, drafts and autosave, other plugins that write a Live Preview URL, or replacing existing breakpoints. Since 2026-10-25 the nightly protocol watch installs payload@beta into the admin fixture as a soft-fail early warning.
|
|
73
|
+
- Payload 4.0.0-beta.4 with Astro 7.4.2 (pre-release): measured on 2026-10-26, outside CI, with a 2.2 release candidate on a consumer site (the real admin driven by headless Chromium). Typing into a title and a text line patched the preview, typing into a rich-text field updated it through 7 fragment renders that answered 200, the frame never reloaded, and all 31 live-preview messages had the shape of the recorded capture. Relationship and upload population were not part of the run.
|
|
63
74
|
- Payload latest: daily protocol watch executes @payloadcms/live-preview@latest against the corpus.
|
|
64
|
-
- Payload 4.0 pre-releases: daily protocol watch against @payloadcms/live-preview@
|
|
75
|
+
- Payload 4.0 pre-releases: daily protocol watch against @payloadcms/live-preview@beta and the real admin against payload@beta, early warning only.
|
|
65
76
|
|
|
66
77
|
<!-- compat-matrix:end -->
|
|
67
78
|
|
|
68
79
|
Nuxt 4 has no row because no CI job installs it. A copy of the Nuxt example in Nuxt 4.5.2's `app/` layout passed the production suite against the mock admin on 2026-10-02 (54 of 54 cases), and a Nuxt 4.5.2 site installed from the 2.1.0 release candidate built and served its pages on 2026-10-18; neither was a real-admin run.
|
|
69
80
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
### Local 2.1.0 release checks
|
|
73
|
-
|
|
74
|
-
The following checks passed on 2026-10-23 on the package archive that was published as 2.1.0; the npm tarball differs from it only in the build timestamp. These are local results, separate from the CI jobs listed above. Browser counts include Chromium, Firefox and WebKit on Linux. `examples/payload-version-lanes` runs the Payload 2.32.3 rows, with `--probe=population`, `--probe=lifecycle` or `--probe=guards` for React and Vue.
|
|
81
|
+
SvelteKit 3 has no row either: no CI job installs it. A copy of the SvelteKit example, moved to SvelteKit 3.1.0 with `npx sv migrate sveltekit-3`, built with adapter-node 6 and passed the 32 SvelteKit cases of the browser suite against the mock admin in Chromium, Firefox and WebKit on 2026-10-26; it was not a real-admin run. The example needed the migration's changes ([docs/sveltekit.md](docs/sveltekit.md)); the package needed none.
|
|
75
82
|
|
|
76
|
-
|
|
77
|
-
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- |
|
|
78
|
-
| Payload 2.32.3 | DOM, React and Vue, each with frontend-only and serverURL modes | 24 per variant, six variants |
|
|
79
|
-
| Payload 2.32.3 | React and Vue hook lifecycle, frontend-only and serverURL | 9 per variant, four variants |
|
|
80
|
-
| Payload 2.32.3 | React and Vue hook guards, frontend-only and serverURL | 21 per variant, four variants |
|
|
81
|
-
| Node 20.19.0 | Peer-free consumer of the archive: every export without a peer under ESM and CommonJS, CLI help, inline script, signed tokens, `payload-session` and the fragment endpoint with its body, deadline and depth limits | 11 runtime cases |
|
|
82
|
-
| Next.js 16.4.2 | Webpack production preview and fragment rendering | 54 browser cases |
|
|
83
|
-
| Astro 4.0.0 | Base adapter with loader and inline delivery, static output and scoped updates, two control builds | 36 browser cases |
|
|
84
|
-
| Astro 4.16.19 | Native React islands, CSP and navigation ownership | 12 browser cases |
|
|
85
|
-
|
|
86
|
-
The Node row runs the official 20.19.0 binary with no framework or optional peer installed; its fragment endpoint uses a custom renderer and its session check a local `/me` stub, not a real Payload admin, and it covers no framework build, development server or browser. The Next checks use a controlled preview cookie and fragment tokens; they do not use a real Payload admin. The Payload 2 graph still has upstream audit findings, including a critical finding in the 2026-10-05 audit. Its passing preview cases establish compatibility, not a clean dependency graph. Payload 3 lifecycle and Payload 4 canary checks are not included in this table. The Payload 2 lifecycle and guards rows run development builds on Linux engines. The lifecycle row covers an owner replaced while a read is held, a document change by navigation and a reload of the preview frame. The guards row covers a late successful response of a replaced owner, messages from a foreign window and a foreign origin, a receiver switch in a running session and a sign-out in the middle of one; neither row covers a token that expires by time or a production build.
|
|
87
|
-
|
|
88
|
-
The two Astro rows can be repeated from the repository. The `astro-floor-base` and `astro-floor-islands` jobs run their specs on every push in Chromium ([Astro peer floors](docs/testing.md#astro-peer-floors)); the counts above are all three engines, with loader and inline delivery at five cases each and two control builds per engine for Astro 4.0.0.
|
|
83
|
+
**A client-rendered app takes a hook.** `useLivePreviewDocument()` from `payload-live-preview/react` or `payload-live-preview/vue` returns the merged document and re-renders your tree. Payload 3 updates use the method-override merge request; Payload 2 scalar updates settle locally and raw relationships use schema-based collection GETs. A newer update aborts any request in flight; an HTTP error is refused instead of becoming the document; each hook has a session of its own, so two previews on one page keep their own documents ([docs/react.md](docs/react.md), [docs/vue.md](docs/vue.md)). The official [`@payloadcms/live-preview-react`](https://payloadcms.com/docs/live-preview/client) / `-vue` hooks ship in lockstep with Payload and remain the safe default if that matters more to you; running either alongside the DOM runtime on one page: [docs/interop.md](docs/interop.md).
|
|
89
84
|
|
|
90
85
|
## Install
|
|
91
86
|
|
|
@@ -107,7 +102,8 @@ import { livePreview } from 'payload-live-preview/plugin';
|
|
|
107
102
|
export default buildConfig({
|
|
108
103
|
plugins: [
|
|
109
104
|
livePreview({
|
|
110
|
-
|
|
105
|
+
// Your site's origin. In development: Astro 4321, Next.js and Nuxt 3000, SvelteKit 5173.
|
|
106
|
+
baseUrl: process.env.FRONTEND_URL ?? 'http://localhost:3000',
|
|
111
107
|
collections: {
|
|
112
108
|
posts: ({ data }) => `/blog/${String(data['slug'] ?? '')}`,
|
|
113
109
|
services: ({ data, locale }) => `/${locale ?? 'en'}/services/${String(data['slug'] ?? '')}`,
|
|
@@ -132,16 +128,16 @@ transform run after any plugin that may write a Live Preview URL in Payload's
|
|
|
132
128
|
effective execution order, or compose both routing policies explicitly. The
|
|
133
129
|
complete merge and conflict rules are in [docs/payload.md](docs/payload.md).
|
|
134
130
|
|
|
135
|
-
In the Payload 3.
|
|
131
|
+
In the Payload 3.92.0 admin, a collection document gets its native Live
|
|
136
132
|
Preview after its first save. The create screen has no preview toggle and no
|
|
137
133
|
frame, for a function-valued URL and for a fixed string alike. The frame that
|
|
138
134
|
appears after the save loads the URL the plugin's function builds from the saved
|
|
139
135
|
document. `tests/real-payload/surface-create.spec.ts` checks this in Chromium,
|
|
140
136
|
Firefox and WebKit.
|
|
141
137
|
|
|
142
|
-
Payload 4 is still a pre-release. The plugin
|
|
143
|
-
4.0.0-beta.4, outside CI; the
|
|
144
|
-
that run covered and what it left out.
|
|
138
|
+
Payload 4 is still a pre-release ([Compatibility](#compatibility)). The plugin
|
|
139
|
+
was measured once against 4.0.0-beta.4, outside CI; the compatibility list says
|
|
140
|
+
what that run covered and what it left out.
|
|
145
141
|
|
|
146
142
|
Use the lower-level callback when another plugin owns the surrounding config or
|
|
147
143
|
when a document may have no preview route:
|
|
@@ -153,7 +149,7 @@ export default buildConfig({
|
|
|
153
149
|
admin: {
|
|
154
150
|
livePreview: {
|
|
155
151
|
url: buildLivePreviewUrl({
|
|
156
|
-
baseUrl: process.env.FRONTEND_URL ?? 'http://localhost:
|
|
152
|
+
baseUrl: process.env.FRONTEND_URL ?? 'http://localhost:3000',
|
|
157
153
|
collections: {
|
|
158
154
|
posts: ({ data }) => `/blog/${String(data['slug'] ?? '')}`,
|
|
159
155
|
services: ({ data, locale }) =>
|
|
@@ -186,6 +182,10 @@ strategies, and the initial draft read.
|
|
|
186
182
|
|
|
187
183
|
## Quick start
|
|
188
184
|
|
|
185
|
+
Install the package, give the Payload admin your site's URLs ([Configure Payload](#configure-payload)), add your framework's adapter, then mark what should update. Each framework guide goes from there to a working preview and names its runnable example.
|
|
186
|
+
|
|
187
|
+
The snippets read the admin origin the browser sees from `PUBLIC_PAYLOAD_ADMIN_ORIGIN` and, where they authorize, Payload's own origin from `PAYLOAD_URL`; the Next.js layout also needs the site's own origin as `SITE_ORIGIN`. The `payload-session` strategy accepts an editor signed in to that admin ([docs/authorization.md](docs/authorization.md)).
|
|
188
|
+
|
|
189
189
|
### Astro
|
|
190
190
|
|
|
191
191
|
```js
|
|
@@ -207,36 +207,151 @@ export default defineConfig({
|
|
|
207
207
|
});
|
|
208
208
|
```
|
|
209
209
|
|
|
210
|
-
|
|
210
|
+
The default `inline` mode writes the runtime into every page at build time, where it starts only inside the admin's preview frame. `mode: 'middleware'` decides per request with an `authorizePreview` module, as the adapters below do: [docs/astro.md](docs/astro.md).
|
|
211
|
+
|
|
212
|
+
### Next.js (App Router)
|
|
213
|
+
|
|
214
|
+
```tsx
|
|
215
|
+
// app/layout.tsx
|
|
216
|
+
import type { ReactNode } from 'react';
|
|
217
|
+
import { headers } from 'next/headers';
|
|
218
|
+
import { LivePreviewScript } from 'payload-live-preview/nextjs';
|
|
219
|
+
import { authorizePreviewRequest } from 'payload-live-preview/server';
|
|
220
|
+
|
|
221
|
+
export default async function RootLayout({ children }: { children: ReactNode }) {
|
|
222
|
+
// Awaited, not rendered as <LivePreviewScript />: docs/nextjs.md says why.
|
|
223
|
+
const livePreview = await LivePreviewScript({
|
|
224
|
+
request: new Request(process.env.SITE_ORIGIN!, { headers: await headers() }),
|
|
225
|
+
// A layout cannot see the query string, so the verdict is the gate.
|
|
226
|
+
inject: 'always',
|
|
227
|
+
allowedOrigins: [process.env.PUBLIC_PAYLOAD_ADMIN_ORIGIN!],
|
|
228
|
+
serverURL: process.env.PUBLIC_PAYLOAD_ADMIN_ORIGIN!,
|
|
229
|
+
mergeDepth: 1,
|
|
230
|
+
authorizePreview: (request) =>
|
|
231
|
+
authorizePreviewRequest(request, {
|
|
232
|
+
type: 'payload-session',
|
|
233
|
+
serverURL: process.env.PAYLOAD_URL!,
|
|
234
|
+
}),
|
|
235
|
+
});
|
|
236
|
+
return (
|
|
237
|
+
<html lang="en">
|
|
238
|
+
<head>{livePreview}</head>
|
|
239
|
+
<body>{children}</body>
|
|
240
|
+
</html>
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Awaited in the layout, the server component returns nothing for a request that is not an authorized preview. The adapter middleware merges `frame-ancestors` and marks the preview response `private, no-store`: [docs/nextjs.md](docs/nextjs.md).
|
|
246
|
+
|
|
247
|
+
### SvelteKit
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
// src/hooks.server.ts
|
|
251
|
+
import { env } from '$env/dynamic/private';
|
|
252
|
+
import { env as publicEnv } from '$env/dynamic/public';
|
|
253
|
+
import { livePreviewHandle } from 'payload-live-preview/sveltekit';
|
|
254
|
+
import { authorizePreviewRequest } from 'payload-live-preview/server';
|
|
255
|
+
|
|
256
|
+
const adminOrigin = publicEnv.PUBLIC_PAYLOAD_ADMIN_ORIGIN ?? '';
|
|
257
|
+
|
|
258
|
+
export const handle = livePreviewHandle({
|
|
259
|
+
allowedOrigins: [adminOrigin],
|
|
260
|
+
// Payload 3.x: re-fetch the populated document; mergeDepth is required with serverURL.
|
|
261
|
+
serverURL: adminOrigin,
|
|
262
|
+
mergeDepth: 1,
|
|
263
|
+
// Required under the strict default: without the hook the handle refuses to
|
|
264
|
+
// start, and every route fails, not only the preview.
|
|
265
|
+
authorizePreview: (request) =>
|
|
266
|
+
authorizePreviewRequest(request, { type: 'payload-session', serverURL: env.PAYLOAD_URL ?? '' }),
|
|
267
|
+
});
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
The handle authorizes the request, injects the runtime into the `<head>` of an authorized preview response and merges the CSP: [docs/sveltekit.md](docs/sveltekit.md).
|
|
271
|
+
|
|
272
|
+
### Nuxt
|
|
211
273
|
|
|
212
|
-
```
|
|
274
|
+
```ts
|
|
275
|
+
// nuxt.config.ts
|
|
276
|
+
export default defineNuxtConfig({
|
|
277
|
+
modules: ['payload-live-preview/nuxt-module'],
|
|
278
|
+
livePreview: {
|
|
279
|
+
authorizePreviewModule: './server/utils/live-preview-auth',
|
|
280
|
+
allowedOrigins: [process.env.PUBLIC_PAYLOAD_ADMIN_ORIGIN!],
|
|
281
|
+
serverURL: process.env.PUBLIC_PAYLOAD_ADMIN_ORIGIN!,
|
|
282
|
+
mergeDepth: 1,
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
```ts
|
|
288
|
+
// server/utils/live-preview-auth.ts
|
|
289
|
+
import { authorizePreviewRequest, type PreviewRequestLike } from 'payload-live-preview/server';
|
|
290
|
+
|
|
291
|
+
export default (request: PreviewRequestLike) =>
|
|
292
|
+
authorizePreviewRequest(request, {
|
|
293
|
+
type: 'payload-session',
|
|
294
|
+
serverURL: process.env.PAYLOAD_URL!,
|
|
295
|
+
});
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
The module registers a Nitro plugin and a server handler, and takes the authorization hook by module reference: [docs/nuxt.md](docs/nuxt.md).
|
|
299
|
+
|
|
300
|
+
### Plain HTML
|
|
301
|
+
|
|
302
|
+
```ts
|
|
303
|
+
// build.ts — runs at build time, never in the browser
|
|
304
|
+
import { generateInlineScript, wrapWithScriptTag } from 'payload-live-preview';
|
|
305
|
+
|
|
306
|
+
const script = generateInlineScript({
|
|
307
|
+
allowedOrigins: [process.env.PUBLIC_PAYLOAD_ADMIN_ORIGIN!],
|
|
308
|
+
// Payload 3.x: re-fetch the populated document; mergeDepth is required with serverURL.
|
|
309
|
+
serverURL: process.env.PUBLIC_PAYLOAD_ADMIN_ORIGIN!,
|
|
310
|
+
mergeDepth: 1,
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
const tag = wrapWithScriptTag(script); // `<script>…</script>`; pass { nonce } for a CSP nonce
|
|
314
|
+
const html = `<!doctype html><html lang="en"><head><meta charset="utf-8">${tag}</head><body>…</body></html>`;
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
The tag goes into `<head>`; the script stays inert outside the admin's preview frame: [docs/html.md](docs/html.md).
|
|
318
|
+
|
|
319
|
+
### Mark what should update
|
|
320
|
+
|
|
321
|
+
The attributes are plain HTML, written the same way in `.astro`, JSX, Svelte and Vue templates. On a server-rendered page that is one attribute per component — the region is rendered again from the unsaved form state, so everything in it stays correct, conditional sections and derived values included:
|
|
322
|
+
|
|
323
|
+
```html
|
|
213
324
|
<section data-payload-fragment="hero" data-payload-depends="title,subtitle,body">
|
|
214
|
-
|
|
325
|
+
<!-- the hero component, rendered by your server -->
|
|
215
326
|
</section>
|
|
216
327
|
```
|
|
217
328
|
|
|
218
|
-
The attribute needs a renderer behind it: a route exporting `createFragmentEndpoint()` with `authorizePreview` or `authorize`, served at request time
|
|
329
|
+
The attribute needs a renderer behind it: a route exporting `createFragmentEndpoint()` from your adapter's entry, with `authorizePreview` or `authorize`, served at request time, and its path in the adapter options as `fragments: { endpoint: '/payload/fragment' }` ([docs/hybrid.md](docs/hybrid.md)). Without `fragments` the bindings inside the boundary are patched instead (`LP0806`), and a boundary with none inside does not update.
|
|
219
330
|
|
|
220
|
-
|
|
331
|
+
Without a server to render it — on a static build, for one — the fields are bound individually, and inside a boundary too. A direct patch retains the target element. A component render or fragment morph can retain it as well when type, key and position stay compatible, but may replace it when they do not:
|
|
221
332
|
|
|
222
|
-
```
|
|
223
|
-
<h1 data-payload-field="title"
|
|
333
|
+
```html
|
|
334
|
+
<h1 data-payload-field="title">…</h1>
|
|
224
335
|
<div data-payload-field="body">…server-rendered rich text…</div>
|
|
225
|
-
<img data-payload-field="hero" alt=
|
|
336
|
+
<img data-payload-field="hero" alt="…" src="…" />
|
|
226
337
|
```
|
|
227
338
|
|
|
228
339
|
Which to reach for, and what each costs: [docs/bindings.md](docs/bindings.md#how-much-markup-this-actually-needs). `pll-codegen annotate` adds the unambiguous bindings and names each one it refuses, with the reason; without `--write` it only reports, and exits with `3` when it found work.
|
|
229
340
|
|
|
230
|
-
That is it: the
|
|
341
|
+
That is it: the runtime detects the admin's iframe and starts patching. Rich text is written from the value shape, and a value that holds an upload or a relationship has the runtime ask Payload to fill it in ([docs/renderers.md](docs/renderers.md#media-urls)). The admin posts raw form values, so relationship and upload fields arrive as IDs until `serverURL` with `mergeDepth` re-fetches the populated document ([docs/options.md](docs/options.md)). Authorization and the initial draft read: your framework's guide and [docs/authorization.md](docs/authorization.md).
|
|
231
342
|
|
|
232
|
-
|
|
343
|
+
## Frameworks
|
|
233
344
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
345
|
+
| | Astro | Next.js | SvelteKit | Nuxt |
|
|
346
|
+
| -------------------------------------------------------- | ----------------------------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------- |
|
|
347
|
+
| Decides per request: authorizes, injects, merges the CSP | `mode: 'middleware'` | `await LivePreviewScript(…)` and `createLivePreviewMiddleware()` | `livePreviewHandle()` | `payload-live-preview/nuxt-module` |
|
|
348
|
+
| The runtime as a hashed, cached asset | `mode: 'loader'` | `delivery: 'asset'` and `createRuntimeAssetRoute()` | `delivery: 'asset'` and `createRuntimeAssetRoute()` | `delivery: 'asset'` and `createRuntimeAssetRoute()` |
|
|
349
|
+
| Renders a fragment boundary with | Astro's container API | `react-dom/server` | `svelte/server` | `vue/server-renderer` |
|
|
350
|
+
| First write waits for hydration | islands keep their own subtree | yes | yes | yes |
|
|
351
|
+
| Rich text and empty fields on the server | the `RichText` and `PreviewBoundary` components | `lexicalToHtml()` and a `data-payload-boundary` element | `lexicalToHtml()` and a `data-payload-boundary` element | `lexicalToHtml()` and a `data-payload-boundary` element |
|
|
352
|
+
| `pll-codegen annotate` reads | `.astro`, also as a Vite plugin | `.jsx`, `.tsx` | `.svelte` | not `.vue` files |
|
|
238
353
|
|
|
239
|
-
|
|
354
|
+
Astro also writes the runtime into a static build (`inline`, `loader`). Its `RichText` and `PreviewBoundary` components write the same markup the other frameworks write by hand with `lexicalToHtml()` and a `data-payload-boundary` element ([docs/bindings.md](docs/bindings.md)). The test depth differs: CI installs Astro 4 to 7 and Next.js 15.5 and 16, SvelteKit 2 and Nuxt 3. Nuxt 3 reached its end of life on 31 July 2026, and no job installs Nuxt 4 yet.
|
|
240
355
|
|
|
241
356
|
## Patch, fragment, route
|
|
242
357
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { L as LivePreviewAstroOptions } from '../../middleware-
|
|
2
|
-
export { a as LivePreviewMiddleware, c as createLivePreviewMiddleware } from '../../middleware-
|
|
3
|
-
import { h as hasPreviewIntent } from '../../options-
|
|
4
|
-
export { P as PreviewAdapterOptions, a as PreviewRequestLike, b as PreviewRequestOptions, c as PreviewSignal } from '../../options-
|
|
5
|
-
import { F as FragmentEndpointOptions$1, a as FragmentRegistry$1, b as FragmentRegistryEntry$1, c as FragmentRenderer$1 } from '../../fragment-endpoint-
|
|
6
|
-
export { d as FragmentRenderInput } from '../../fragment-endpoint-
|
|
7
|
-
export { A as AUTHORIZATION_LOCALS_KEY, a as AUTHORIZATION_OUTCOME_LOCALS_KEY, L as LivePreviewLocals, N as NONCE_LOCALS_KEY } from '../../locals-
|
|
1
|
+
import { L as LivePreviewAstroOptions } from '../../middleware-CI-UTf6R.js';
|
|
2
|
+
export { a as LivePreviewMiddleware, c as createLivePreviewMiddleware } from '../../middleware-CI-UTf6R.js';
|
|
3
|
+
import { h as hasPreviewIntent } from '../../options-CnYybG51.js';
|
|
4
|
+
export { P as PreviewAdapterOptions, a as PreviewRequestLike, b as PreviewRequestOptions, c as PreviewSignal } from '../../options-CnYybG51.js';
|
|
5
|
+
import { F as FragmentEndpointOptions$1, a as FragmentRegistry$1, b as FragmentRegistryEntry$1, c as FragmentRenderer$1 } from '../../fragment-endpoint-DGGLIijM.js';
|
|
6
|
+
export { d as FragmentRenderInput } from '../../fragment-endpoint-DGGLIijM.js';
|
|
7
|
+
export { A as AUTHORIZATION_LOCALS_KEY, a as AUTHORIZATION_OUTCOME_LOCALS_KEY, L as LivePreviewLocals, N as NONCE_LOCALS_KEY } from '../../locals-zz2gi9dj.js';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* The Astro integration for `astro.config.mjs`: `livePreview({ ... })`.
|