Skip to content

Commit 7e16cf7

Browse files
authored
feat: CMS live preview SDK (#5435)
1 parent 7e2f6f5 commit 7e16cf7

167 files changed

Lines changed: 6016 additions & 183 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,7 @@ Available factories:
6969

7070
This project uses the Webiny framework.
7171
A `webiny` MCP server is available.
72-
When helping with Webiny-related tasks:
73-
74-
1. Call `list_webiny_skills` to see available skills.
75-
2. Call `get_webiny_skill` with the relevant topic before writing code.
72+
When helping with Webiny-related tasks, call `get_started()` first.
7673

7774
## CI/CD - GitHub Actions
7875

CLAUDE.md

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,4 @@ If any of the steps fail, and you fix anything, you must rerun all scripts from
7676

7777
This project uses the Webiny framework.
7878
A `webiny` MCP server is available.
79-
When helping with Webiny-related tasks:
80-
81-
1. Call `list_webiny_skills` to see available skills.
82-
2. Call `get_webiny_skill` with the relevant topic before writing code.
79+
When helping with Webiny-related tasks, call `get_started()` first.
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
# Plan: CMS Preview SDK & Component Mapping System
2+
3+
> Source PRD: `ai-context/prds/cms-preview-sdk.md`
4+
5+
## Architectural decisions
6+
7+
Durable decisions that apply across all phases:
8+
9+
- **Packages**: Three packages — `@webiny/cms-sdk` (core), `@webiny/cms-nextjs` (Next.js binding), and the existing `@webiny/sdk` for API calls. Mirrors the WB pattern: `website-builder-sdk``website-builder-react``website-builder-nextjs`.
10+
- **Model schema**: `CmsModel.settings.previewUrl: string` for the preview URL. `CmsDynamicZoneTemplate.componentName: string` for the component mapping. Both exposed via the existing `getContentModel` query.
11+
- **SDK modes**: Three modes detected automatically — `live` (production, fetches published), `preview` (drafts via `preview: true`), `editing` (iframe, receives data via postMessage). Same environment detection as WB SDK (`window.parent !== window`).
12+
- **Bridge protocol**: Reuses the WB `Messenger` pattern — `postMessage` with origin validation and namespaced event types (e.g., `cms.preview.*`).
13+
- **Template discriminator**: `_templateId` is the universal discriminator. No `__typename` anywhere in the SDK or rendering pipeline.
14+
- **Entry fetching**: Uses the existing Webiny SDK's `getEntry`/`listEntries`. The SDK passes `["values.*"]` as the field selection — a new wildcard convention. The backend's `getEntry` resolver expands `values.*` to the full field selection using the model's `ValuesSelectionGenerator` (the model and AST are already available in the resolver). This means the SDK never needs to know about field types or DZ structures — it just asks for "all values" and the server handles it. `ref` fields are excluded from the generated selection.
15+
- **Component resolution**: `_templateId` → model template definition → `componentName` → component registry lookup → render.
16+
17+
---
18+
19+
## Phase 0: Wildcard field selection (`values.*`)
20+
21+
**User stories**: 1
22+
23+
### What to build
24+
25+
Add support for the `values.*` wildcard in the backend's generic `getEntry`/`listEntries` resolvers. This is the foundation that lets the SDK (and any API consumer) fetch all entry values without knowing the model's field structure.
26+
27+
**Backend**: In the generic CMS resolver (`getEntryResolver.ts`), when the `fields` array contains `"values.*"`, expand it to `values { <full selection> }` using the model's `ValuesSelectionGenerator`. The model is already resolved in the resolver, and the `ValuesSelectionGenerator` is already a registered service. The expansion replaces the `values.*` entry in the fields array with the generated selection; other fields (like `id`, `entryId`, `createdOn`) pass through unchanged.
28+
29+
The same wildcard support should apply to `listEntries` as well.
30+
31+
**Test**: Write tests that verify:
32+
33+
- `values.*` expands to the correct GraphQL selection for models with scalar fields, object fields, DZ fields (with templates), and nested DZs
34+
- Mixing `values.*` with explicit system fields (`id`, `entryId`) works correctly
35+
- Models with no fields produce a valid (empty or minimal) selection
36+
- The expanded query actually returns the expected entry data end-to-end (integration test via the existing CMS test infrastructure)
37+
38+
### Acceptance criteria
39+
40+
- [ ] `getEntry` resolver expands `values.*` to the full values selection using `ValuesSelectionGenerator`
41+
- [ ] `listEntries` resolver supports the same `values.*` wildcard
42+
- [ ] Explicit fields (`id`, `entryId`, `createdOn`) can be mixed with `values.*`
43+
- [ ] DZ template values are returned with `_templateId` and all template fields
44+
- [ ] Object fields and nested structures are fully expanded
45+
- [ ] Models with no fields don't error out
46+
- [ ] Integration tests pass for models with scalar, object, DZ, and nested DZ fields
47+
48+
---
49+
50+
## Phase 1: Model preview URL + SDK skeleton + basic entry rendering
51+
52+
**User stories**: 1, 4, 12, 13, 17
53+
54+
### What to build
55+
56+
The thinnest possible end-to-end slice: a CMS model gets a preview URL setting, a new SDK package can fetch and display that model's entries on a Next.js page.
57+
58+
**API side**: Add `previewUrl` to the `CmsModel.settings` schema. The model editor UI gets a "Preview URL" text field in the model settings area. The value is persisted and returned in `getContentModel` responses.
59+
60+
**SDK packages**: Create `@webiny/cms-sdk` with the core architecture — `ContentSdk` singleton with `init(config)`, environment detection (`isClient`, `isServer`, `isEditing`), and a data provider that wraps the existing `@webiny/sdk` `CmsSdk`. The SDK always passes `["id", "entryId", "values.*"]` to `getEntry`. Create `@webiny/cms-nextjs` as a thin Next.js wrapper (headers provider, re-exports).
61+
62+
**Next.js demo**: A basic Next.js page fetches an entry via the SDK and renders scalar fields (title, slug, body) as plain HTML. No DZ component rendering yet — DZ values are accessible as raw data.
63+
64+
### Acceptance criteria
65+
66+
- [ ] `CmsModel.settings.previewUrl` is persisted and returned in the `getContentModel` GraphQL response
67+
- [ ] Model editor UI has a "Preview URL" field in model settings
68+
- [ ] `@webiny/cms-sdk` package exists with `ContentSdk.init(config)`, environment detection, and `getEntry(modelId, entryId)` that passes `["id", "entryId", "values.*"]`
69+
- [ ] `@webiny/cms-nextjs` package exists and re-exports the core SDK with Next.js headers provider
70+
- [ ] A Next.js page can fetch and render an entry's scalar fields without writing any GraphQL
71+
- [ ] SDK supports `preview: true` config to fetch draft entries
72+
- [ ] SDK works in both SSR and client-side contexts
73+
74+
---
75+
76+
## Phase 2: Component registration + bridge discovery
77+
78+
**User stories**: 2, 3, 5, 20
79+
80+
### What to build
81+
82+
The component catalog system: frontend developers register components with the SDK, and the CMS model editor discovers them via an iframe bridge.
83+
84+
**SDK side**: Add `createComponent(ReactComponent, manifest)` factory (mirrors WB's pattern). Add `ComponentRegistry` that stores registered components. When the SDK detects editing mode (iframe), it establishes a `Messenger` connection and sends `cms.preview.component.register` messages for each registered component — including name, label, and input definitions.
85+
86+
**Admin side**: The DZ template editing dialog gets a "Component" picker. When the model has a `previewUrl`, the dialog opens a hidden iframe to that URL. The bridge receives component registrations and populates the picker dropdown. The user selects which component renders this template.
87+
88+
**Per-route scoping**: Components are passed to the SDK's entry renderer (like WB's `DocumentRenderer` receiving `components` prop), so each route can declare its own component catalog.
89+
90+
### Acceptance criteria
91+
92+
- [ ] `createComponent(Component, manifest)` creates a component blueprint with name, label, and input definitions
93+
- [ ] Components are registered with the SDK and sent to the CMS editor via the bridge when in editing mode
94+
- [ ] The DZ template editor shows a "Component" dropdown populated from the bridge-discovered catalog
95+
- [ ] Only components registered on the preview URL page for that model appear in the picker
96+
- [ ] Component discovery happens live — adding a new component on the frontend and refreshing the iframe makes it appear immediately
97+
98+
---
99+
100+
## Phase 3: Component mapping storage + DZ rendering
101+
102+
**User stories**: 6, 9, 10, 14, 16
103+
104+
### What to build
105+
106+
Store the template→component mapping and render DZ entries as actual React components.
107+
108+
**API side**: Add `componentName: string` to `CmsDynamicZoneTemplate`. When the user selects a component in the template editor (Phase 2), the `componentName` is saved on the template. It's returned as part of the model definition in `getContentModel`.
109+
110+
**SDK side**: Add `ComponentResolver` that takes entry data and the model definition, iterates DZ values, reads `_templateId`, looks up the template to get `componentName`, resolves it in the `ComponentRegistry`, and renders the matched React component with the template's field values as props.
111+
112+
**Entry renderer**: Add `<EntryRenderer entry={entry} model={model} components={components} />` React component. It renders DZ field values as mapped components. Scalar fields are passed through as-is for the developer's layout to consume.
113+
114+
### Acceptance criteria
115+
116+
- [ ] `CmsDynamicZoneTemplate.componentName` is persisted and returned in model definition queries
117+
- [ ] SDK fetches model definition and builds the `_templateId``componentName` mapping automatically
118+
- [ ] `<EntryRenderer>` renders DZ template values as the mapped React components
119+
- [ ] Components receive the template's field values as props/inputs
120+
- [ ] Scalar fields (title, slug, etc.) are accessible on the entry object for manual rendering
121+
- [ ] `_templateId` is the only discriminator used — no `__typename` dependency
122+
- [ ] Nested DZ fields (DZ inside a DZ template) resolve correctly
123+
124+
---
125+
126+
## Phase 4: Live preview in iframe
127+
128+
**User stories**: 7, 8, 11, 19
129+
130+
### What to build
131+
132+
Real-time live preview: the CMS entry editor embeds the preview URL in an iframe and pushes form data on every change.
133+
134+
**Admin side**: The entry editor detects that the model has a `previewUrl` and embeds it in a preview pane (side-by-side layout, similar to the existing `extensions/livePreview/` pattern). On every form field change, the editor sends the full entry data to the iframe via `cms.preview.entry.update` postMessage.
135+
136+
**SDK side**: The `EditingSdk` variant listens for `cms.preview.entry.update` messages and updates its internal entry state. The `<EntryRenderer>` reactively re-renders when the entry data changes. For models without DZ fields, the developer's layout still receives updated scalar field values.
137+
138+
**Data flow**: Editor form data → postMessage → SDK EditingSdk → reactive state update → `<EntryRenderer>` re-render. Full entry sent on every change (not patches).
139+
140+
### Acceptance criteria
141+
142+
- [ ] CMS entry editor embeds the preview URL iframe when the model has a `previewUrl`
143+
- [ ] Every form field change sends the full entry data to the iframe via postMessage
144+
- [ ] The SDK receives live updates and re-renders the entry in real time
145+
- [ ] DZ template components update live as the user edits template field values
146+
- [ ] Scalar field changes (title, slug) propagate to the preview immediately
147+
- [ ] Models without DZ fields still get live preview of scalar fields
148+
- [ ] The preview pane shows a side-by-side layout (editor + preview)
149+
150+
---
151+
152+
## Phase 5: Component input definitions in template editor
153+
154+
**User story**: 15
155+
156+
### What to build
157+
158+
When a component is mapped to a template, the template editor displays the component's input definitions so the content modeler understands what the component expects.
159+
160+
**Admin side**: After the user selects a component in the template editor's component picker, the dialog shows a read-only summary of the component's declared inputs — name, label, description, type. This helps the modeler understand whether the template's fields align with what the component needs.
161+
162+
### Acceptance criteria
163+
164+
- [ ] Template editor shows the mapped component's input definitions (name, label, description) after selection
165+
- [ ] Input definitions are sourced from the component manifest received via the bridge
166+
- [ ] The display is read-only and informational — no editing of component inputs from the CMS side
167+
168+
---
169+
170+
## Phase 6: Reference field hooks
171+
172+
**User story**: 18
173+
174+
### What to build
175+
176+
Explicit SDK utilities for developers to fetch referenced entries where auto-fetching doesn't cover them.
177+
178+
**SDK side**: Provide hooks or methods (e.g., `useEntryRef(refValue)` or `sdk.resolveRef(refValue)`) that take a ref field value (`{ entryId, modelId }`) and fetch the referenced entry using the SDK's data provider. This gives developers control over when and how references are resolved, without the SDK attempting recursive auto-fetching.
179+
180+
### Acceptance criteria
181+
182+
- [ ] SDK provides a method to resolve a ref field value to a full entry
183+
- [ ] The method uses the same SDK config (preview mode, auth) as the main entry fetching
184+
- [ ] Developers can call the ref resolver explicitly in their component code
185+
- [ ] The ref resolver works in all three SDK modes (live, preview, editing)

0 commit comments

Comments
 (0)