Content Preview
Content Preview lets you see how a draft page will look on your actual storefront — with live updates as you edit in the CMS.
How it works
- The CMS opens your storefront in an iframe with
?alokai_cms_preview=true - The storefront detects preview mode and fetches the latest draft using the preview token
- As you edit in the CMS, changes are sent to the iframe via
postMessage - The storefront re-renders the page with the updated data using a server action (debounced 300ms)
- You see the real rendered page update live — no manual save or refresh needed
Tokens
Preview mode uses a token with the preview permission. The default Content Preview token is auto-created with every space.
ALOKAI_CMS_DELIVERY_TOKEN=myorg_tok_abc123... # token with delivery permissionALOKAI_CMS_PREVIEW_TOKEN=myorg_tok_def456... # token with preview permissionThe storefront SDK uses the delivery token by default. When ?alokai_cms_preview=true is in the URL, it switches to the preview token and fetches draft content.
Setting up preview
1. Configure a preview URL in the CMS
Go to Settings → Preview URLs and click New Preview URL:
- Name — e.g. “Local Development” or “Staging”
- URL — Your storefront URL, e.g.
http://localhost:3013orhttps://staging.example.com - Default — Check if this is the primary preview target
2. Set up the storefront integration
The storefront needs three components in sf-modules/cms-alokai-cms/:
connect-cms-page.tsx — connects CMS data to page components
import type { ComponentType } from 'react';import { getSdk } from '@/sdk';import LivePreviewWrapper from '@/sf-modules/cms-alokai-cms/components/live-preview-wrapper';
const componentsByPath: Record<string, ComponentType<any>> = {};
const appLocaleToCmsLocale: Record<string, string> = { de: 'de-DE', en: 'en-US',};
export default function connectCmsPage<TProps>( PageComponent: ComponentType<TProps>, config: { getCmsPagePath: (props: TProps) => Promise<string> | string },) { async function CmsPage(props: any) { const sdk = await getSdk(); const path = await config.getCmsPagePath(props); const params = await props.params; const locale = appLocaleToCmsLocale[params.locale]; const searchParams = await props.searchParams; const isPreview = searchParams?.alokai_cms_preview === 'true';
componentsByPath[path] = PageComponent;
const page = await sdk.unifiedAlokaiCms .getPage({ locale, path, ...(isPreview && { preview: true }) }) .catch(() => null);
// Server action: re-renders the page with live data from postMessage async function rerender(pageData: Record<string, unknown>) { 'use server'; const Component = componentsByPath[path]; return <Component {...props} page={pageData} />; }
if (isPreview) { return ( <> <div id="ssr-content"> <PageComponent {...props} page={page} /> </div> <LivePreviewWrapper rerender={rerender} /> </> ); }
return <PageComponent {...props} page={page} />; }
return CmsPage;}live-preview-wrapper.tsx — receives live updates and triggers server re-render
'use client';
import { debounce } from 'lodash-es';import type { ReactNode } from 'react';import { useCallback, useEffect, useState } from 'react';
export default function LivePreviewWrapper({ rerender,}: { rerender: (pageData: Record<string, unknown>) => Promise<ReactNode>;}) { const [previewContent, setPreviewContent] = useState<ReactNode>(null);
const debouncedRerender = useCallback( debounce(async (pageData: Record<string, unknown>) => { setPreviewContent(await rerender(pageData)); }, 300), [rerender], );
useEffect(() => { function handleMessage(event: MessageEvent) { if (event.data?.type === 'alokai-cms:preview') { const page = event.data.page; if (page) debouncedRerender(page); } } window.addEventListener('message', handleMessage); return () => window.removeEventListener('message', handleMessage); }, [debouncedRerender]);
useEffect(() => { const ssrContent = document.getElementById('ssr-content'); if (previewContent && ssrContent) { ssrContent.style.display = 'none'; } }, [previewContent]);
return <>{previewContent}</>;}3. Use in a page
// app/[locale]/(cms)/[[...slug]]/page.tsximport connectCmsPage from '@/sf-modules/cms-alokai-cms/components/connect-cms-page';import RenderCmsContent from '@/sf-modules/cms-alokai-cms/components/render-cms-content';
export default connectCmsPage( ({ page }) => { if (!page) return notFound(); return ( <> <RenderCmsContent item={page.componentsAboveFold} /> <RenderCmsContent item={page.componentsBelowFold} /> </> ); }, { async getCmsPagePath(props) { const params = await props.params; return `/${params.slug?.join('/') ?? ''}`; }, },);Using preview in the editor
Click the Preview button in the page editor toolbar. The editor panel collapses and the page renders in a full-width iframe. As you edit fields, the preview updates live.
How live preview works (architecture)
The live preview follows the same pattern as Contentful’s live preview in Alokai:
- CMS editor sends
alokai-cms:previewpostMessage on every edit with the full component tree LivePreviewWrapper(client component) receives the message, debounces 300ms- Calls
rerender(pageData)— a Next.js server action defined inconnect-cms-page - The server action renders
<PageComponent page={pageData} />on the server with the real storefront components - The rendered JSX is returned to the client and displayed, hiding the original SSR content
This ensures live preview uses the actual storefront components (not simplified placeholders), supports server-only features, and doesn’t require a page refresh.
Preview outside the iframe
Visit any page with ?alokai_cms_preview=true appended:
https://your-storefront.com/some-page?alokai_cms_preview=trueThis fetches draft content using the preview token. Live postMessage updates won’t work outside the CMS iframe, but you’ll see the latest saved draft.
postMessage reference
| Message type | When sent | Data |
|---|---|---|
alokai-cms:preview | Every edit in the CMS | { page: { componentsAboveFold: [...], ... } } |
alokai-cms:saved | After clicking Save | (no data) |
alokai-cms:locale | Locale switch in editor | { locale: "en-US", cookieName: "..." } |