Why headless delivery with Graph
Section titled “Why headless delivery with Graph”Traditional CMS architecture couples content management to content rendering. The CMS stores the content and also generates the HTML. This works well until you need to serve the same content to a mobile app, a kiosk, a partner site, or a single-page application — each requiring different rendering logic.
Optimizely Graph decouples these layers. The CMS remains the system of record for content. Graph indexes that content and exposes it through a GraphQL API. Your frontend — a Next.js app, a React SPA, a mobile client — queries Graph for exactly the data it needs and renders it using its own component library. The CMS and the frontend evolve independently.
This recipe walks through the complete architecture: how content flows from the CMS editor to Graph to a Next.js frontend, with query patterns you can adapt to your content model.
Architecture overview
Section titled “Architecture overview”┌─────────────────────────────────────────────────────────┐│ Content Authors ││ (CMS Editor / Visual Builder) │└────────────────────────┬────────────────────────────────┘ │ Create, edit, publish ▼┌─────────────────────────────────────────────────────────┐│ Optimizely CMS ││ (SaaS or PaaS instance) ││ ││ Content types → Content instances → Publishing events │└────────────────────────┬────────────────────────────────┘ │ Automatic sync on publish ▼┌─────────────────────────────────────────────────────────┐│ Optimizely Graph ││ ││ • Indexes all published content ││ • Generates GraphQL schema from content types ││ • Supports filtering, sorting, full-text search ││ • Handles localization and personalization queries ││ • Provides real-time and cached endpoints │└────────────────────────┬────────────────────────────────┘ │ GraphQL queries ▼┌─────────────────────────────────────────────────────────┐│ Frontend Application ││ (Next.js / React / Vue) ││ ││ • Queries Graph at build time (SSG) or request time ││ (SSR/ISR) ││ • Maps content types to React components ││ • Handles routing, layout, and interactivity ││ • Deploys independently (Vercel, Netlify, AWS, etc.) │└─────────────────────────────────────────────────────────┘Data flow: editor to browser
Section titled “Data flow: editor to browser”- Author publishes content in the CMS editor (a page, a component, a media item)
- CMS sends a publish event to Graph. On CMS SaaS, this is automatic. On CMS PaaS, Graph syncs via the configured integration.
- Graph indexes the content and updates its GraphQL schema if new content types or properties were added
- Frontend queries Graph using GraphQL — either at build time (static generation), at request time (server-side rendering), or with incremental static regeneration (ISR) for a balance of performance and freshness
- Frontend renders the response using React components that map to CMS content types
Step 1: Configure Graph integration
Section titled “Step 1: Configure Graph integration”Retrieve your Graph credentials from the Optimizely dashboard:
- Graph endpoint — The GraphQL API URL for your environment
- Single key — For public, cached queries (suitable for frontend use)
- HMAC key — For authenticated queries with draft content access (use server-side only)
Step 2: Set up the Next.js project
Section titled “Step 2: Set up the Next.js project”# Create a new Next.js application
npx create-next-app@latest my-opti-site --typescript --app
cd my-opti-site
# Install a GraphQL client
npm install graphql-request graphql import { GraphQLClient } from 'graphql-request';
const GRAPH_ENDPOINT = process.env.OPTIMIZELY_GRAPH_ENDPOINT!;
const GRAPH_KEY = process.env.OPTIMIZELY_GRAPH_SINGLE_KEY!;
export const graphClient = new GraphQLClient(GRAPH_ENDPOINT, {
headers: {
Authorization: `Bearer ${GRAPH_KEY}`,
},
}); Store your Graph credentials in .env.local:
OPTIMIZELY_GRAPH_ENDPOINT=https://cg.optimizely.com/content/v2
OPTIMIZELY_GRAPH_SINGLE_KEY=your-single-key-here Step 3: Query content from Graph
Section titled “Step 3: Query content from Graph”Graph generates a GraphQL schema from your CMS content types. Each content type becomes a queryable type in the schema.
import { gql } from 'graphql-request';
export const GET_LANDING_PAGE = gql`
query GetLandingPage($url: String!) {
LandingPage(
where: {
_metadata: { url: { default: { eq: $url } } }
_metadata: { status: { eq: "Published" } }
}
) {
items {
_metadata {
key
displayName
url {
default
}
}
metaTitle
metaDescription
mainContent {
__typename
... on HeroBlock {
heading
bodyText
backgroundImage {
url
}
ctaText
ctaLink
}
... on TextBlock {
heading
body
}
... on CardBlock {
title
description
image {
url
}
linkUrl
}
}
}
}
}
`;
export const GET_ALL_PAGES = gql`
query GetAllPages {
LandingPage(where: { _metadata: { status: { eq: "Published" } } }) {
items {
_metadata {
url {
default
}
}
}
}
}
`; Step 4: Map content types to React components
Section titled “Step 4: Map content types to React components”Create a component registry that maps CMS content type names to React components.
import { HeroBanner } from './HeroBanner';
import { TextSection } from './TextSection';
import { ContentCard } from './ContentCard';
// Map CMS content type names to React components
const componentMap: Record<string, React.ComponentType<any>> = {
HeroBlock: HeroBanner,
TextBlock: TextSection,
CardBlock: ContentCard,
};
interface ContentRendererProps {
items: Array<{ __typename: string; [key: string]: any }>;
}
export function ContentRenderer({ items }: ContentRendererProps) {
return (
<>
{items.map((item, index) => {
const Component = componentMap[item.__typename];
if (!Component) {
console.warn(`No component for type: ${item.__typename}`);
return null;
}
return <Component key={index} {...item} />;
})}
</>
);
} Step 5: Build the page route
Section titled “Step 5: Build the page route”import { graphClient } from '@/lib/graph-client';
import { GET_LANDING_PAGE, GET_ALL_PAGES } from '@/lib/queries';
import { ContentRenderer } from '@/components/content-renderer';
import { notFound } from 'next/navigation';
interface PageProps {
params: { slug: string[] };
}
export async function generateStaticParams() {
const data = await graphClient.request(GET_ALL_PAGES);
return data.LandingPage.items.map((page: any) => ({
slug: page._metadata.url.default.split('/').filter(Boolean),
}));
}
export default async function Page({ params }: PageProps) {
const url = '/' + params.slug.join('/');
const data = await graphClient.request(GET_LANDING_PAGE, { url });
const page = data.LandingPage?.items?.[0];
if (!page) {
notFound();
}
return (
<main>
<h1>{page._metadata.displayName}</h1>
<ContentRenderer items={page.mainContent || []} />
</main>
);
}
// Revalidate every 60 seconds (ISR)
export const revalidate = 60; Rendering strategy tradeoffs
Section titled “Rendering strategy tradeoffs”| Strategy | When to use | Graph query timing |
|---|---|---|
| Static Generation (SSG) | Marketing pages, blog posts — content changes infrequently | Build time only |
| Incremental Static Regeneration (ISR) | Content updated regularly but does not need instant reflection | Build time + background revalidation |
| Server-Side Rendering (SSR) | Personalized content, preview mode, content that must be instant | Every request |
For most content-driven sites, ISR provides the best balance. Pages load fast from cache, and Graph serves fresh content within the revalidation window.
Preview and draft content
Section titled “Preview and draft content”To let CMS authors preview unpublished content in the Next.js frontend, use the HMAC key and query for draft content.
import { GraphQLClient } from 'graphql-request';
// HMAC key allows access to draft content
// Use server-side only — never expose in client bundles
const GRAPH_HMAC_KEY = process.env.OPTIMIZELY_GRAPH_HMAC_KEY!;
export const previewClient = new GraphQLClient(
process.env.OPTIMIZELY_GRAPH_ENDPOINT!,
{
headers: {
Authorization: `Bearer ${GRAPH_HMAC_KEY}`,
},
}
); When to use this pattern
Section titled “When to use this pattern”This architecture fits when:
- You need to serve content to multiple channels (web, mobile, kiosks) from a single CMS
- Your frontend team works in React, Next.js, Vue, or another JavaScript framework rather than .NET
- You want to deploy the frontend independently of the CMS
- Performance is critical and you want edge caching or static generation
- You are on CMS SaaS (headless-first by design) or migrating CMS PaaS toward headless delivery