Skip to content

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

  1. The CMS opens your storefront in an iframe with ?alokai_cms_preview=true
  2. The storefront detects preview mode and fetches the latest draft using the preview token
  3. As you edit in the CMS, changes are sent to the iframe via postMessage
  4. The storefront re-renders the page with the updated data using a server action (debounced 300ms)
  5. 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.

Terminal window
ALOKAI_CMS_DELIVERY_TOKEN=myorg_tok_abc123... # token with delivery permission
ALOKAI_CMS_PREVIEW_TOKEN=myorg_tok_def456... # token with preview permission

The 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:3013 or https://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.tsx
import 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:

  1. CMS editor sends alokai-cms:preview postMessage on every edit with the full component tree
  2. LivePreviewWrapper (client component) receives the message, debounces 300ms
  3. Calls rerender(pageData) — a Next.js server action defined in connect-cms-page
  4. The server action renders <PageComponent page={pageData} /> on the server with the real storefront components
  5. 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=true

This 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 typeWhen sentData
alokai-cms:previewEvery edit in the CMS{ page: { componentsAboveFold: [...], ... } }
alokai-cms:savedAfter clicking Save(no data)
alokai-cms:localeLocale switch in editor{ locale: "en-US", cookieName: "..." }