payload-live-preview 2.0.6 → 2.1.1

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.
Files changed (116) hide show
  1. package/README.md +148 -38
  2. package/dist/adapters/astro/FragmentBridge.astro +25 -0
  3. package/dist/adapters/astro/PreviewBoundary.astro +2 -4
  4. package/dist/adapters/astro/RichText.astro +26 -5
  5. package/dist/adapters/astro/index.d.ts +41 -9
  6. package/dist/adapters/astro/index.js +1 -1
  7. package/dist/adapters/astro/index.js.map +1 -1
  8. package/dist/adapters/astro/middleware-entry.d.ts +3 -3
  9. package/dist/adapters/astro/middleware-entry.js +1 -1
  10. package/dist/adapters/astro/middleware-entry.js.map +1 -1
  11. package/dist/adapters/nextjs/index.d.ts +17 -12
  12. package/dist/adapters/nextjs/index.js +1 -1
  13. package/dist/adapters/nextjs/index.js.map +1 -1
  14. package/dist/adapters/nuxt/index.d.ts +11 -6
  15. package/dist/adapters/nuxt/index.js +1 -1
  16. package/dist/adapters/nuxt/index.js.map +1 -1
  17. package/dist/adapters/nuxt/module.d.ts +64 -13
  18. package/dist/adapters/nuxt/module.js +1 -1
  19. package/dist/adapters/nuxt/module.js.map +1 -1
  20. package/dist/adapters/react/index.d.ts +11 -7
  21. package/dist/adapters/react/index.js +1 -1
  22. package/dist/adapters/react/index.js.map +1 -1
  23. package/dist/adapters/sveltekit/index.d.ts +10 -5
  24. package/dist/adapters/sveltekit/index.js +1 -1
  25. package/dist/adapters/sveltekit/index.js.map +1 -1
  26. package/dist/adapters/vue/index.d.ts +10 -7
  27. package/dist/adapters/vue/index.js +1 -1
  28. package/dist/adapters/vue/index.js.map +1 -1
  29. package/dist/annotate.d.ts +15 -2
  30. package/dist/annotate.js.map +1 -1
  31. package/dist/client.cjs +1 -1
  32. package/dist/client.cjs.map +1 -1
  33. package/dist/client.d.cts +367 -266
  34. package/dist/client.d.ts +367 -266
  35. package/dist/client.js +1 -1
  36. package/dist/client.js.map +1 -1
  37. package/dist/codegen-cli.js.map +1 -1
  38. package/dist/codegen.cjs.map +1 -1
  39. package/dist/codegen.d.cts +7 -1
  40. package/dist/codegen.d.ts +7 -1
  41. package/dist/codegen.js.map +1 -1
  42. package/dist/core.cjs +1 -1
  43. package/dist/core.cjs.map +1 -1
  44. package/dist/core.d.cts +549 -393
  45. package/dist/core.d.ts +549 -393
  46. package/dist/core.js +1 -1
  47. package/dist/core.js.map +1 -1
  48. package/dist/doctor-cli.d.ts +1 -1
  49. package/dist/doctor-cli.js +1 -1
  50. package/dist/doctor-cli.js.map +1 -1
  51. package/dist/doctor.d.ts +2 -2
  52. package/dist/doctor.js +1 -1
  53. package/dist/doctor.js.map +1 -1
  54. package/dist/{document-session-BfBfLVpg.d.ts → document-session-DWi9H3Qk.d.ts} +7 -0
  55. package/dist/{fragment-endpoint-DATKU183.d.ts → fragment-endpoint-B2qcRw5T.d.ts} +22 -7
  56. package/dist/fragment.cjs +1 -1
  57. package/dist/fragment.cjs.map +1 -1
  58. package/dist/fragment.d.cts +37 -16
  59. package/dist/fragment.d.ts +37 -16
  60. package/dist/fragment.js +1 -1
  61. package/dist/fragment.js.map +1 -1
  62. package/dist/index.cjs +1 -1
  63. package/dist/index.cjs.map +1 -1
  64. package/dist/index.d.cts +607 -405
  65. package/dist/index.d.ts +607 -405
  66. package/dist/index.js +1 -1
  67. package/dist/index.js.map +1 -1
  68. package/dist/lean.cjs +1 -1
  69. package/dist/lean.cjs.map +1 -1
  70. package/dist/lean.d.cts +5 -2
  71. package/dist/lean.d.ts +5 -2
  72. package/dist/lean.js +1 -1
  73. package/dist/lean.js.map +1 -1
  74. package/dist/lexical.cjs +1 -1
  75. package/dist/lexical.cjs.map +1 -1
  76. package/dist/lexical.d.cts +32 -11
  77. package/dist/lexical.d.ts +32 -11
  78. package/dist/lexical.js +1 -1
  79. package/dist/lexical.js.map +1 -1
  80. package/dist/{locals-CNUGYbA_.d.ts → locals-B1mF3aTb.d.ts} +1 -1
  81. package/dist/{middleware-CWObDO37.d.ts → middleware-BGobLN9P.d.ts} +11 -5
  82. package/dist/migrate.js +1 -1
  83. package/dist/migrate.js.map +1 -1
  84. package/dist/{options-uHKrDM2U.d.ts → options-Cm8fC7dj.d.ts} +31 -1
  85. package/dist/payload.cjs +1 -1
  86. package/dist/payload.cjs.map +1 -1
  87. package/dist/payload.d.cts +18 -2
  88. package/dist/payload.d.ts +18 -2
  89. package/dist/payload.js +1 -1
  90. package/dist/payload.js.map +1 -1
  91. package/dist/plugin.cjs +2 -0
  92. package/dist/plugin.cjs.map +1 -0
  93. package/dist/plugin.d.cts +30 -0
  94. package/dist/plugin.d.ts +30 -0
  95. package/dist/plugin.js +2 -0
  96. package/dist/plugin.js.map +1 -0
  97. package/dist/plugins.cjs +1 -1
  98. package/dist/plugins.cjs.map +1 -1
  99. package/dist/plugins.d.cts +102 -29
  100. package/dist/plugins.d.ts +102 -29
  101. package/dist/plugins.js +1 -1
  102. package/dist/plugins.js.map +1 -1
  103. package/dist/{probe-Bzm4fYFL.d.ts → probe-DGPDsnbj.d.ts} +6 -2
  104. package/dist/server.cjs +1 -1
  105. package/dist/server.cjs.map +1 -1
  106. package/dist/server.d.cts +51 -7
  107. package/dist/server.d.ts +51 -7
  108. package/dist/server.js +1 -1
  109. package/dist/server.js.map +1 -1
  110. package/dist/structural.cjs +2 -2
  111. package/dist/structural.cjs.map +1 -1
  112. package/dist/structural.d.cts +61 -13
  113. package/dist/structural.d.ts +61 -13
  114. package/dist/structural.js +2 -2
  115. package/dist/structural.js.map +1 -1
  116. package/package.json +33 -4
package/README.md CHANGED
@@ -10,61 +10,82 @@
10
10
 
11
11
  > **Live preview for Payload CMS on server-rendered and static sites** — Astro, Next.js, SvelteKit, Nuxt and plain HTML.
12
12
 
13
- The official live-preview packages are React and Vue hooks: they re-render a hydrated component tree, so they cannot touch markup a server produced. This package makes the admin's real-time preview work where no client framework owns the page. Mark what should update — one attribute per component on a server-rendered page with a fragment endpoint, per field on a static one — and edits reach the preview iframe as the editor types: bound elements are patched in place, fragment boundaries are rendered again by your server. No rebuild, no reload. A client-rendered app gets React and Vue hooks with the same merge underneath. Strict by default, zero runtime dependencies.
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 every framework adapter's example app runs end to end in Chromium, Firefox and WebKit on every push to `main` and on pull requests. Astro has the most coverage: the widest version matrix (4.x to 7.x), the only end-to-end tests against a real Payload admin, the `RichText` and `PreviewBoundary` components, and the annotator as a Vite plugin (`pll-codegen annotate` itself also reads `.jsx`, `.tsx` and `.svelte`).
15
+ One runtime serves every framework, and every framework adapter's example app runs end to end in Chromium, Firefox and WebKit on every push to `main` and on pull requests. Astro has the most coverage: the widest version matrix (4.x to 7.x), the `RichText` and `PreviewBoundary` components, and the annotator as a Vite plugin (`pll-codegen annotate` itself also reads `.jsx`, `.tsx` and `.svelte`).
16
16
 
17
17
  **New here? Start with the [Astro guide](docs/astro.md), or pick your framework in the [documentation index](docs/README.md).**
18
18
 
19
19
  ## Highlights
20
20
 
21
- - **One runtime, every frontend.** One TypeScript runtime compiled to a self-contained inline script of about 35 KB gzip — 28 KB with the lean artifact, 38 to 40 KB once the route or fragment prelude is appended ([docs/options.md](docs/options.md)). The adapters for Astro, Next.js, SvelteKit and Nuxt deliver it; where they decide per request they also authorize the request, merge `frame-ancestors` into your CSP and mark the changed response `private, no-store`. They provide the fragment endpoint and, outside Astro, the route that serves the runtime asset.
21
+ - **One runtime, every frontend.** One TypeScript runtime compiled to a self-contained inline script of about 44 KB gzip — 36 KB with the lean artifact, 47 to 49 KB once the route or fragment prelude is appended ([docs/options.md](docs/options.md)). The adapters for Astro, Next.js, SvelteKit and Nuxt deliver it; where they decide per request they also authorize the request, merge `frame-ancestors` into your CSP and mark the changed response `private, no-store`. They provide the fragment endpoint and, outside Astro, the route that serves the runtime asset.
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
- - **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, `<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)).
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 automatic rich-text detection: `data-payload-field` alone is enough. 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.
25
+ - **Lexical renderer.** 20 node types including `upload`, `relationship`, `block`, `autolink`, tabs, indent and RTL, with rich-text detection from the value shape (an element that can hold uploads or relationships needs `data-payload-richtext`). 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).
29
29
  - **Strict by default.** Escape-by-default sanitizer, URL and `srcset` validation, policed attribute writes, prototype-pollution guards, per-instance clients, typed bindings with `pll-codegen`.
30
- - **Also in the box.** `revealEditedField` scrolls the field being typed in into view ([docs/reveal.md](docs/reveal.md)); a hydrated island keeps its subtree and re-renders itself from a `payload-live-preview:update` event ([docs/hybrid.md](docs/hybrid.md#islands-on-the-same-page)); `data-payload-owner` scopes bindings on a page that previews several documents, `PreviewBoundary` keeps an anchor for a field that is still empty, and `autoBind: 'unique'` finds bindings by value ([docs/bindings.md](docs/bindings.md)); array items render from a template through a keyed morph (`payload-live-preview/structural`); an `aria-live` region announces connect, update and disconnect; the runtime binds again after a back/forward-cache restore or a soft navigation, and the Next.js and Nuxt adapters wait for hydration before the first write.
30
+ - **Also in the box.** `revealEditedField` scrolls the field being typed in into view ([docs/reveal.md](docs/reveal.md)); a hydrated island keeps its subtree and re-renders itself from a `payload-live-preview:update` event ([docs/hybrid.md](docs/hybrid.md#islands-on-the-same-page)); `data-payload-owner` scopes bindings on a page that previews several documents, `PreviewBoundary` keeps an anchor for a field that is still empty, and `autoBind: 'unique'` finds bindings by value ([docs/bindings.md](docs/bindings.md)); array items render from a template through a keyed morph (`payload-live-preview/structural`); an `aria-live` region announces connect, update and disconnect; the runtime binds again after a back/forward-cache restore or a soft navigation, and the Next.js, SvelteKit and Nuxt adapters wait for hydration before the first write.
31
31
 
32
32
  ## Compatibility
33
33
 
34
- | | Payload 2.x | Payload 3.x |
35
- | ---------------------------------------------- | ------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
36
- | Scalar field updates | ✅ | ✅ |
37
- | Rich text (Lexical) | ✅ | ✅ |
38
- | Relationship / upload population | The admin's populated data, used as sent; not verified against a real 2.x admin | ✅ with `serverURL` (REST merge) |
39
- | Schema-driven field typing (`fieldSchemaJSON`) | ✅ | — (3.x removed it; DOM heuristics + Lexical auto-detection take over) |
34
+ | | Payload 2.x | Payload 3.x |
35
+ | ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
36
+ | Scalar field updates | ✅ | ✅ |
37
+ | Rich text (Lexical) | ✅ | ✅ |
38
+ | 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) |
39
+ | Schema-driven field typing (`fieldSchemaJSON`) | ✅ | — (3.x removed it; DOM heuristics + Lexical auto-detection take over) |
40
40
 
41
41
  <!-- compat-matrix:start -->
42
42
 
43
- | Framework | Supported | Tested in CI on every push (version, browsers) |
44
- | --------- | -------------- | --------------------------------------------------------------------------------- |
45
- | Astro | >=4.0.0 <8.0.0 | 7.3.2 (chromium, firefox, webkit); 6.x (chromium); 5.x (chromium); 4.x (chromium) |
46
- | Next.js | App Router, 16 | 16.3.8 (chromium, firefox, webkit) |
47
- | SvelteKit | 2.x | 2.70.2 (chromium, firefox, webkit) |
48
- | Nuxt | 3.x | 3.21.11 (chromium, firefox, webkit) |
43
+ | Framework | Supported | Tested in CI on every push (version, browsers) |
44
+ | --------- | ---------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
45
+ | Astro | >=4.0.0 <8.0.0 | 7.3.2 (chromium, firefox, webkit); 6.x (chromium); 5.x (chromium); 4.x (chromium) |
46
+ | Next.js | App Router, 15.5 and 16 | 16.4.2 (chromium, firefox, webkit); 15.x (chromium) |
47
+ | SvelteKit | 2.x | 2.70.2 (chromium, firefox, webkit) |
48
+ | Nuxt | 3.x (end of life since 31 July 2026: kept working, not officially supported) | 3.21.11 (chromium, firefox, webkit) |
49
49
 
50
50
  Node >=20.19.0; the unit and integration suites run on Node 20, 22, 24, 26. Every version in the table is what the fixture lockfile or the matrix job installs, checked by `npm run compat:check`.
51
51
 
52
52
  Vite 5 through 8: that is what the supported framework majors install (Astro 7 → 8, Astro 6 → 7, Astro 5 → 6, Astro 4 → 5, SvelteKit 2 → 5/6/7/8, Nuxt 3 → 7), measured 2026-09-06. `npm run compat:check` keeps the devDependency and the fixture lockfiles inside that span; `npm run compat:refresh` re-reads it from the registry.
53
53
 
54
- The React and Vue hooks and the Next.js and Nuxt fragment renderers run twice on every push: at the floor of each peer range, which the `hook-matrix` job installs over the lockfile (React 18.0.0, Vue 3.3.0), and at what the lockfile installs (React 19.2.8, Vue 3.5.42). Svelte is mocked in the SvelteKit fragment suite, so `svelte >=5` is measured only through its fixture.
54
+ The framework hooks and fragment renderers run twice on every push: at the floor of each peer range, as unit tests in jsdom that the `hook-matrix` job runs after installing the floors over the lockfile (React 18.0.0, Vue 3.3.0, Svelte 5.0.0), and at what the lockfile installs (React 19.2.8, Vue 3.5.42, Svelte 5.56.9), in the unit tests and the browser suites; the SvelteKit renderer is measured against a component compiled by `svelte/compiler` and rendered by the real `svelte/server`.
55
55
 
56
- - Payload 2.32.3: wire corpus captured from a real admin in a one-off round (examples/ has no Payload 2 fixture), replayed in tests/integration/wire-corpus.test.ts, plus fieldSchemaJSON typing in tests/integration/schema-driven.test.ts; relationship and upload population is not verified against a real 2.x admin.
56
+ - Payload 2.32.3: wire corpus captured from a real admin and replayed in tests/integration/wire-corpus.test.ts, plus fieldSchemaJSON typing in tests/integration/schema-driven.test.ts; local release checks also exercise the real admin in DOM, React and Vue previews, each in frontend-only and serverURL modes (24 browser cases per variant), and run the React hook and the Vue composable through three lifecycle journeys (9 browser cases per variant) and seven guard journeys (21 browser cases per variant) in development builds; the legacy dependency graph retains upstream audit findings.
57
57
  - Payload 3.85.0: wire corpus captured from a real admin, replayed in tests/integration/wire-corpus.test.ts.
58
58
  - Payload 3.88.0: wire corpus captured from a real admin, replayed in tests/integration/wire-corpus.test.ts.
59
59
  - Payload 3.89.0: wire corpus captured from a real admin, replayed in tests/integration/wire-corpus.test.ts.
60
60
  - Payload 3.90.2: real admin E2E fixture (examples/payload-backend), run by CI on every push.
61
61
  - Payload 4.0.0-canary.33: wire corpus captured from a real Payload 4 admin in a one-off upgrade round, replayed in tests/integration/wire-corpus.test.ts; the fixture itself stays on 3.x.
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, and no CI job installs 4.0.0-beta.4.
62
63
  - Payload latest: daily protocol watch executes @payloadcms/live-preview@latest against the corpus.
63
64
  - Payload 4.0 pre-releases: daily protocol watch against @payloadcms/live-preview@canary, early warning only.
64
65
 
65
66
  <!-- compat-matrix:end -->
66
67
 
67
- **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. Every accepted update, a text edit too, sends one merge request, and a newer one aborts the 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).
68
+ 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
+
70
+ **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).
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.
75
+
76
+ | Version | Checked population | Passed cases |
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.
68
89
 
69
90
  ## Install
70
91
 
@@ -72,11 +93,58 @@ The React and Vue hooks and the Next.js and Nuxt fragment renderers run twice on
72
93
  npm install payload-live-preview
73
94
  ```
74
95
 
75
- Three entries cover most projects. The root `payload-live-preview` carries the client, the inline script generator, the renderers and four of the five built-in plugins; the unbound-fields overlay and `PluginManager` are on `payload-live-preview/plugins`. `payload-live-preview/astro`, `/nextjs`, `/sveltekit` and `/nuxt` hold one framework adapter each; `/nuxt-module` registers the Nuxt one from `nuxt.config.ts`, and since it cannot carry `authorizePreview` it needs `defaults: 'v1'` or `strict: false` ([docs/nuxt.md](docs/nuxt.md)). `payload-live-preview/server` is the privileged surface for server code: `definePreview()`, `authorizePreviewRequest()`, `issuePreviewToken()` and `createPreviewBindings()`. The focused entries (`core`, `client`, `lexical`, `structural`, `plugins`, `fragment`, `lean`, `payload`, `codegen`, `codegen/astro`, `annotate`, `doctor`, `migrate`) are listed in [docs/options.md](docs/options.md).
96
+ The root `payload-live-preview` carries the client, the inline script generator, the renderers and four of the five built-in plugins; the unbound-fields overlay and `PluginManager` are on `payload-live-preview/plugins`. `payload-live-preview/plugin` configures Payload's root Live Preview panel, while `/payload` keeps the lower-level URL builder. `payload-live-preview/astro`, `/nextjs`, `/sveltekit` and `/nuxt` hold one framework adapter each; `/nuxt-module` registers the Nuxt one from `nuxt.config.ts` and takes `authorizePreview` by module reference, `authorizePreviewModule` ([docs/nuxt.md](docs/nuxt.md)). `payload-live-preview/server` is the privileged surface for server code: `definePreview()`, `authorizePreviewRequest()`, `issuePreviewToken()` and `createPreviewBindings()`. Every focused entry is listed in [docs/options.md](docs/options.md).
76
97
 
77
98
  ## Configure Payload
78
99
 
79
- Enable live preview in `payload.config.ts`. The `url` callback maps the edited document to the frontend URL shown in the preview iframe; `buildLivePreviewUrl` replaces the usual lookup-table boilerplate:
100
+ The config plugin enables mapped collections and globals, installs their shared
101
+ URL callback and can set the toolbar breakpoints. It is structural across
102
+ Payload 2.32.3 and 3.x and imports neither version:
103
+
104
+ ```ts
105
+ import { livePreview } from 'payload-live-preview/plugin';
106
+
107
+ export default buildConfig({
108
+ plugins: [
109
+ livePreview({
110
+ baseUrl: process.env.FRONTEND_URL ?? 'http://localhost:4321',
111
+ collections: {
112
+ posts: ({ data }) => `/blog/${String(data['slug'] ?? '')}`,
113
+ services: ({ data, locale }) => `/${locale ?? 'en'}/services/${String(data['slug'] ?? '')}`,
114
+ },
115
+ globals: {
116
+ homepage: '/',
117
+ },
118
+ breakpoints: [
119
+ { label: 'Mobile', name: 'mobile', width: 375, height: 667 },
120
+ { label: 'Desktop', name: 'desktop', width: 1440, height: 900 },
121
+ ],
122
+ }),
123
+ ],
124
+ });
125
+ ```
126
+
127
+ Existing root collection/global lists are kept in order and deduplicated with
128
+ the mappings. Existing breakpoints stay when the option is omitted; supplying
129
+ it replaces the list. A competing root URL or a URL on a selected entity that
130
+ already exists when this transform runs is a configuration error. Make this
131
+ transform run after any plugin that may write a Live Preview URL in Payload's
132
+ effective execution order, or compose both routing policies explicitly. The
133
+ complete merge and conflict rules are in [docs/payload.md](docs/payload.md).
134
+
135
+ In the Payload 3.90.2 admin, a collection document gets its native Live
136
+ Preview after its first save. The create screen has no preview toggle and no
137
+ frame, for a function-valued URL and for a fixed string alike. The frame that
138
+ appears after the save loads the URL the plugin's function builds from the saved
139
+ document. `tests/real-payload/surface-create.spec.ts` checks this in Chromium,
140
+ Firefox and WebKit.
141
+
142
+ Payload 4 is still a pre-release. The plugin was measured once against
143
+ 4.0.0-beta.4, outside CI; the [compatibility list](#compatibility) says what
144
+ that run covered and what it left out.
145
+
146
+ Use the lower-level callback when another plugin owns the surrounding config or
147
+ when a document may have no preview route:
80
148
 
81
149
  ```ts
82
150
  import { buildLivePreviewUrl } from 'payload-live-preview/payload';
@@ -87,8 +155,9 @@ export default buildConfig({
87
155
  url: buildLivePreviewUrl({
88
156
  baseUrl: process.env.FRONTEND_URL ?? 'http://localhost:4321',
89
157
  collections: {
90
- posts: ({ data }) => `/blog/${String(data.slug ?? '')}`,
91
- services: ({ data, locale }) => `/${locale}/services/${String(data.slug ?? '')}`,
158
+ posts: ({ data }) => `/blog/${String(data['slug'] ?? '')}`,
159
+ services: ({ data, locale }) =>
160
+ `/${locale ?? 'en'}/services/${String(data['slug'] ?? '')}`,
92
161
  },
93
162
  globals: {
94
163
  homepage: '/',
@@ -106,15 +175,20 @@ export default buildConfig({
106
175
  });
107
176
  ```
108
177
 
109
- The helper appends `?preview=true`, one of the query parameters (`preview`, `draft`, `livePreview`) the adapters read as preview intent. The parameter is client-controlled: it selects delivery and never authorizes draft access. A hand-written `url: ({ data, locale, collectionConfig, globalConfig }) => string` callback works the same way; the [official docs](https://payloadcms.com/docs/live-preview/overview) have the full contract. A resolver may return `null` for a document without a route, and `fallback: null` declines every unmapped one; Payload then shows no iframe.
178
+ The helper appends `?preview=true`, one of the query parameters (`preview`, `draft`, `livePreview`) the adapters read as preview intent. The parameter is client-controlled: it selects delivery and never authorizes draft access. A hand-written `url: ({ data, locale, collectionConfig, globalConfig }) => string` callback works the same way; the [official docs](https://payloadcms.com/docs/live-preview/overview) have the full contract. The helper's nullable overload lets a resolver return `null` for a document without a route, and `fallback: null` declines every unmapped one; Payload 3 then shows no iframe. Payload 2.32.3's published callback result is string-only, so use the non-null overload and a string fallback there. The config plugin deliberately stays inside that common contract.
110
179
 
111
- To verify the iframe request without a session cookie crossing origins, mint a short-lived token inside the `url` callback with `issuePreviewToken()` and check it in the adapter with the `signed-token` strategy. [docs/authorization.md](docs/authorization.md) shows both sides, the `payload-session` and `verifier` strategies, and the initial draft read.
180
+ The config plugin accepts no signing secret or token option. Where no session
181
+ cookie crosses origins, the manual `signed-token` URL callback is a Payload 3
182
+ server-side integration only; never put its secret in a Payload 2 callback.
183
+ [docs/authorization.md](docs/authorization.md) explains that boundary, the
184
+ open multi-request token lifecycle, the `payload-session` and `verifier`
185
+ strategies, and the initial draft read.
112
186
 
113
187
  ## Quick start
114
188
 
115
189
  ### Astro
116
190
 
117
- ```ts
191
+ ```js
118
192
  // astro.config.mjs
119
193
  import { defineConfig } from 'astro/config';
120
194
  import { livePreview } from 'payload-live-preview/astro';
@@ -143,7 +217,7 @@ Then mark what should update. On a server-rendered page that is one attribute pe
143
217
 
144
218
  The attribute needs a renderer behind it: a route exporting `createFragmentEndpoint()` with `authorizePreview` or `authorize`, served at request time (an SSR adapter, `prerender = false` on that route), and its path in the options above 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.
145
219
 
146
- On a static build there is no server to render it, so the fields are bound individually — and inside a boundary too, for the ones an editor types into while watching, because a patch keeps focus and the caret where a re-render would not:
220
+ On a static build there is no server to render it, so 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:
147
221
 
148
222
  ```astro
149
223
  <h1 data-payload-field="title">{title}</h1>
@@ -153,18 +227,53 @@ On a static build there is no server to render it, so the fields are bound indiv
153
227
 
154
228
  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.
155
229
 
156
- That is it: the inline script detects the admin's iframe and starts patching. Rich text is detected from the value shape; `data-payload-richtext` only forces it. Payload 3.x 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)). Injection modes, request-time middleware, authorization and the initial draft read: [docs/astro.md](docs/astro.md).
230
+ That is it: the inline script detects the admin's iframe and starts patching. Rich text is written from the value shape, but a rich-text element that can hold uploads or relationships needs `data-payload-richtext`: only then does the runtime ask Payload to fill those nodes in ([docs/renderers.md](docs/renderers.md#media-urls)). Payload 2.x and 3.x can post 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)). Injection modes, request-time middleware, authorization and the initial draft read: [docs/astro.md](docs/astro.md).
157
231
 
158
232
  ### Other frameworks
159
233
 
160
- - **Next.js (App Router)** — `<LivePreviewScript />` in `app/layout.tsx`, the adapter middleware for headers: [docs/nextjs.md](docs/nextjs.md).
234
+ - **Next.js (App Router)** — `await LivePreviewScript(…)` in `app/layout.tsx`, the adapter middleware for headers: [docs/nextjs.md](docs/nextjs.md).
161
235
  - **SvelteKit** — `livePreviewHandle()` in `hooks.server.ts`: [docs/sveltekit.md](docs/sveltekit.md).
162
236
  - **Nuxt** — a Nitro plugin plus a server handler: [docs/nuxt.md](docs/nuxt.md).
163
237
  - **Plain HTML** — `generateInlineScript()` at build time: [docs/html.md](docs/html.md).
164
238
 
239
+ Whatever the framework, the admin also needs your frontend's URLs: the config plugin under [Configure Payload](#configure-payload) supplies them.
240
+
165
241
  ## Patch, fragment, route
166
242
 
167
- A binding is patched in place by default. A `data-payload-fragment` boundary is rendered by your server from the unsaved form state instead — conditional sections, derived values, custom blocks, the component's own logic — and morphed in with focus and visitor state intact; the runtime posts the fields to the same-origin endpoint named in `fragments: { endpoint }`, built with `createFragmentEndpoint()`, which every adapter entry exports — Astro renders through its container API, Next.js through `react-dom/server`, SvelteKit through `svelte/server`, Nuxt through `vue/server-renderer` — and patches the boundary's own bindings when the server cannot render. With `routeStrategy: true` or `fragments` set, a binding in `<head>`, or one marked `data-payload-strategy="route"`, refreshes the whole route once per revision with scroll and focus kept; without either it is patched like any other. The route is matched by top-level field name, so a dotted binding such as `meta.title` refreshes it only with `data-payload-depends="meta"`. Markup, endpoint, deployment requirements and the abuse model: [docs/hybrid.md](docs/hybrid.md).
243
+ A binding is patched in place by default. A `data-payload-fragment` boundary is rendered by your server from the unsaved form state instead — conditional sections, derived values, custom blocks, the component's own logic — and morphed into the page. Compatible retained nodes keep their focus and live properties; a node that changes kind, loses its pairing or crosses an ownership boundary is replaced and loses that state. The runtime posts the fields to the same-origin endpoint named in `fragments: { endpoint }`, built with `createFragmentEndpoint()`, which every adapter entry exports — Astro renders through its container API, Next.js through `react-dom/server`, SvelteKit through `svelte/server`, Nuxt through `vue/server-renderer` — and patches the boundary's own bindings when the server cannot render. With `routeStrategy: true` or `fragments` set, a binding in `<head>`, or one marked `data-payload-strategy="route"`, refreshes the whole route once per revision. The route morph follows the same retention rule and restores focus after a retained keyed move; it does not promise state for replaced nodes. Without either strategy the binding is patched like any other. The route is matched by top-level field name, so a dotted binding such as `meta.title` refreshes it only with `data-payload-depends="meta"`. Markup, endpoint, deployment requirements and the abuse model: [docs/hybrid.md](docs/hybrid.md).
244
+
245
+ Custom elements are protected from fragment, route and structural morphs by
246
+ default. Add the opt-in attribute `data-payload-morph` to a custom element in
247
+ your template when you want live preview to update its attributes and
248
+ server-rendered light DOM (the children inside the element).
249
+
250
+ ```html
251
+ <my-component data-payload-morph>
252
+ <!-- Server-rendered content that live preview can update -->
253
+ </my-component>
254
+ ```
255
+
256
+ The attribute must appear on the element already on the preview page and in
257
+ the updated HTML returned by your template. Adding it to the template normally
258
+ covers both. The attribute opts in when present; its value is ignored.
259
+ Compatible hosts with the same tag and namespace retain their identity,
260
+ instance properties and listeners while attributes and light DOM update.
261
+ If only one version has the attribute, or the elements are incompatible, the
262
+ existing replacement behavior applies.
263
+ Shadow roots are never touched; islands, `data-payload-owned`, editable content
264
+ and nested protected descendants remain protected. Ordinary elements are unchanged.
265
+
266
+ The morph makes the host's attributes and light DOM match the server's markup,
267
+ so what a component added to its own host or built inside it is removed or
268
+ replaced on the next update (the state attributes `open`, `value`, `checked` and
269
+ `selected` are left alone unless the template names them). Keep markup a
270
+ component owns in its shadow root, or mark it `data-payload-owned`. Components
271
+ must react to child changes, refresh cached references, preserve UI state, and
272
+ clean up listeners or observers. `connectedCallback` does not rerun for child
273
+ updates; DOM moves can still reconnect a retained host. Consumers may emit the
274
+ marker only in preview mode on initial and subsequent renders; preview
275
+ detection stays in the application. See
276
+ [ADR 0008](docs/architecture/0008-keyed-morph-ownership.md#10-custom-element-light-dom-opt-in-2026-10-10).
168
277
 
169
278
  ## Events and plugins
170
279
 
@@ -182,9 +291,9 @@ Every event, transforms, custom field renderers, the built-in plugins and the pl
182
291
 
183
292
  ## Security model
184
293
 
185
- - **Preview intent is not authorization.** The query parameter, the iframe destination and the referer are client-controlled; the adapters count the query alone by default. `allowedOrigins` governs browser `postMessage` senders and `shouldInject` filters routes; neither authenticates the request. `authorizePreviewRequest()` does, and where an adapter decides per request with `authorizePreview` — `createLivePreviewMiddleware()`, `livePreviewHandle()`, `livePreviewNitroPlugin()`, `defineLivePreviewServerHandler()` — that one result controls draft reads, `private, no-store` caching, CSP changes and runtime injection, and `<LivePreviewScript />` gates the Next.js script on the same verdict. Other setups decide less. Astro's build-time `inline` and `loader` modes and the synchronous `livePreviewScriptProps()` and `renderLivePreviewScript()` put the runtime or its bootstrap into every page that renders them, where it does not start outside a preview frame. Astro's `mode: 'middleware'` and the Nuxt module cannot carry `authorizePreview`, so they need `defaults: 'v1'` (or `strict: false`) and inject on intent alone.
186
- - **Origin validation.** Every incoming message is checked against `allowedOrigins`; `document.referrer` is ignored by default, and after the first accepted update the inline runtime and `LivePreviewClient` lock to that origin (the React and Vue hooks do not). The adapters merge a `frame-ancestors` policy for the admin origins without clobbering the rest of your CSP.
187
- - **Sanitization and URL validation.** Lexical and HTML writes pass a DOM sanitizer with a curated allow-list — `<script>`, `<form>`, `<iframe>`, `<svg>`, event handlers and `style` are rejected — and every `href`, `src`, `srcset` and `poster` must be `http(s)`, `mailto:`, `tel:` or relative; external links get `rel="noopener noreferrer"`.
294
+ - **Preview intent is not authorization.** The query parameter, the iframe destination and the referer are client-controlled; the adapters count the query alone by default. `allowedOrigins` governs browser `postMessage` senders and `shouldInject` filters routes; neither authenticates the request. `authorizePreviewRequest()` does, and where an adapter decides per request with `authorizePreview` — `createLivePreviewMiddleware()`, `livePreviewHandle()`, `livePreviewNitroPlugin()`, `defineLivePreviewServerHandler()` — that one result controls draft reads, `private, no-store` caching, CSP changes and runtime injection, and `await LivePreviewScript(…)` gates the Next.js script on the same verdict. Other setups decide less. Astro's build-time `inline` and `loader` modes and the synchronous `livePreviewScriptProps()` and `renderLivePreviewScript()` put the runtime or its bootstrap into every page that renders them, where it does not start outside a preview frame. Astro's `mode: 'middleware'` and the Nuxt module serialize their options, so they take the hook by module reference (`authorizePreviewModule`); with neither hook nor reference they refuse the strict default, and `strict: false` (or the deprecated `defaults: 'v1'`) injects on intent alone.
295
+ - **Origin validation.** Every incoming message is checked against `allowedOrigins`; `document.referrer` is ignored by default, and after the first accepted update the inline runtime and `LivePreviewClient` lock to that origin (the React and Vue hooks do not: a message from a second listed origin is taken after the first, when the source policy lets its window speak). The adapters merge a `frame-ancestors` policy for the admin origins without clobbering the rest of your CSP.
296
+ - **Sanitization and URL validation.** Lexical and HTML writes in the browser pass a DOM sanitizer with a curated allow-list — `<script>`, `<form>`, `<iframe>`, `<svg>`, event handlers and `style` are rejected — and every `href`, `src`, `srcset` and `poster` must be `http(s)`, `mailto:`, `tel:` or relative; external links get `rel="noopener noreferrer"`. A server render sanitizes with the `document` it is given; without one, `lexicalToHtml()` returns custom renderers' markup as written and warns, and `requireDocument: true` makes it throw instead, the 3.0 default ([docs/security.md](docs/security.md#html-sanitization)).
188
297
  - **Binding attributes are disclosure.** `data-payload-field` names a CMS field and `data-payload-owner` a document. `createPreviewBindings({ authorization })` suppresses them, companions included, on public responses. Never key CSS off `data-payload-*`.
189
298
  - **Policed writes, no prototype pollution.** `data-payload-attribute` refuses event handlers, `style`, `srcdoc`, `formaction`, `form`, `id`, `name`, `is`, `srcset` and `imagesrcset`; nested lookups refuse `__proto__`, `prototype` and `constructor`, and incoming data is never merged into existing objects.
190
299
 
@@ -194,8 +303,8 @@ Full details in [docs/security.md](docs/security.md). Report vulnerabilities per
194
303
 
195
304
  - **Nothing updates.** Run `__livePreview.inspect()` in the preview iframe's console first — the handle exists once the injected runtime has started inside a preview frame; a page that starts `LivePreviewClient` itself calls `client.inspect()`. It names the cause in most cases. The common ones: the admin origin is missing from `allowedOrigins`, the page is not inside an iframe, the bound element does not exist (an empty field needs an anchor).
196
305
  - **Relationship fields show IDs.** Set `serverURL` with `mergeDepth`; Payload 3.x posts unpopulated form values.
197
- - **The preview iframe refuses to load.** The host sets `X-Frame-Options` or a restrictive `frame-ancestors`. The adapters merge `frame-ancestors` on authorized preview responses; an `X-Frame-Options: DENY` from a proxy must go.
198
- - **The adapter refuses to start.** The strict default needs `authorizePreview`, a non-empty `allowedOrigins` (`https:` in production) and no referer trust; `defaults: 'v1'` stages a migration one row at a time.
306
+ - **The preview iframe refuses to load.** The host sets `X-Frame-Options` or a restrictive `frame-ancestors`. The adapters merge `frame-ancestors` on authorized preview responses, and browsers frame by that directive alone; an `X-Frame-Options: DENY` from a proxy still blocks a preview response that has none, so it must go there.
307
+ - **The adapter refuses the configuration.** The strict default needs `authorizePreview`, a non-empty `allowedOrigins` (`https:` in production) and no referer trust; the deprecated `defaults: 'v1'` stages a migration one row at a time, and `pll migrate` writes out the rows it stands for.
199
308
 
200
309
  `inspect()` readings, `pll doctor` and every diagnostic code: [docs/troubleshooting.md](docs/troubleshooting.md). `pll doctor` sends its preview request with `?preview=true`; a preview behind `authorizePreview` is audited with an editor's credential. A signed token goes in the URL as `?previewToken=…`, which the visitor request drops; `--header "x-preview-token: …"` works only where the token strategy sets `transport: { kind: 'header' }`; a Payload session is `--header "Cookie: payload-token=…"`.
201
310
 
@@ -205,6 +314,7 @@ The reading path, with a glossary: [docs/README.md](docs/README.md).
205
314
 
206
315
  - Framework guides: [Astro](docs/astro.md) · [Next.js](docs/nextjs.md) · [SvelteKit](docs/sveltekit.md) · [Nuxt](docs/nuxt.md) · [Plain HTML](docs/html.md) · [React hook](docs/react.md) · [Vue composable](docs/vue.md)
207
316
  - [docs/bindings.md](docs/bindings.md) — data attributes, field types, owners, typed bindings and codegen
317
+ - [docs/payload.md](docs/payload.md) — Payload 2/3 config plugin, merge rules and URL conflicts
208
318
  - [docs/options.md](docs/options.md) — package entries, every option and its default, Payload 3.x population
209
319
  - [docs/authorization.md](docs/authorization.md) — strategies, signed tokens, the initial draft read
210
320
  - [docs/hybrid.md](docs/hybrid.md) — patch, fragment and route
@@ -212,7 +322,7 @@ The reading path, with a glossary: [docs/README.md](docs/README.md).
212
322
  - [docs/deployment.md](docs/deployment.md) — CSP, caches, proxies
213
323
  - [docs/troubleshooting.md](docs/troubleshooting.md) — `inspect()`, `pll doctor`, diagnostic codes
214
324
  - [docs/security.md](docs/security.md) · [docs/migration.md](docs/migration.md) · [docs/interop.md](docs/interop.md) · [docs/reveal.md](docs/reveal.md)
215
- - Maintainers: [docs/testing.md](docs/testing.md), [docs/benchmarks.md](docs/benchmarks.md), [architecture decisions](docs/architecture/README.md), [CONTRIBUTING.md](CONTRIBUTING.md)
325
+ - Maintainers: [docs/testing.md](docs/testing.md), [docs/benchmarks.md](docs/benchmarks.md), [docs/audit.md](docs/audit.md), [architecture decisions](docs/architecture/README.md), [CONTRIBUTING.md](CONTRIBUTING.md)
216
326
 
217
327
  ## License
218
328
 
@@ -0,0 +1,25 @@
1
+ ---
2
+ /** The consumer's Astro compiler owns this internal template, not a private factory.
3
+ * Fresh container locals carry only the registry-selected component and its props.
4
+ */
5
+ import type { AstroInstance } from 'astro';
6
+
7
+ type BridgeData = {
8
+ component: AstroInstance['default'];
9
+ props: Record<string, unknown>;
10
+ };
11
+
12
+ function isBridgeData(value: unknown): value is BridgeData {
13
+ return (
14
+ typeof value === 'object' && value !== null &&
15
+ 'component' in value && typeof value.component === 'function' &&
16
+ 'props' in value && typeof value.props === 'object' && value.props !== null &&
17
+ !Array.isArray(value.props)
18
+ );
19
+ }
20
+
21
+ const bridge: unknown = Reflect.get(Astro.locals, '__payloadLivePreviewFragment');
22
+ if (!isBridgeData(bridge)) throw new Error('Missing trusted fragment bridge');
23
+ const Component = bridge.component;
24
+ ---
25
+ <Component {...bridge.props} />
@@ -7,10 +7,8 @@
7
7
  * see when they fill a previously empty field. `PreviewBoundary` renders the
8
8
  * wrapper always: with content when the value is present, and as an empty,
9
9
  * `hidden` element otherwise — out of layout and out of the accessibility
10
- * tree, so a public visitor and a screen reader see exactly what they would
11
- * have seen without live preview. When an update fills the field, the
12
- * runtime removes `hidden`; when it empties it, the runtime hides the
13
- * wrapper again.
10
+ * tree. When an update fills the field, the runtime removes `hidden`; when
11
+ * it empties it, the runtime hides the wrapper again.
14
12
  *
15
13
  * Usage:
16
14
  *
@@ -32,12 +32,13 @@
32
32
  * Their output is yours to escape here. The built-in renderers escape
33
33
  * every value they interpolate, but the sanitizer that would catch a
34
34
  * custom renderer's mistake needs a DOM, and there is none during SSR
35
- * unless the project supplied one with `setSanitizerDocument()`. See
36
- * "HTML sanitization" in docs/security.md.
35
+ * unless the page passes one in `document`. With `requireDocument` the
36
+ * render throws instead of returning unsanitised markup, as 3.0 will by
37
+ * default (ADR 0025). See "HTML sanitization" in docs/security.md.
37
38
  */
38
39
  // Self-referencing import so the shipped copy in dist/ resolves
39
40
  // through the package exports map in consumer projects.
40
- import { isLexicalContent, lexicalToHtml } from 'payload-live-preview';
41
+ import { isLexicalContent, lexicalToHtml, type SanitizerDocument } from 'payload-live-preview';
41
42
 
42
43
  interface Props {
43
44
  /** The Lexical value from Payload (may be null/undefined/empty). */
@@ -50,11 +51,31 @@ interface Props {
50
51
  /** Wrapper element. Default `div`. */
51
52
  as?: string;
52
53
  class?: string;
54
+ /**
55
+ * The DOM the sanitizer parses with on the server, e.g. linkedom's
56
+ * `parseHTML(...).document`, for this render. Without it the document
57
+ * `setSanitizerDocument()` supplied is used.
58
+ */
59
+ document?: SanitizerDocument;
60
+ /** Throw instead of rendering unsanitised markup when no document is available (ADR 0025). */
61
+ requireDocument?: boolean;
53
62
  }
54
63
 
55
- const { value, field, as: tag = 'div', class: className } = Astro.props;
64
+ const {
65
+ value,
66
+ field,
67
+ as: tag = 'div',
68
+ class: className,
69
+ document: sanitizerDocument,
70
+ requireDocument,
71
+ } = Astro.props;
56
72
  const Tag = tag;
57
- const html = isLexicalContent(value) ? lexicalToHtml(value) : '';
73
+ const html = isLexicalContent(value)
74
+ ? lexicalToHtml(value, {
75
+ ...(sanitizerDocument === undefined ? {} : { document: sanitizerDocument }),
76
+ ...(requireDocument === undefined ? {} : { requireDocument }),
77
+ })
78
+ : '';
58
79
  ---
59
80
 
60
81
  <Tag
@@ -1,15 +1,16 @@
1
- import { L as LivePreviewAstroOptions } from '../../middleware-CWObDO37.js';
2
- export { a as LivePreviewMiddleware, c as createLivePreviewMiddleware } from '../../middleware-CWObDO37.js';
3
- import { h as hasPreviewIntent } from '../../options-uHKrDM2U.js';
4
- export { P as PreviewAdapterOptions, a as PreviewRequestLike, b as PreviewRequestOptions, c as PreviewSignal } from '../../options-uHKrDM2U.js';
5
- import { F as FragmentEndpointOptions$1, a as FragmentRegistry$1, b as FragmentRegistryEntry$1, c as FragmentRenderer$1 } from '../../fragment-endpoint-DATKU183.js';
6
- export { d as FragmentRenderInput } from '../../fragment-endpoint-DATKU183.js';
7
- export { A as AUTHORIZATION_LOCALS_KEY, a as AUTHORIZATION_OUTCOME_LOCALS_KEY, L as LivePreviewLocals, N as NONCE_LOCALS_KEY } from '../../locals-CNUGYbA_.js';
1
+ import { L as LivePreviewAstroOptions } from '../../middleware-BGobLN9P.js';
2
+ export { a as LivePreviewMiddleware, c as createLivePreviewMiddleware } from '../../middleware-BGobLN9P.js';
3
+ import { h as hasPreviewIntent } from '../../options-Cm8fC7dj.js';
4
+ export { P as PreviewAdapterOptions, a as PreviewRequestLike, b as PreviewRequestOptions, c as PreviewSignal } from '../../options-Cm8fC7dj.js';
5
+ import { F as FragmentEndpointOptions$1, a as FragmentRegistry$1, b as FragmentRegistryEntry$1, c as FragmentRenderer$1 } from '../../fragment-endpoint-B2qcRw5T.js';
6
+ export { d as FragmentRenderInput } from '../../fragment-endpoint-B2qcRw5T.js';
7
+ export { A as AUTHORIZATION_LOCALS_KEY, a as AUTHORIZATION_OUTCOME_LOCALS_KEY, L as LivePreviewLocals, N as NONCE_LOCALS_KEY } from '../../locals-B1mF3aTb.js';
8
8
 
9
9
  /**
10
10
  * The Astro integration for `astro.config.mjs`: `livePreview({ ... })`.
11
11
  * Inline and loader modes deliver at build time; middleware mode registers
12
- * the request-time middleware through a serialized options module.
12
+ * the request-time middleware through a serialized options module. Every
13
+ * build also records its island module table for fragment containers (ADR 0021).
13
14
  */
14
15
 
15
16
  type ScriptStage = 'head-inline' | 'page' | 'before-hydration' | 'page-ssr';
@@ -60,11 +61,41 @@ interface AstroIntegrationLike {
60
61
  readonly name: string;
61
62
  readonly hooks: {
62
63
  readonly 'astro:config:setup': (ctx: AstroConfigSetupContext) => void;
64
+ readonly 'astro:config:done': (ctx: {
65
+ readonly config: {
66
+ readonly output?: string;
67
+ readonly build?: {
68
+ readonly server?: URL;
69
+ };
70
+ };
71
+ readonly buildOutput?: string;
72
+ }) => void;
73
+ readonly 'astro:build:ssr': (ctx: {
74
+ readonly manifest: {
75
+ readonly entryModules?: Readonly<Record<string, string>>;
76
+ readonly base?: string;
77
+ readonly assetsPrefix?: string | Readonly<Record<string, string>>;
78
+ };
79
+ }) => void;
80
+ readonly 'astro:build:done': (ctx: {
81
+ readonly logger?: {
82
+ readonly info: (message: string) => void;
83
+ };
84
+ }) => Promise<void>;
63
85
  };
64
86
  }
65
87
  /** Build the integration. The injected runtime stays inert outside the admin's preview iframe. */
66
88
  declare function livePreview(options?: LivePreviewAstroOptions): AstroIntegrationLike;
67
89
 
90
+ /** Resolves an island's component, renderer and before-hydration specifiers. */
91
+ type IslandModuleResolver = (specifier: string) => Promise<string>;
92
+ /**
93
+ * Pass as the container's `resolve`, next to the framework's server and client
94
+ * renderers: `AstroContainer.create({ resolve: resolveIslandModule })`.
95
+ * Needs Astro 4.16 or later, whose container accepts `resolve`.
96
+ */
97
+ declare const resolveIslandModule: IslandModuleResolver;
98
+
68
99
  /**
69
100
  * Manual embedding for `.astro` templates, for the cases the integration's
70
101
  * automatic injection does not fit (one layout, a conditional).
@@ -108,6 +139,7 @@ type FragmentEndpointOptions = FragmentEndpointOptions$1<AstroComponentLike>;
108
139
  /** Build the endpoint; export it as the `POST` of a non-prerendered Astro API route. */
109
140
  declare function createFragmentEndpoint(options: FragmentEndpointOptions): (context: {
110
141
  readonly request: Request;
142
+ readonly locals?: unknown;
111
143
  }) => Promise<Response>;
112
144
 
113
- export { type AstroComponentLike, type AstroIntegrationLike, type FragmentEndpointOptions, type FragmentRegistry, type FragmentRegistryEntry, type FragmentRenderer, LivePreviewAstroOptions, type RenderScriptOptions, createFragmentEndpoint, hasPreviewIntent, isPreviewRequest, livePreview, renderLivePreviewScript };
145
+ export { type AstroComponentLike, type AstroIntegrationLike, type FragmentEndpointOptions, type FragmentRegistry, type FragmentRegistryEntry, type FragmentRenderer, type IslandModuleResolver, LivePreviewAstroOptions, type RenderScriptOptions, createFragmentEndpoint, hasPreviewIntent, isPreviewRequest, livePreview, renderLivePreviewScript, resolveIslandModule };