Virtual Happy Hour this month, Jun 28, we'll be getting a sneak preview at our soon to launch SaaS CMS!

Try our conversational search powered by Generative AI!

Szymon Uryga
Nov 27, 2023
(2 votes)

Optimizely Graph and Next.js: Building Scalable Headless Solutions

Optimizely Graph harnesses the capabilities of GraphQL, an intuitive and efficient query language to, transform content within an Optimizely CMS into a structured, interconnected graph. Complementing this, Next.js, a React-based frontend framework, empowers developers to build performant and SEO-friendly web applications, enabling static and dynamic rendering and many more amazing features that can enhance your headless solution.
In this article we delve into the symbiotic relationship between Optimizely Graph and Next.js, exploring how this pairing facilitates the crafting of dynamic headless experiences, from enabling draft modes and On-Page Editing to harnessing static site generation. We'll also investigate the utilization of webhooks for efficient page revalidation, enabling seamless updates across web applications.

What is Optimizely Graph used for?

Using Optimizely Graph we are able to separate our admin panel from our application, the so called headless, since we have two separate applications that are independent of each other. Headless in Optimizely has long been possible by using the Content Delivery API, but Graph simplifies a lot of things.
What is the Optimizely Graph used for?

How does Optimizely Graph work?

Optimizely Graph is a separate service that is hosted on a CDN, made fast by implementing the latest, most modern technologies using global serverless Edge computing, auto scaling microservices and the latest search engine.
There are two ways to deliver content to the Graph service:
  1. Scheduled job  
  2. Event Triggering, for example publishing a page.
The Content Delivery API is used under the hood, delivering content to Graph. When you want to introduce something custom, such as adding business logic to a block, you can do so using an override of the serialization used in the Content Delivery API.
In the image below we can see how the Optimizely Graph works. It all starts in the admin panel, in the picture labeled with the Optimizely logo. From there, using the Content Delivery API, the content goes to the GraphQl Server. GraphQl Server is hosted on Azure and is embedded on a CDN. The GraphQl Server combines the admin panel and frontend application used by users. The frontend application communicates directly with GraphQl Server, receiving all the necessary data.
How does Optimizely Graph work?

Support for Commerce

As of early December, Optimizely Graph has been officially released and is production ready. The released version supports CMS only and allows read-only options. At a recent webinar held by Optimizely, the company said that they are getting a lot of requests for Commerce support and it is in the approval stage by Project Management. Keeping in mind that commerce is much more complicated than CMS, it may take longer to work on this. In summary, currently no work is being done to implement support for commerce, and once this plan is approved, we will have to wait a while for it to be ready for use in production. However, using other Optimizely products using Content Delivery API, we are able to implement Headless Commerce. There is also an option to use Graph for CMS-related stuff, and Content Delivery API for commerce-related stuff.


Personalization as we know it so far, i.e. creating visitor groups and adding personalization in Content Area and XHTML strings, **will not work**.
Here's where other products can help, like Optimizely Web Experimentation, where you can create personalized campaigns and immediately measure which variance converts better. However, this is an additional product charged separately.
There is another option that is being worked on: the integration of Optimizely Graph with Optimizely Data Platform RLS (Real Time Segment), which is the next generation of “Visitor Groups”. ODP is also an additional product that is charged separately.

Content Recommendations

Content Recommendation works by scraping the page source of the site. if it is valid HTML, there should be no problem with it. If we use Server Side Rendering, there is pure HTML code in the page source, so with SSR, Content Recommendations works. The problem with Content Recs is that if you choose client-side rendering, it means that non-native HTML elements such as <teaser-block/> are included in HTML, so the scraper is not able to collect the correct data.

Hosting on DXP

It is possible to continue to have the frontend application hosted on DXP, then the client app (Node.js) and the backend (.net) are served on the same address by proxying requests from .net to the Node.js process. An example of such a solution can be found here
Hosting on DXP
Source: Github
Vercel is the recommended hosting platform for Next.js to fully leverage its capabilities, such as Incremental Static Regeneration (ISR) (example of code), image and font optimization. By hosting Next.js on Vercel, you can achieve better performance due to its seamless integration with Next.js and support for these advanced features.
Vercel's administration panel provides a user-friendly interface for managing deployments, making it accessible even to non-technical users. This allows for easy deployment to production after reviewing changes on staging. While there are inherent risks in allowing non-technical users to deploy changes, for small changes like increasing font size, the risks are minimal. The clear and intuitive interface of Vercel's administration panel makes it convenient for non-technical users to manage deployments with confidence.
An example of a starter for Optimizely Feature Flags with Next.js is available for preview and use here.

Understanding Next.js Rendering Strategies

Next.js employs three main server rendering strategies:
1. Static Rendering (Default): Routes are pre-rendered at build time or after data revalidation. The rendered result is cached and can be distributed via Content Delivery Networks (CDN). This strategy is suitable for static content like blog posts or product pages.
2. Dynamic Rendering: Routes are rendered for each user at request time. This method is ideal for personalized data or information specific to each user, such as cookies or search parameters.
3. Dynamic Routes with Cached Data: Next.js allows for routes with a mix of cached and uncached data. This flexibility means you can have personalized, dynamic content while benefiting from caching. For instance, an e-commerce page might use cached product data along with uncached, personalized customer information.
These server rendering strategies offer benefits like enhanced performance, improved security and better initial page load times. Leveraging Next.js's capabilities, developers can efficiently manage and render content, allowing for highly personalized and dynamic web experiences in your headless solution.

Fetching data from Optimizely Graph

A very useful thing when working with Optimizely Graph is codegen for GraphQL Schema - `@graphql-codegen/cli`. With this tool, all we need to do is create the appropriate snippets or queries for our Optimizely Graph, and the CLI will generate a fully-typed SDK for us.
Let’s configure our codegen. First, install following dependencies:
npm i -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-generic-sdk @graphql-codegen/typescript-operations
Now, we need to create a config file where we define plugins, schema source, destination file etc.
// codegen.yaml
documents: './src/graphql/**/*.graphql'
      - 'typescript'
      - 'typescript-operations'
      - 'typescript-generic-sdk'
      rawRequest: true
      avoidOptionals: true
This configuration will make sure that our SDK and types will be generated in the ./src/types/generated.ts based on the Optimizely Graph schema and fragments and queries defined in the ./src/graphql/*.
Now, let’s create our custom fetcher that we will use to query data from the Optimizely Graph.
interface OptimizelyFetchOptions {
  headers?: Record<string, string>
  cache?: RequestCache
  preview?: boolean
interface OptimizelyFetch<Variables> extends OptimizelyFetchOptions {
  query: string
  variables?: Variables
export const optimizelyFetch = async <Response, Variables = {}>({
  cache = 'force-cache',
}: OptimizelyFetch<Variables>): Promise<GraphqlResponse<Response> & { headers: Headers }> => {
  const configHeaders = headers ?? {}
  if (preview) {
    configHeaders.Authorization = `Basic ${process.env.OPTIMIZELY_PREVIEW_SECRET}`
  try {
    const endpoint = `${process.env.OPTIMIZELY_API_URL}?auth=${process.env.OPTIMIZELY_SINGLE_KEY}`
    const response = await fetch(endpoint, {
      method: 'POST',
      headers: {
        Accept: 'application/json',
        'Content-Type': 'application/json',
      body: JSON.stringify({
        ...(query && { query }),
        ...(variables && { variables }),
    const result = await response.json()
    return {
      headers: response.headers,
  } catch (e) {
    if (isVercelCommerceError(e)) {
      throw {
        status: e.status || 500,
        message: e.message,
    throw {
      error: e,
The `process.env.OPTIMIZELY_PREVIEW_SECRET` variable is a generated base64 string based on your AppKey and AppSecret credentials. For more details I recommend you to take a look at Kunal’s article
Let’s create a wrapper function around our `optimizelyFetch` that maps the values properly to the `getSdk` function which is an auto-generated SDK.
async function requester<R, V>(doc: DocumentNode, vars?: V, options?: OptimizelyFetchOptions) {
  const request = await optimizelyFetch<R>({
    query: print(doc),
    variables: vars ?? {},
  return {
    _headers: request.headers,
export const optimizely = getSdk(requester)
In the `./src/graphql` (path defined in the `documents` field in the codegen configuration file) create your first query:
// GetContentById.graphql
query GetContentById($id: Int, $workId: Int) {
  Content(where: { ContentLink: { Id: { eq: $id }, WorkId: { eq: $workId } } }) {
    items {
      ContentLink {
After properly configuring codegen and firing the command that generates the SDK, we get a fully-typed client.
fully typed client

Draft Mode with Optimizely Graph and Next.js

With Optimzely and Next.js we can easily configure previews, drafts and on-page editing to fully take advantage of the headless architecture. In order to let Optimizely know what is the URL to our Frontend, we need to follow a few simple steps.
Firstly, go to the `Manage Websites` section in the CMS settings. Go ahead and create a website. The important thing at this point will be to set the appropriate host names. You need to add one line for the backend application and one for the frontend application, remembering to set the Primary type for the frontend application:
Manage websites cms settings
At this point, Optimizely will know what URL to use for Preview Mode iframe and for the `View on website` button.
Now, let’s handle displaying correct route in the Preview Mode. By default, the generated URL adds `/episerver/cms/content/**/*` pathname to the configured host and passes it to the iframe. We can redirect all requests from that URL to our custom API handler that will turn on the draft mode for the corresponding page.
// next.config.js
async redirects() {
  return [
      source: '/episerver/cms/content/:path*',
      destination: '/api/draft/:path*',
      permanent: false,
Example: `EPiServer/CMS/Content/en/artists/almost-up-today,,35/` is redirected to `api/draft/en/artists/almost-up-today,,35/`
Create an API route that will handle the draft mode:
// api/draft/[[...content]]/route.ts
export async function GET(request: NextRequest, { params }: { params: { content: string[] } }) {
  const { searchParams } = new URL(request.url)
  const token = searchParams.get('preview_token')
  const payload = decodeToken(token)
  const contentId = payload?.contentId
  const contentWorkId = payload?.contentWorkId
  const { slugs } = getSlugs(params.content)
  if (!slugs || !contentId || !token) {
    return notFound()
  const newPathname = slugs.join('/')
  const { data, errors } = await optimizely.GetContentById(
    { id: contentId, workId: contentWorkId ?? null },
    { preview: true }
  if (errors) {
    const errorsMessage = => error.message).join(', ')
    return new Response(errorsMessage, { status: 401 })
  cookies().set(PREVIEW_TOKEN_COOKIE_NAME, token)
The `[[…content]]` segment is a naming convention that let’s you catch all route segments, which will be available in the params object argument.
Let’s handle the draft mode in the Page component:
export default async function HomePage() {
  const { isEnabled: isDraftModeEnabled } = draftMode()
  let pageResponse
  if (isDraftModeEnabled) {
    const preview_token = cookies().get(PREVIEW_TOKEN_COOKIE_NAME)
    if (!preview_token) {
      return notFound()
    const payload = decodeToken(preview_token.value)
    const contentId = payload?.contentId ?? null
    const workId = payload?.contentWorkId ?? null
    pageResponse = await optimizely.StartPageQuery(
      { contentId, workId },
      { preview: true, cache: 'no-store' }
  } else {
    pageResponse = await optimizely.StartPageQuery()
  const startPage =[0]
  if (!startPage) {
    return notFound()
  return (
    <div className="container mx-auto py-10">
      <div className="w-full px-4">
        <h1 className="text-3xl font-bold mb-10 max-w-prose" data-epi-edit="TeaserText">
      <ContentAreaMapper blocks={startPage.MainContentArea} />
Be careful here though - using cookies in a page component turns it to be dynamically rendered - unless it’s used in `if` block that checks for the draft mode.
The optimizely script to utilize On-Page Editing feature:
import Script from 'next/script'
import { draftMode } from 'next/headers'
export default function RootLayout({ children }: { children: React.ReactNode }) {
  const { isEnabled: isDraftModeEnabled } = draftMode()
return (
{isDraftModeEnabled && <Script src={`${process.env.OPTIMIZELY_BACKEND_URL}/episerver/cms/latest/clientresources/communicationinjector.js`} />}      
  <html lang="en">
        <body className={inter.className}>
Draft Mode


To enable on page editing (OPE) we need to add an attribute to the html that will enable this, you can read more about it in the documentation
<div className="container mx-auto py-10">
  <div className="w-full px-4">
    <h1 data-epi-edit="TeaserText" className="text-3xl font-bold mb-10 max-w-prose">
  <ContentAreaMapper blocks={startPage?.MainContentArea} />

Static Site Generation

Static Site Generation (SSG) is a powerful feature in Next.js that enables the pre-rendering of pages at build time. During the build process, Next.js generates HTML files for each page in your application. These HTML files are static, meaning they represent the state of the page at the time of the build. The pre-rendered HTML files can be served to users without requiring server-side rendering for every request.
Benefits of Static Site Generation in Next.js:
1. Improved Performance: Since the pages are pre-rendered at build time, users receive static HTML files, reducing the need for server-side processing during runtime. This leads to faster page loads and improved overall performance.
3. SEO Optimization: Search engines favor static HTML content. SSG helps improve search engine optimization (SEO) by providing crawlers with easily indexable and readable content. This can positively impact a website's search engine ranking.
4. Lower Server Load: Static pages can be served directly from a content delivery network (CDN) without the need for server-side processing. This reduces the load on the server, allowing it to handle more concurrent users efficiently.
5. Cost Efficiency: Serving static content is typically more cost-effective than dynamically generating content for each user request. With SSG, you can generate pages during the build process, reducing the need for server resources during runtime.
6. Better User Experience: Static pages load quickly, providing a smoother and more responsive user experience. Users see the content faster, leading to higher user satisfaction and engagement.
7. CDN Compatibility: Static files can be easily distributed across a content delivery network, ensuring that the content is delivered from servers geographically closer to the user. This minimizes latency and further enhances page load times.
8. Offline Support: Static pages can be easily cached, enabling offline access for users. Once the pages are loaded, they can be stored in the browser cache, allowing users to access the content even without an internet connection.
How to Achieve This with Optimizely Graph:
In Next.js, there is a method called generateStaticParams that must return all static routes. To obtain this information, we need to query Optimizely Graph for all routes:
The GraphQL query to the Content Graph:
query AllPages($pageType: [String]) {
  Content(locale: en, where: { ContentType: { in: $pageType } }) {
    items {
Implementation in TypeScript:
import { optimizely } from '@/api/optimizely'
export const getStandardPagesUrls = async () => {
  const pages = await optimizely.AllPages({ pageType: 'StandardPage' })
  const pagesWithUrls = pages?.data?.Content?.items?.filter((page) => !!page?.RelativePath)
  const paths = pagesWithUrls?.map((page) => {
    const segments = page?.RelativePath?.split('/').filter(String)
    return {
      name: page?.Name,
      segments: segments,
      path: page?.RelativePath,
  return paths
Considering that our project uses `[[…page]]` (check Next.js Dynamic Routes), we need to split the path into a string array, such as /en/alloy-meet ⇒ [’en’, ‘alloy-meet’]. Therefore, we perform a split operation
export async function generateStaticParams() {
  try {
    const paths = await getStandardPagesUrls()
    return paths?.map((path) => path.segments) ?? []
  } catch (e) {
    console.error(`Failed to retrieve static pages: ${e}`)
    return []
In summary, Static Site Generation in Next.js offers a way to generate performant, SEO-friendly, and cost-effective websites by pre-rendering pages at build time. It leverages the benefits of static content while providing the flexibility and convenience of a dynamic web framework.

Using Webhooks for Page Revalidation (ISR)

Let's start by understanding what ISR (Incremental Static Regeneration) is.
Next.js empowers you to create or update static pages *after* building your site. Incremental Static Regeneration (ISR) allows you to utilize static generation on a per-page basis, eliminating the need to rebuild the entire site. With ISR, you can maintain the advantages of static pages while effortlessly scaling to millions.
In essence, ISR enables you to instruct Next.js to update the cache for a specific page and use the most recent published content.
Now the question arises: How do we know when the content in the content graph has been updated and returns the freshest content? This is where webhooks come into play. You can configure a webhook to inquire about content revalidation from the provided API when it's ready. The link to configure the webhook can be found here:
Optimizely Webhook Configuration
On this page, you need to choose the type of authentication. I opted for Header HMAC authentication (epi-hmac xxx), and the token used here is also employed for fetching unpublished content in draft mode. The token is a combination of AppKey and AppSecret. You can learn more about it here
Before configuring the webhook, we need to create an API route that will handle the revalidation logic. For us, this route is `api/revalidate`.
import { optimizely } from "@/api/optimizely";
import { revalidatePath } from "next/cache";
export async function POST(request: Request) {
  const { searchParams } = new URL(request.url);
  const webhookSecret = searchParams?.get("cg_webhook_secret");
  if (webhookSecret !== process.env.CG_WEBHOOK_SECRET) {
    return new Response("Invalid credentials", {
      status: 401,
  const requestJson = await request.json();
  const docId = requestJson?.data?.docId || "";
  if (docId && docId.includes("Published")) {
    const [guid] = docId.split("_");
    try {
      const { data, errors } = await optimizely.GetContentByGuid({ guid });
      if (errors) {
        return new Response("Error fetching content", { status: 500 });
      const url = data?.Content?.items?.[0]?.RelativePath;
      if (!url) {
        return new Response("Page Not Found", { status: 400 });
      const normalizeUrl = url.startsWith("/") ? url : `/${url}`;
      // Someone published new changes, flush the cache
      console.log(`Flushing cache for: ${normalizeUrl}`);
    } catch (error) {
      console.error("Error processing webhook:", error);
      return new Response("Internal Server Error", { status: 500 });
  return new Response("OK", {
    status: 200,
To locally test the webhook communication, you can use ngrok tunneling. It will proxy your http://localhost:3000 to https and generate a randomly assigned link. After creating it, you can use that link for webhook configuration.

Webhook Configuration:

Webhook configuration
Disabled: false
url: <your-url>/api/revalidate?cg_webhook_secret=<secret from env>
method: POST
Example `requestJson`:
  timestamp: '2023-11-24T13:48:58.1384743+00:00',
  tenantId: 'bd1b0e79e7cf4548b046292ce22ea898',
  type: { subject: 'doc', action: 'updated' },
  data: { docId: '7b08c7d5-7585-47e5-add7-5822da68c3ce_en_Published' }
flushing cache for: /en
Unfortunately, the webhook only returns the `GUID` and not the `RelativePath`. To extract the path, we need to query the Optimizely graph for information about the page based on the GUID.
query GetContentByGuid($guid: String) {
  Content(where: { ContentLink: { GuidValue: { eq: $guid } } }) {
    items {
All these components allow us to generate the entire site during production builds and return all pages from the cache. When we publish a new change to one of the pages, we only revalidate that specific page, enabling a seamless and efficient process.

Pros and Cons of Optimizely Graph and Next.js Headless Solution

Pros  Cons 
Performance (SSR, SSG, ISR) Additional layer of complexity
Uses Edge No support for Commerce
Uses latest technology A more expensive solution than the traditional Hybrid in terms of development
Attractive for devs Publishing content in CMS does not equate to an immediate change on the site
Good SEO
A single source for content CMS -> web, mobile app…
Optimizely Graph has several advantages, including improved performance through server-side rendering (SSR) and static site generation (SSG). It leverages edge computing technology and utilizes the latest technology stack, making it attractive for developers. It also offers good search engine optimization (SEO) capabilities and provides a centralized content management system (CMS) for managing content across web and mobile applications.
However, there are some drawbacks to consider. Implementing the solution adds an additional layer of complexity to the development process. It currently lacks support for commerce functionality. The development costs can be higher compared to traditional hybrid solutions. It's also important to note that publishing content in the CMS does not guarantee immediate changes on the website.
In summary, while the solution offers performance benefits, modern technology and a centralized content management system, it also presents complexities, limitations in commerce support and higher development costs. It's crucial to assess these factors based on specific requirements before deciding on its adoption.

Best Practices and Tips

Avoid using Apollo Client

Avoid using Apollo Client in your Next.js project. While it is a powerful GraphQL client, Next.js provides robust native support for fetching data using the built-in `fetch` function. Apollo Client, being feature-rich, can introduce unnecessary overhead to your project.
Next.js comes with several built-in features that make it well-suited for handling GraphQL requests. It provides extensive capabilities, such as cache tags and revalidation, making it unnecessary to rely on external heavyweight libraries like Apollo Client.
By sticking to Next.js native features and optimizing your GraphQL requests using the built-in fetch function, you can keep your project lightweight, efficient, and well-aligned with the framework's best practices.


Use Tailwind CSS

I recommend using Tailwind CSS as it provides an optimal solution tailored for Next.js. Tailwind CSS generates all styles during the build process into a single file, which is typically very small (in our Next.js projects, the CSS file is around 16-30kb). This approach enhances performance and aligns well with Next.js conventions.

UI Librabry

If you're in search of a UI library with versatile components, we highly recommend choosing Shadcn UI. It has proven to be an excellent choice in several of our projects. Shadcn UI offers extensive customization options and has consistently met all our requirements.
More about shadcn

Use Codegen

Utilizing code generation alongside Optimizely Graph can save a significant amount of time. This is because you won't need to manually type page or block types. Code generation also creates TypeScript methods for interacting with Optimizely Graph. All you have to do is create a GraphQL query and run the code generation command. Notably, you can pass your custom fetcher, enabling customization according to your requirements.


In conclusion, the integration of Optimizely Graph and Next.js offers a powerful solution for building scalable headless applications. By leveraging GraphQL capabilities through Optimizely Graph, developers can structure and interconnect content within Optimizely CMS in a more efficient and intuitive manner. This is complemented by Next.js, a React-based frontend framework that enables the creation of performant and SEO-friendly web applications, supporting both static and dynamic rendering.
The symbiotic relationship between Optimizely Graph and Next.js allows for the crafting of dynamic headless experiences. This includes features like draft modes, On-Page Editing, static site generation, and efficient page revalidation through webhooks.
In summary, the Optimizely Graph and Next.js integration provides a robust foundation for developing scalable, performant, and SEO-friendly headless applications, with the flexibility to adapt to various use cases and requirements.
Nov 27, 2023


Scott Rockers
Scott Rockers Jun 17, 2024 06:04 PM

Hello Szymon,

Great write up in this article. Thank you for it.

Our company has successfully been able to set up the page revalidation, and it seems to be working as expected.

The main problem and question we have is around block revalidation. Specifically on shared blocks that are being used on multiple pages, the returned json coming from the webhook contains the GUID ID of the block.  As stated in your write up "the webhook only returns the
`GUID` and not the `RelativePath`. To extract the path, we need to query the Optimizely graph for information about the page based on the GUID." 

And we are using the content graph call you provided to get the relative path.
"query GetContentByGuid($guid: String) { Content(where: { ContentLink: { GuidValue: { eq: $guid } } }) { items { RelativePath } } }"
and this works great for pages, but currently this does not help us in terms of revalidating block level updates, as there is no relative path for pages in terms of shared blocks.

Do you or your team have a solution around revalidating cache when dealing with shared blocks that are being used on a ton of pages?



Please login to comment.
Latest blogs
Remove a segment from the URL in CMS 12

Problem : I have created thousands of pages dynamically using schedule jobs with different templates (e.g. one column, two columns, etc..) and stor...

Sanjay Kumar | Jun 21, 2024

Copying property values part 2

After publishing my last article about copying property values to other language versions, I received constructive feedback on how could I change t...

Grzegorz Wiecheć | Jun 18, 2024 | Syndicated blog

Enhancing online shopping through Optimizely's personalized product recommendations

In this blog, I have summarized my experience of using and learning product recommendation feature of Optimizely Personalization Artificial...

Hetaxi | Jun 18, 2024

New Series: Building a .NET Core headless site on Optimizely Graph and SaaS CMS

Welcome to this new multi-post series where you can follow along as I indulge in yet another crazy experiment: Can we make our beloved Alloy site r...

Allan Thraen | Jun 14, 2024 | Syndicated blog