Sets up and troubleshoots live preview for Optimizely CMS in React applications, including framework detection and route creation.
How this skill is triggered — by the user, by Claude, or both
Slash command
/optimizely-cms-skills:optimizely-previewThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
This skill helps you set up live preview for Optimizely CMS in React applications or troubleshoot existing preview configurations.
This skill helps you set up live preview for Optimizely CMS in React applications or troubleshoot existing preview configurations.
Live preview allows editors to see content changes in real-time before publishing. When properly configured, editors can click "Preview" in the Optimizely CMS and see their changes instantly in your application without leaving the editor interface.
Before setting up preview, detect which React framework the user is using:
Check for Next.js:
next.config.js or next.config.mjs in the project rootsrc/app/ or app/ directory existssrc/pages/ or pages/ directory existsCheck for TanStack Start:
@tanstack/react-start dependencysrc/routes/ or app/routes/If unclear: Ask the user which framework they're using
Create the preview route in the correct location based on the detected framework.
Create src/app/preview/page.tsx (or app/preview/page.tsx if no src directory):
import { GraphClient, type PreviewParams } from '@optimizely/cms-sdk';
import {
OptimizelyComponent,
withAppContext,
} from '@optimizely/cms-sdk/react/server';
import { PreviewComponent } from '@optimizely/cms-sdk/react/client';
import Script from 'next/script';
type Props = {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
};
export async function Page({ searchParams }: Props) {
const client = new GraphClient(process.env.OPTIMIZELY_GRAPH_SINGLE_KEY!, {
graphUrl: process.env.OPTIMIZELY_GRAPH_GATEWAY,
});
const response = await client.getPreviewContent(
(await searchParams) as PreviewParams,
);
return (
<>
<Script
src={`${process.env.OPTIMIZELY_CMS_URL}/util/javascript/communicationinjector.js`}
></Script>
<PreviewComponent />
<OptimizelyComponent content={response} />
</>
);
}
export default withAppContext(Page);
Key components explained:
withAppContext(Page): Required HOC that initializes request-scoped context for preview datagetPreviewContent(): Fetches the correct content version based on CMS preview parameters<Script>: Loads the communication injector from CMS for two-way communication<PreviewComponent />: Client component that handles real-time preview updates<OptimizelyComponent />: Renders the content using registered componentsCreate src/pages/preview.tsx (or pages/preview.tsx):
import { GraphClient, type PreviewParams } from '@optimizely/cms-sdk';
import {
OptimizelyComponent,
withAppContext,
} from '@optimizely/cms-sdk/react/server';
import { PreviewComponent } from '@optimizely/cms-sdk/react/client';
import Script from 'next/script';
import { GetServerSideProps } from 'next';
type Props = {
content: any;
};
export const getServerSideProps: GetServerSideProps = async (context) => {
const client = new GraphClient(process.env.OPTIMIZELY_GRAPH_SINGLE_KEY!, {
graphUrl: process.env.OPTIMIZELY_GRAPH_GATEWAY,
});
const response = await client.getPreviewContent(
context.query as PreviewParams,
);
return {
props: {
content: response,
},
};
};
function PreviewPage({ content }: Props) {
return (
<>
<Script
src={`${process.env.OPTIMIZELY_CMS_URL}/util/javascript/communicationinjector.js`}
></Script>
<PreviewComponent />
<OptimizelyComponent content={content} />
</>
);
}
export default withAppContext(PreviewPage);
Create src/routes/preview.tsx:
import { createFileRoute } from "@tanstack/react-router";
import { type PreviewParams } from "@optimizely/cms-sdk";
import { OptimizelyComponent } from "@optimizely/cms-sdk/react/server";
import { PreviewComponent } from "@optimizely/cms-sdk/react/client";
import { withAppContext } from "@optimizely/cms-sdk/react/server";
import { createServerFn } from "@tanstack/react-start";
import { renderServerComponent } from "@tanstack/react-start/rsc";
import { GraphClient } from "@optimizely/cms-sdk";
type Props = {
search: PreviewParams & {
ver: number;
};
};
const convertToStrings = (it: PreviewParams & {
ver: number;
}): PreviewParams => ({
...it,
ver: String(it.ver)
})
async function Page({ search }: Props) {
const client = new GraphClient(process.env.OPTIMIZELY_GRAPH_SINGLE_KEY!, {
graphUrl: process.env.OPTIMIZELY_GRAPH_GATEWAY,
});
const stringOnlySearch = convertToStrings(search)
const content = await client.getPreviewContent(stringOnlySearch);
return (
<>
<script
src={`${process.env.OPTIMIZELY_CMS_URL}/util/javascript/communicationinjector.js`}
/>
<PreviewComponent />
<OptimizelyComponent content={content} />
</>
);
}
const PageWithContext = withAppContext(Page);
const getPreviewPage = createServerFn().handler(
async ({ data: { search } }: any) => {
const Renderable = await renderServerComponent(
<PageWithContext search={search} />,
);
return { Renderable };
},
);
export const Route = createFileRoute("/preview")({
loader: async ({ location: { search } }) => {
const { Renderable } = await getPreviewPage({
data: { search },
} as any);
return { Renderable };
},
component: Preview,
});
function Preview() {
const { Renderable } = Route.useLoaderData();
return <>{Renderable}</>;
}
Key differences for TanStack Start:
createFileRoute from @tanstack/react-routercreateServerFn and renderServerComponent from @tanstack/react-startver parameter comes as a number and needs to be converted to a stringFor other React frameworks (Remix, Vite + React Router, etc.):
/preview route using your framework's routing systemclient.getPreviewContent()<script> or <Script>)<PreviewComponent /> and <OptimizelyComponent />withAppContextThe core concepts remain the same across all frameworks - only the routing and data-fetching mechanisms differ.
Check if .env file exists. If not, create it. Add or verify these variables:
OPTIMIZELY_GRAPH_SINGLE_KEY=your_single_key_here
OPTIMIZELY_GRAPH_GATEWAY=https://cg.optimizely.com/content/v2
OPTIMIZELY_CMS_URL=https://your-cms-instance.optimizely.com
Important notes:
OPTIMIZELY_CMS_URL should NOT have a trailing slashUpdate .gitignore to ensure .env is not committed:
.env
.env.local
Test the preview route locally:
Start the dev server (if not running):
npm run dev or pnpm dev or yarn devnpm run dev or similarTest the route:
http://localhost:3000/preview (or whatever port the dev server uses)Common issues at this stage:
@optimizely/cms-sdk is installedThe user needs to configure these settings in their Optimizely CMS instance. Provide clear instructions:
http://localhost:3000https://yourdomain.comhttp://localhost:3000/previewhttps://yourdomain.com/previewThe preview URL format in CMS should look like:
http://localhost:3000/preview
The CMS will automatically append the preview parameters (preview_token, key, locale, etc.) as query parameters.
Some browsers and CMS instances may require HTTPS even for local development. If preview isn't working over HTTP:
For Next.js:
Update your dev script in package.json:
"dev": "next dev --experimental-https"
Then use https://localhost:3000/preview in your CMS preview URL configuration.
For other frameworks:
vite dev --https or configure HTTPS in vite.config.tsWhen preview isn't working, systematically check common issues: blank screens, missing preview button, preview not updating, 404 errors, and environment variable issues. See references/troubleshooting.md for comprehensive diagnostic steps and solutions.
After basic preview works, enhance the editor experience by adding click-to-edit functionality using getPreviewUtils(). See references/click-to-edit.md for detailed guidance on preview attributes, helper functions, and best practices.
withAppContext - This is required, not optionalFor detailed information, consult:
references/troubleshooting.md - Comprehensive troubleshooting guide for preview issues including blank screens, missing buttons, update problems, and environment variable issuesreferences/click-to-edit.md - Click-to-edit features and preview utilities including pa(), src(), best practices, and common mistakesnpx claudepluginhub episerver/content-js-sdk --plugin optimizely-cms-skillsGenerates React components for Optimizely CMS content types and display templates, mapping properties to React props and following SDK patterns.
Adds and configures Contentful in an existing Next.js project. Covers SDK install, env vars, production/preview clients, content fetching in App Router or Pages Router, and Draft Mode preview flows.
Sets up Nuxt Studio module for visual content editing and CMS in Nuxt 3+ sites with @nuxt/content, covering installation, OAuth authentication (GitHub/GitLab/Google), Cloudflare subdomain deployment, and Monaco/TipTap editor config.