Skip to content

Live Preview Architecture

The CMS live preview lets editors see changes in real-time as they edit content. It follows the same architecture as Contentful’s live preview in Alokai storefronts.

Overview

┌─────────────────────┐ postMessage ┌─────────────────────────┐
│ CMS Editor │ ──────────────────────▶ │ Storefront (iframe) │
│ │ alokai-cms:preview │ │
│ - Edit fields │ { page: { ... } } │ LivePreviewWrapper │
│ - Component tree │ │ ├─ receives data │
│ - Styles │ │ ├─ debounce 300ms │
│ │ │ ├─ calls rerender() │
│ │ │ │ (server action) │
│ │ │ └─ replaces SSR │
└─────────────────────┘ └─────────────────────────┘

How it works

1. CMS sends postMessage

When the editor changes any field, the CMS serializes the full component tree (with resolved styles) and sends it to the storefront iframe:

iframe.contentWindow.postMessage({
type: "alokai-cms:preview",
page: {
componentsAboveFold: [
{ component: "Hero", id: "abc", title: "Updated Title", styles: "...", uniqueClass: "hero-abc" }
],
componentsBelowFold: [...]
}
}, "*");

This happens on every keystroke — not debounced on the CMS side.

2. LivePreviewWrapper receives and debounces

The LivePreviewWrapper is a client component that:

  • Listens for alokai-cms:preview messages via window.addEventListener("message")
  • Debounces for 300ms (prevents overloading the server with rapid edits)
  • Calls the rerender server action with the page data
const debouncedRerender = debounce(async (pageData) => {
const content = await rerender(pageData);
setPreviewContent(content);
}, 300);

3. Server action renders with real components

The rerender function is a Next.js server action defined in connect-cms-page.tsx:

async function rerender(pageData: Record<string, unknown>) {
'use server';
const Component = componentsByPath[path];
return <Component {...props} page={pageData} />;
}

This runs on the server, renders the page component with the updated data using all the real storefront components (Hero, Banner, etc.), and returns the rendered JSX to the client.

4. SSR content is hidden

When the first live update arrives, the LivePreviewWrapper hides the original server-rendered content:

useEffect(() => {
const ssrContent = document.getElementById('ssr-content');
if (previewContent && ssrContent) {
ssrContent.style.display = 'none';
}
}, [previewContent]);

The live-rendered content replaces it visually.

Message types

TypeDirectionWhenPayload
alokai-cms:previewCMS → StorefrontEvery edit{ page: { componentsAboveFold: [...], ... } }
alokai-cms:savedCMS → StorefrontAfter Save(none)
alokai-cms:localeCMS → StorefrontLocale switch{ locale: "en-US", cookieName: "..." }

Preview mode detection

The storefront detects preview mode via the ?alokai_cms_preview=true query parameter:

const searchParams = await props.searchParams;
const isPreview = searchParams?.alokai_cms_preview === 'true';

When isPreview is true:

  • The SDK uses the previewToken instead of deliveryToken
  • Draft/unpublished content is returned
  • LivePreviewWrapper is rendered alongside SSR content

Layout preview

Layout editing (header/footer) uses a separate LayoutPreviewWrapper that renders layout zones client-side with simplified preview components. This is because layout components (Navigation, Footer) are simpler and can be approximated client-side.

The layout preview page at /[locale]/layout-preview is a minimal page that only renders LayoutPreviewWrapper.

Comparison with Contentful

AspectContentfulAlokai CMS
Communication@contentful/live-preview SDK + postMessageDirect postMessage
Data patchingClient-side field merging via useContentfulLiveUpdatesFull page data sent each time
Re-renderingServer action (rerender)Server action (rerender)
Debounce300ms300ms
SSR hiding#ssr-content hidden#ssr-content hidden
ComponentsReal storefront components via server actionReal storefront components via server action

The core pattern is identical — the only difference is how the data arrives (Contentful’s SDK vs direct postMessage).

File structure

sf-modules/cms-alokai-cms/
├── components/
│ ├── connect-cms-page.tsx # Connects CMS to pages, defines rerender server action
│ ├── live-preview-wrapper.tsx # Client component: receives postMessage, calls rerender
│ └── render-cms-content.tsx # Maps CMS component types to React components