Step 9 of 10 (90% complete)
Preview Mode — 5-Minute Setup
Step Code
The code for this specific step can be found on the following branch:
Setting up preview mode has historically been one of the more painful parts of integrating with a headless CMS. With @optimizely/cms-sdk, getting in-context editing working is genuinely a five-minute job.
How It Works
The SDK provides a getPreviewContent() method on GraphClient. This method takes the preview parameters that Optimizely sends to your preview URL and fetches the unpublished draft content. You do not need to write any GraphQL query, handle auth tokens, or parse the preview payload — the SDK handles all of it.
The only things you need to do are:
- Create a minimal layout at
app/preview/layout.tsx - Create a page at
app/preview/page.tsxthat callsgetPreviewContentand renders the result - Configure the preview URL in your Optimizely CMS admin panel
The Preview Layout
// app/preview/layout.tsx export default function PreviewLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <body> <Suspense>{children}</Suspense> </body> </html> ) }
Intentionally minimal — no header, no footer, no fonts. The preview layout wraps content in Suspense because the page component is async.
The Preview Page
// app/preview/page.tsx import { GraphClient } from '@optimizely/cms-sdk' import { OptimizelyComponent } from '@optimizely/cms-sdk/react/server' import Script from 'next/script' export default async function PreviewPage({ searchParams }) { const client = new GraphClient(process.env.OPTIMIZELY_GRAPH_SINGLE_KEY!, { graphUrl: process.env.OPTIMIZELY_GRAPH_URL, }) const response = await client.getPreviewContent(await searchParams) if (!response) { return <div>No content found for the given parameters.</div> } return ( <> <Script src={`${process.env.OPTIMIZELY_CMS_HOST}/util/javascript/communicationinjector.js`} /> <OptimizelyComponent content={response} /> </> ) }
Two things to notice:
getPreviewContent(searchParams)— the SDK extracts everything it needs from the search params that Optimizely appends to the preview URL (content key, version, locale, etc.)communicationinjector.js— this script from your CMS instance is what enables the two-way communication between the CMS editor UI and your preview page. It is what makes the editor toolbar, property highlighting, and click-to-edit work
Configuring the Preview URL in CMS
Full documentation: https://docs.developers.optimizely.com/content-management-system/v1.0.0-CMS-SaaS/docs/live-preview-with-react
In your Optimizely CMS admin panel:
- Go to the Live Preview tab
- Select Use Preview Tokens
- Click Enabled under Preview URL format
- Optionally add or edit rows for specific content types — the system adds a default format automatically
- Set the preview URL:
- Local development:
http://localhost:3000/preview - Production:
https://your-domain.com/preview
- Local development:
- Click Save
You can configure different URLs per environment — local, staging, and production can all point to different hosts.
Optimizely appends its own query parameters to your preview URL when opening a draft (content key, version, locale, etc.). Your page reads those via searchParams and passes them directly to getPreviewContent() — no manual parsing needed.
In-Context Editing with data-epi-edit
For editors to be able to click directly on text, images, and other fields on the preview page and edit them inline, add data-epi-edit attributes to your component JSX:
export default function HeroBlock({ content: { title, subtitle } }: Props) { return ( <section> <h1 data-epi-edit="title">{title}</h1> <p data-epi-edit="subtitle">{subtitle}</p> </section> ) }
The value of data-epi-edit must match the property key from your contentType() definition. When communicationinjector.js is loaded, it reads these attributes and makes those elements editable in the CMS preview.
Note: The preview route is excluded from locale middleware routing. The shouldExclude() function in proxy.ts checks for /preview in the path and skips locale redirection — this ensures Optimizely's preview parameters are passed through unchanged.
Have questions? I'm here to help!