feat(exo): preview SDK split into three-layer workspace packages [SPA-4692]#28
Draft
Thomas Kellermeier (Chaoste) wants to merge 5 commits into
Draft
feat(exo): preview SDK split into three-layer workspace packages [SPA-4692]#28Thomas Kellermeier (Chaoste) wants to merge 5 commits into
Thomas Kellermeier (Chaoste) wants to merge 5 commits into
Conversation
Adds the postMessage-based editor↔preview protocol as an internal module of experiences-core, plus an opt-in usePreviewOverride hook and matching enablePreview / previewCapabilities / previewTargetOrigin props on ClientExperienceRenderer. Ships as part of the existing packages — no separate npm artefact. The flag is a no-op in production because origin resolution is gated on ancestorOrigins matching a Contentful editor allow-list, so leaving it on outside the editor costs nothing. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Swap the protocol payload from the renderer's internal PortableRenderPlan back to the API-generic ExperiencePayload shape (aliased as HydratedView). Third-party renderers, native adapters, and test harnesses can now consume the wire without depending on any renderer's internal IR. ClientExperienceRenderer converts at the boundary: an incoming HydratedView is resolved into a PortableRenderPlan via the same resolveExperience path customers already use for fetched payloads. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Introduces the three-layer split for the preview SDK, plus adapters for
React and Svelte, so the "framework-specific code is thin" claim is
observable in the file tree:
- @contentful/experiences-preview-core protocol + channel interface +
PreviewClient state machine
(framework-agnostic, DOM-agnostic)
- @contentful/experiences-preview-web postMessage transport
(DOM-dependent; future
WebSocket/WebRTC transports
land here)
- @contentful/experiences-preview-react usePreviewOverride +
useResolvedPreviewPlan hooks
- @contentful/experiences-preview-svelte createPreviewOverride +
createResolvedPreviewPlan runes
Customers still install only @contentful/experiences-react (or
-svelte); the preview packages are transitive direct dependencies.
enablePreview prop stays; internal wiring now imports from the sibling
preview-adapter-* package instead of from local files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
React and Svelte `ClientExperienceRenderer` no longer throw when rendered on the server. Server-side render emits the same output as `ServerExperienceRenderer` (viewport seeded from `initialViewportId`, matchMedia listeners are gated on `typeof window`); client-side render takes over after hydration. For React, `enablePreview` is additionally gated on an `isHydrated` flag sourced via `useSyncExternalStore` with a `false` server snapshot, so the preview override only wakes up after the tree has hydrated — matching what would happen with a `'use client'` boundary but without the extra file. For Svelte, `$effect` already runs client-only, so removing the throw is sufficient. Consumers can now render `ClientExperienceRenderer` directly from a server component / server load and get SSR HTML for first paint plus editor-driven live updates once hydrated. No client-boundary shell required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ents Replaces the "universal ClientExperienceRenderer" from the previous commit with three focused renderers, one per intent: - `ServerExperienceRenderer` — RSC-only. Static HTML, zero client-JS. Pick for marketing / blog / landing pages with no interactivity. - `ClientExperienceRenderer` — client-only; throws on the server. Pick for tests, native shells, or apps that never SSR. - `ExperienceRenderer` — hybrid. SSR first paint + client hydration + `enablePreview`. Pick for any editable / preview-capable route. The hybrid is a two-line handoff: `useSyncExternalStore` (React) / `$state + $effect` (Svelte) returns `false` on the server and on the first pre-hydration client render, `true` afterwards. Before hydration it delegates to `ServerExperienceRenderer` (byte-identical HTML on both sides); after hydration it renders `ClientExperienceRenderer`, which owns the preview wire. Customers pick by import — no wrapper component, no `'use client'` boundary file, no `enablePreview && isHydrated` gymnastics. The component name at the call site tells the reader what the route does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
6 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Comparison branch for the ExO optimization PR (#26). Same monorepo, different strategy: the preview SDK ships as workspace packages that are direct dependencies of the renderer adapters — customers install nothing extra.
Companion PR #29 stacks on top and shows the customer-side integration diff — one import.
Summary
shared/experiences/packages/; the three-layer split (core / web / adapter) is visible in the file tree.@contentful/experiences-react(or-svelte); the preview packages arrive transitively.enablePreviewprop onExperienceRenderertoggles the wire; no-op when the app isn't inside a Contentful editor.'use client'boilerplate needed at the call site:ServerExperienceRenderer— RSC-only, static HTML, zero client-JS.ClientExperienceRenderer— client-only; throws on the server. For tests / native shells / non-SSR apps.ExperienceRenderer— hybrid; SSR first paint + client hydration +enablePreview. Pick for editable / preview-capable routes.Changes
@contentful/experiences-preview-core— protocol + channel interface +PreviewClientstate machine (framework-agnostic, DOM-agnostic).@contentful/experiences-preview-web— postMessage transport (DOM-dependent; future WebSocket/WebRTC land here).@contentful/experiences-preview-react—usePreviewOverride+useResolvedPreviewPlanhooks.@contentful/experiences-preview-svelte—createPreviewOverride+createResolvedPreviewPlanrunes.ExperienceRenderer— hybrid renderer in both@contentful/experiences-reactand@contentful/experiences-svelte. Delegates toServerExperienceRendererbefore hydration andClientExperienceRendererafter.useSyncExternalStore(React) /$state + $effect(Svelte) drives the two-phase handoff without hydration mismatches.experiences-reactandexperiences-svelteeach add a single-line dep on their matching preview adapter package and gainenablePreview/previewCapabilities/previewTargetOriginprops onExperienceRendererandClientExperienceRenderer.ClientExperienceRenderercontinues to throw on the server; documentation now points atExperienceRendereras the hybrid path.HydratedView(alias for the API-genericExperiencePayload), not the renderer-internalPortableRenderPlan— third-party renderers and native adapters can consume the wire without depending on any renderer's IR.@contentful/experiences-core.