payload-live-preview 2.0.6 → 2.1.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.
Files changed (116) hide show
  1. package/README.md +141 -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 +544 -392
  45. package/dist/core.d.ts +544 -392
  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 +598 -400
  65. package/dist/index.d.ts +598 -400
  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 +43 -4
  107. package/dist/server.d.ts +43 -4
  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,50 +10,50 @@
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 prerelease 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.
@@ -64,7 +64,27 @@ The React and Vue hooks and the Next.js and Nuxt fragment renderers run twice on
64
64
 
65
65
  <!-- compat-matrix:end -->
66
66
 
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).
67
+ 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.
68
+
69
+ ### Local 2.1 prerelease checks
70
+
71
+ The following checks passed on the 2.1.0 release candidate, the package archive the release pull request's CI built, on 2026-10-18; the Node row on 2026-10-22. 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.
72
+
73
+ | Version | Checked population | Passed cases |
74
+ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- |
75
+ | Payload 2.32.3 | DOM, React and Vue, each with frontend-only and serverURL modes | 24 per variant, six variants |
76
+ | Payload 2.32.3 | React and Vue hook lifecycle, frontend-only and serverURL | 9 per variant, four variants |
77
+ | Payload 2.32.3 | React and Vue hook guards, frontend-only and serverURL | 21 per variant, four variants |
78
+ | 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 |
79
+ | Next.js 16.3.8 | Webpack production preview and fragment rendering | 54 browser cases |
80
+ | Astro 4.0.0 | Base adapter with loader and inline delivery, static output and scoped updates, two control builds | 36 browser cases |
81
+ | Astro 4.16.19 | Native React islands, CSP and navigation ownership | 12 browser cases |
82
+
83
+ 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.
84
+
85
+ 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.
86
+
87
+ **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).
68
88
 
69
89
  ## Install
70
90
 
@@ -72,11 +92,54 @@ The React and Vue hooks and the Next.js and Nuxt fragment renderers run twice on
72
92
  npm install payload-live-preview
73
93
  ```
74
94
 
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).
95
+ 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
96
 
77
97
  ## Configure Payload
78
98
 
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:
99
+ The config plugin enables mapped collections and globals, installs their shared
100
+ URL callback and can set the toolbar breakpoints. It is structural across
101
+ Payload 2.32.3 and 3.x and imports neither version:
102
+
103
+ ```ts
104
+ import { livePreview } from 'payload-live-preview/plugin';
105
+
106
+ export default buildConfig({
107
+ plugins: [
108
+ livePreview({
109
+ baseUrl: process.env.FRONTEND_URL ?? 'http://localhost:4321',
110
+ collections: {
111
+ posts: ({ data }) => `/blog/${String(data['slug'] ?? '')}`,
112
+ services: ({ data, locale }) => `/${locale ?? 'en'}/services/${String(data['slug'] ?? '')}`,
113
+ },
114
+ globals: {
115
+ homepage: '/',
116
+ },
117
+ breakpoints: [
118
+ { label: 'Mobile', name: 'mobile', width: 375, height: 667 },
119
+ { label: 'Desktop', name: 'desktop', width: 1440, height: 900 },
120
+ ],
121
+ }),
122
+ ],
123
+ });
124
+ ```
125
+
126
+ Existing root collection/global lists are kept in order and deduplicated with
127
+ the mappings. Existing breakpoints stay when the option is omitted; supplying
128
+ it replaces the list. A competing root URL or a URL on a selected entity that
129
+ already exists when this transform runs is a configuration error. Make this
130
+ transform run after any plugin that may write a Live Preview URL in Payload's
131
+ effective execution order, or compose both routing policies explicitly. The
132
+ complete merge and conflict rules are in [docs/payload.md](docs/payload.md).
133
+
134
+ In the Payload 3.90.2 admin, a collection document gets its native Live
135
+ Preview after its first save. The create screen has no preview toggle and no
136
+ frame, for a function-valued URL and for a fixed string alike. The frame that
137
+ appears after the save loads the URL the plugin's function builds from the saved
138
+ document. `tests/real-payload/surface-create.spec.ts` checks this in Chromium,
139
+ Firefox and WebKit.
140
+
141
+ Use the lower-level callback when another plugin owns the surrounding config or
142
+ when a document may have no preview route:
80
143
 
81
144
  ```ts
82
145
  import { buildLivePreviewUrl } from 'payload-live-preview/payload';
@@ -87,8 +150,9 @@ export default buildConfig({
87
150
  url: buildLivePreviewUrl({
88
151
  baseUrl: process.env.FRONTEND_URL ?? 'http://localhost:4321',
89
152
  collections: {
90
- posts: ({ data }) => `/blog/${String(data.slug ?? '')}`,
91
- services: ({ data, locale }) => `/${locale}/services/${String(data.slug ?? '')}`,
153
+ posts: ({ data }) => `/blog/${String(data['slug'] ?? '')}`,
154
+ services: ({ data, locale }) =>
155
+ `/${locale ?? 'en'}/services/${String(data['slug'] ?? '')}`,
92
156
  },
93
157
  globals: {
94
158
  homepage: '/',
@@ -106,15 +170,20 @@ export default buildConfig({
106
170
  });
107
171
  ```
108
172
 
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.
173
+ 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
174
 
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.
175
+ The config plugin accepts no signing secret or token option. Where no session
176
+ cookie crosses origins, the manual `signed-token` URL callback is a Payload 3
177
+ server-side integration only; never put its secret in a Payload 2 callback.
178
+ [docs/authorization.md](docs/authorization.md) explains that boundary, the
179
+ open multi-request token lifecycle, the `payload-session` and `verifier`
180
+ strategies, and the initial draft read.
112
181
 
113
182
  ## Quick start
114
183
 
115
184
  ### Astro
116
185
 
117
- ```ts
186
+ ```js
118
187
  // astro.config.mjs
119
188
  import { defineConfig } from 'astro/config';
120
189
  import { livePreview } from 'payload-live-preview/astro';
@@ -143,7 +212,7 @@ Then mark what should update. On a server-rendered page that is one attribute pe
143
212
 
144
213
  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
214
 
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:
215
+ 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
216
 
148
217
  ```astro
149
218
  <h1 data-payload-field="title">{title}</h1>
@@ -153,18 +222,51 @@ On a static build there is no server to render it, so the fields are bound indiv
153
222
 
154
223
  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
224
 
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).
225
+ 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
226
 
158
227
  ### Other frameworks
159
228
 
160
- - **Next.js (App Router)** — `<LivePreviewScript />` in `app/layout.tsx`, the adapter middleware for headers: [docs/nextjs.md](docs/nextjs.md).
229
+ - **Next.js (App Router)** — `await LivePreviewScript(…)` in `app/layout.tsx`, the adapter middleware for headers: [docs/nextjs.md](docs/nextjs.md).
161
230
  - **SvelteKit** — `livePreviewHandle()` in `hooks.server.ts`: [docs/sveltekit.md](docs/sveltekit.md).
162
231
  - **Nuxt** — a Nitro plugin plus a server handler: [docs/nuxt.md](docs/nuxt.md).
163
232
  - **Plain HTML** — `generateInlineScript()` at build time: [docs/html.md](docs/html.md).
164
233
 
165
234
  ## Patch, fragment, route
166
235
 
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).
236
+ 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).
237
+
238
+ Custom elements are protected from fragment, route and structural morphs by
239
+ default. Add the opt-in attribute `data-payload-morph` to a custom element in
240
+ your template when you want live preview to update its attributes and
241
+ server-rendered light DOM (the children inside the element).
242
+
243
+ ```html
244
+ <my-component data-payload-morph>
245
+ <!-- Server-rendered content that live preview can update -->
246
+ </my-component>
247
+ ```
248
+
249
+ The attribute must appear on the element already on the preview page and in
250
+ the updated HTML returned by your template. Adding it to the template normally
251
+ covers both. The attribute opts in when present; its value is ignored.
252
+ Compatible hosts with the same tag and namespace retain their identity,
253
+ instance properties and listeners while attributes and light DOM update.
254
+ If only one version has the attribute, or the elements are incompatible, the
255
+ existing replacement behavior applies.
256
+ Shadow roots are never touched; islands, `data-payload-owned`, editable content
257
+ and nested protected descendants remain protected. Ordinary elements are unchanged.
258
+
259
+ The morph makes the host's attributes and light DOM match the server's markup,
260
+ so what a component added to its own host or built inside it is removed or
261
+ replaced on the next update (the state attributes `open`, `value`, `checked` and
262
+ `selected` are left alone unless the template names them). Keep markup a
263
+ component owns in its shadow root, or mark it `data-payload-owned`. Components
264
+ must react to child changes, refresh cached references, preserve UI state, and
265
+ clean up listeners or observers. `connectedCallback` does not rerun for child
266
+ updates; DOM moves can still reconnect a retained host. Consumers may emit the
267
+ marker only in preview mode on initial and subsequent renders; preview
268
+ detection stays in the application. See
269
+ [ADR 0008](docs/architecture/0008-keyed-morph-ownership.md#10-custom-element-light-dom-opt-in-2026-10-10).
168
270
 
169
271
  ## Events and plugins
170
272
 
@@ -182,9 +284,9 @@ Every event, transforms, custom field renderers, the built-in plugins and the pl
182
284
 
183
285
  ## Security model
184
286
 
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"`.
287
+ - **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 `defaults: 'v1'` (or `strict: false`) injects on intent alone.
288
+ - **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.
289
+ - **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
290
  - **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
291
  - **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
292
 
@@ -194,8 +296,8 @@ Full details in [docs/security.md](docs/security.md). Report vulnerabilities per
194
296
 
195
297
  - **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
298
  - **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.
299
+ - **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.
300
+ - **The adapter refuses the configuration.** 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.
199
301
 
200
302
  `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
303
 
@@ -205,6 +307,7 @@ The reading path, with a glossary: [docs/README.md](docs/README.md).
205
307
 
206
308
  - 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
309
  - [docs/bindings.md](docs/bindings.md) — data attributes, field types, owners, typed bindings and codegen
310
+ - [docs/payload.md](docs/payload.md) — Payload 2/3 config plugin, merge rules and URL conflicts
208
311
  - [docs/options.md](docs/options.md) — package entries, every option and its default, Payload 3.x population
209
312
  - [docs/authorization.md](docs/authorization.md) — strategies, signed tokens, the initial draft read
210
313
  - [docs/hybrid.md](docs/hybrid.md) — patch, fragment and route
@@ -212,7 +315,7 @@ The reading path, with a glossary: [docs/README.md](docs/README.md).
212
315
  - [docs/deployment.md](docs/deployment.md) — CSP, caches, proxies
213
316
  - [docs/troubleshooting.md](docs/troubleshooting.md) — `inspect()`, `pll doctor`, diagnostic codes
214
317
  - [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)
318
+ - 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
319
 
217
320
  ## License
218
321
 
@@ -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 };