<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom"><title type="text">Blog posts by Michał Mitas</title><link href="http://world.optimizely.com" /><updated>2026-01-26T00:00:00.0000000Z</updated><id>https://world.optimizely.com/blogs/micha-mitas/</id> <generator uri="http://world.optimizely.com" version="2.0">Optimizely World</generator> <entry><title>Building AI-Powered Tools with Optimizely Opal - A Step-by-Step Guide</title><link href="https://michalmitas.com/blog/opal-tools-tutorial" /><id>Learn how to build and integrate custom tools with Optimizely Opal using the Opal Tools SDK. This tutorial walks through creating tools, handling authorization, and leveraging AI reasoning.</id><updated>2026-01-26T00:00:00.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>2025 Wrapped: Optimizely&#39;s Year of AI Acceleration and Opal Momentum</title><link href="https://michalmitas.com/blog/2025-wrapped" /><id>A look back at Optimizely&#39;s most transformative year yet, with Opal at the center - and what&#39;s coming next in 2026.</id><updated>2025-12-29T00:00:00.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Going Headless: 3 Ways to Store Custom Data in Optimizely Graph</title><link href="https://world.optimizely.com/blogs/micha-mitas/dates/2025/11/going-headless-3-ways-to-store-custom-data-in-optimizely-graph/" /><id>&lt;p&gt;Welcome to another installment of my&amp;nbsp;&lt;em&gt;Going Headless&lt;/em&gt;&amp;nbsp;series. Previously, we covered:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/link/e41f24d11ff7488db0b8ccc26b9711a7.aspx&quot;&gt;Going Headless: Making the Right Architectural Choices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/link/425ad6b768e94410ab706643559c4f96.aspx&quot;&gt;Going Headless: On-Page Editing with Optimizely Graph and Next.js&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/link/c4887f08a93144b19377f27068985f71.aspx&quot;&gt;Going Headless: Optimizely Graph vs Content Delivery API&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So far, we&#39;ve talked about architectural decisions, on-page editing, and how Optimizely Graph compares to other APIs.&lt;br /&gt;Today, we&#39;ll take the next step and look at&amp;nbsp;&lt;strong&gt;how to store and expose your custom data in Optimizely Graph&lt;/strong&gt;&amp;nbsp;&amp;mdash; data that doesn&#39;t necessarily come directly from CMS properties.&lt;/p&gt;
&lt;p&gt;This article walks through&amp;nbsp;&lt;strong&gt;three different approaches&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Using the Conventions API&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Creating a custom&lt;/strong&gt;&amp;nbsp;&lt;span style=&quot;color: rgb(224, 62, 45);&quot;&gt;&lt;span style=&quot;color: rgb(186, 55, 42);&quot;&gt;IContentApiModelProperty&lt;/span&gt;&amp;nbsp;&lt;/span&gt;&lt;strong&gt;implementation&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Using the Graph Source SDK&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Each method offers a different level of control, flexibility, and effort. Let&#39;s dive in.&lt;/p&gt;
&lt;h2 class=&quot;flex scroll-m-28 flex-row items-center gap-2&quot;&gt;1. Using the Conventions API&lt;/h2&gt;
&lt;p&gt;The first and most straightforward method for influencing how your data is indexed in Optimizely Graph is through the&amp;nbsp;&lt;strong&gt;Conventions API&lt;/strong&gt;. This API allows developers to modify or extend how CMS content is represented when it&#39;s sent to the indexing pipeline. In other words, before your content gets published to Graph, the Conventions API gives you a hook to reshape or enrich the data.&lt;/p&gt;
&lt;h3 class=&quot;flex scroll-m-28 flex-row items-center gap-2&quot;&gt;When to use it&lt;/h3&gt;
&lt;p&gt;If you only need to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Exclude specific properties or content types,&lt;/li&gt;
&lt;li&gt;Include some custom fields,&lt;/li&gt;
&lt;li&gt;Add some lightweight metadata derived from existing content properties&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;hellip;then the Conventions API is your best friend.&lt;/p&gt;
&lt;h3 class=&quot;flex scroll-m-28 flex-row items-center gap-2&quot;&gt;Example:&lt;/h3&gt;
&lt;p&gt;Let&#39;s say you have a&amp;nbsp;&lt;span style=&quot;color: rgb(186, 55, 42);&quot;&gt;LandingPage&amp;nbsp;&lt;/span&gt;and&amp;nbsp;&lt;span style=&quot;color: rgb(224, 62, 45);&quot;&gt;&lt;span style=&quot;color: rgb(186, 55, 42);&quot;&gt;InternalSettingsPage&lt;/span&gt;&amp;nbsp;&lt;/span&gt;content types:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;// InternalSettingsPage.cs
[ContentType(DisplayName = &quot;Internal Settings Page&quot;)]
public class InternalSettingsPage : SitePageData
{
    [Display(
        Name = &quot;Some Internal Setting&quot;,
        Description = &quot;A confidential value / setting that should not be published in Graph&quot;)]
    public virtual string? SomeInternalSetting { get; set; }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;// LandingPage.cs
using EPiServer.Core;
using EPiServer.DataAnnotations;
using System.ComponentModel.DataAnnotations;

[ContentType(DisplayName = &quot;Landing Page&quot;)]
public class LandingPage : SitePageData
{
    [Display(
        Name = &quot;Secret Property&quot;,
        Description = &quot;A confidential value not published in Graph&quot;)]
    public virtual string? SecretProperty { get; set; }

    [Display(
        Name = &quot;Title Prefix&quot;,
        Description = &quot;Prefix for the page title&quot;)]
    public virtual string? TitlePrefix { get; set; }

    [Display(
        Name = &quot;Title&quot;,
        Description = &quot;The main page title&quot;)]
    public virtual string? Title { get; set; }

    public virtual string PageTitle()
    {
        return TitlePrefix + &quot; &quot; + Title;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For these content types you want to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Exclude&amp;nbsp;&lt;span style=&quot;color: rgb(186, 55, 42);&quot;&gt;InternalSettingsPage&lt;/span&gt;&amp;nbsp;content type from being indexed to Graph&lt;/li&gt;
&lt;li&gt;Exclude the&amp;nbsp;&lt;span style=&quot;color: rgb(186, 55, 42);&quot;&gt;SecretProperty&lt;/span&gt;&amp;nbsp;to prevent it from leaking into Graph, and&lt;/li&gt;
&lt;li&gt;Include custom&amp;nbsp;&lt;span style=&quot;color: rgb(186, 55, 42);&quot;&gt;PageTitle&lt;/span&gt;&amp;nbsp;property (being a mix of TitlePrefix and Title properties) in the Graph index&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can do it with a simple convention registration:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;// GraphConventions.cs
using EPiServer.Framework;
using EPiServer.Framework.Initialization;
using EPiServer.ServiceLocation;
using Optimizely.ContentGraph.Cms.NetCore.ConventionsApi;

[ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
public class GraphConventions : IInitializableModule
{
    public void Initialize(InitializationEngine context)
    {
        var conventionRepository = context.Locate.Advanced.GetInstance&amp;lt;ConventionRepository&amp;gt;();

        conventionRepository.ExcludeContentType&amp;lt;InternalSettingsPage&amp;gt;();

        conventionRepository.ForInstancesOf&amp;lt;LandingPage&amp;gt;()
            .ExcludeField(x =&amp;gt; x.SecretProperty);
            
       conventionRepository.ForInstancesOf&amp;lt;LandingPage&amp;gt;()
            .IncludeField(x =&amp;gt; x.PageTitle());
    }

    public void Uninitialize(InitializationEngine context) { }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once this convention is registered, Optimizely will skip excluded content types and apply defined adjustments whenever a &lt;code&gt;LandingPage&lt;/code&gt; is serialized for indexing in Graph.&lt;/p&gt;
&lt;h2 class=&quot;flex scroll-m-28 flex-row items-center gap-2&quot;&gt;2. Creating a Custom&amp;nbsp;&lt;span style=&quot;color: rgb(186, 55, 42);&quot;&gt;IContentApiModelProperty&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Sometimes conventions alone aren&#39;t enough.&lt;br /&gt;Maybe you want to inject&amp;nbsp;&lt;strong&gt;dynamic data&lt;/strong&gt;&amp;nbsp;that doesn&#39;t exist in CMS at all &amp;mdash; data coming from another service, or a calculated value that depends on runtime conditions.&lt;/p&gt;
&lt;p&gt;That&#39;s when&amp;nbsp;&lt;span style=&quot;color: rgb(186, 55, 42);&quot;&gt;IContentApiModelProperty&lt;/span&gt;&amp;nbsp;comes into play.&lt;/p&gt;
&lt;h3 class=&quot;flex scroll-m-28 flex-row items-center gap-2&quot;&gt;What it does&lt;/h3&gt;
&lt;p&gt;&lt;span style=&quot;color: rgb(186, 55, 42);&quot;&gt;IContentApiModelProperty&lt;/span&gt;&amp;nbsp;allows you to define&amp;nbsp;&lt;strong&gt;a new property&lt;/strong&gt;&amp;nbsp;that will be available for all content items (or selected ones) when indexed in Optimizely Graph.&lt;br /&gt;You can think of it as a plug-in point for adding custom, computed, or external data to the Graph model.&lt;/p&gt;
&lt;h3 class=&quot;flex scroll-m-28 flex-row items-center gap-2&quot;&gt;Example: Adding a custom property&lt;/h3&gt;
&lt;p&gt;Here&#39;s a simple implementation:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;using Optimizely.ContentGraph.Cms.Core.ContentApiModelProperties;

public sealed class CustomApiModelProperty : IContentApiModelProperty
{
    private readonly IContentLoader _contentLoader;

    public CustomApiModelProperty(IContentLoader contentLoader)
    {
        _contentLoader = contentLoader;
    }

    public string Name =&amp;gt; &quot;CustomPropertyName&quot;;

    public object GetValue(ContentApiModel contentApiModel)
    {
        // Example: Load the content and derive a value dynamically
        if (_contentLoader.TryGet(
            new ContentReference(contentApiModel.ContentLink.Id ?? 0), 
            out SitePageData page))
        {
            return $&quot;calculated-value-for-{page.Name}&quot;;
        }

        return null;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When this property is registered,&amp;nbsp;&lt;strong&gt;every content item&lt;/strong&gt;&amp;nbsp;sent to Optimizely Graph will include an additional field called&amp;nbsp;&lt;span style=&quot;color: rgb(186, 55, 42);&quot;&gt;CustomPropertyName,&lt;/span&gt; with whatever value you return from&amp;nbsp;&lt;span style=&quot;color: rgb(186, 55, 42);&quot;&gt;GetValue()&lt;span style=&quot;color: rgb(255, 255, 255);&quot;&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;For example, the Graph output might look like this:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;{
  &quot;name&quot;: &quot;About Us&quot;,
  &quot;customPropertyName&quot;: &quot;calculated-value-for-About Us&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 class=&quot;flex scroll-m-28 flex-row items-center gap-2&quot;&gt;Practical uses&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Injecting content from external APIs (for example, pulling stock data for product pages)&lt;/li&gt;
&lt;li&gt;Calculating derived values (e.g., reading time, rating averages)&lt;/li&gt;
&lt;li&gt;Adding environment or contextual metadata&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 class=&quot;flex scroll-m-28 flex-row items-center gap-2&quot;&gt;3. Using the Graph Source SDK&lt;/h2&gt;
&lt;p&gt;The last approach is the most flexible &amp;mdash; and the most decoupled from CMS.&lt;br /&gt;If you have data&amp;nbsp;&lt;strong&gt;completely external&lt;/strong&gt;&amp;nbsp;to the CMS (e.g., from a CRM, ERP, or a custom database), you can use the&amp;nbsp;&lt;strong&gt;Optimizely Graph Source SDK&lt;/strong&gt;&amp;nbsp;to push it directly into Graph.&lt;/p&gt;
&lt;p&gt;The SDK gives you full control over what and how you publish to Optimizely Graph, allowing you to create&amp;nbsp;&lt;strong&gt;custom schemas&lt;/strong&gt;&amp;nbsp;and&amp;nbsp;&lt;strong&gt;populate data manually&lt;/strong&gt;.&lt;/p&gt;
&lt;h3 class=&quot;flex scroll-m-28 flex-row items-center gap-2&quot;&gt;When to use it&lt;/h3&gt;
&lt;p&gt;Use the SDK when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You need to index non-CMS data (e.g., product catalogs, events, user profiles)&lt;/li&gt;
&lt;li&gt;You want to synchronize third-party data sources with Graph&lt;/li&gt;
&lt;li&gt;You&#39;re building a headless architecture where CMS is just one of multiple content sources&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 class=&quot;flex scroll-m-28 flex-row items-center gap-2&quot;&gt;Example: Pushing external data&lt;/h3&gt;
&lt;p&gt;Here&#39;s a simplified example based on the&amp;nbsp;&lt;a href=&quot;https://github.com/episerver/graph-source-sdk&quot;&gt;Graph Source SDK on GitHub&lt;/a&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;using Optimizely.Graph.Source.Sdk;
using Optimizely.Graph.Source.Sdk.SourceConfiguration;

public class ExternalProduct
{
    public string? Id { get; set; }
    public string? Name { get; set; }
    public double Price { get; set; }
}

// Initialize the GraphSourceClient by calling the Create method
var source = &quot;custom-source&quot;;
var appKey = &quot;your-app-key&quot;;
var secret = &quot;your-secret&quot;;

// Initialize the GraphSourceClient by calling the Create method
var client = GraphSourceClient.Create(new Uri(&quot;https://cg.optimizely.com&quot;), source, appKey, secret);

// Add a language preference
client.AddLanguage(&quot;en&quot;);

// Configure content type for ExternalProduct
client.ConfigureContentType&amp;lt;ExternalProduct&amp;gt;()
    .Field(x =&amp;gt; x.Id, IndexingType.Searchable)
    .Field(x =&amp;gt; x.Name, IndexingType.Searchable)
    .Field(x =&amp;gt; x.Price, IndexingType.Queryable);

// Save content types to Optimizely Graph
client.SaveTypesAsync();

// Instantiate and assign values for ExternalProduct
var product = new ExternalProduct
{
    Id = &quot;SKU-001&quot;,
    Name = &quot;Custom Running Shoes&quot;,
    Price = 129.99,
};

// Use the client to sync the product
client.SaveContentAsync(generateId: (x) =&amp;gt; x.Id, &quot;en&quot;, product);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This code defines a custom schema &lt;span style=&quot;color: rgb(186, 55, 42);&quot;&gt;(ExternalProduct)&lt;/span&gt; and publishes it to Optimizely Graph.&lt;br /&gt;Once indexed, you can query your data directly through GraphQL &amp;mdash; just like CMS content.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Important note about Optimizely Graph sources:&lt;/strong&gt;&lt;br /&gt;In the code above, we specified the source as&amp;nbsp;&lt;span style=&quot;color: rgb(186, 55, 42);&quot;&gt;custom-source,&lt;/span&gt; but you may choose any name to logically separate your data sources. Each&amp;nbsp;&lt;em&gt;source&lt;/em&gt;&amp;nbsp;acts as its own container in Optimizely Graph.&lt;/p&gt;
&lt;p&gt;By default, CMS content is stored in the&amp;nbsp;&lt;span style=&quot;color: rgb(186, 55, 42);&quot;&gt;default&lt;/span&gt;&amp;nbsp;source. If you were to use&amp;nbsp;&lt;span style=&quot;color: rgb(186, 55, 42);&quot;&gt;default&lt;/span&gt;&amp;nbsp;as the source when pushing custom types, you risk overwriting your entire Graph schema for that source. This is because calling&amp;nbsp;&lt;span style=&quot;color: rgb(186, 55, 42);&quot;&gt;client.SaveTypesAsync()&lt;/span&gt;&amp;nbsp;replaces all content types in the target source with only those specified by&amp;nbsp;&lt;span style=&quot;color: rgb(186, 55, 42);&quot;&gt;ConfigureContentType.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;To avoid disrupting your CMS data and schema, always register external data under a separate source name (such as&amp;nbsp;&lt;span style=&quot;color: rgb(186, 55, 42);&quot;&gt;custom-source).&lt;/span&gt; This keeps your external types isolated and safe from unintended changes.&lt;/p&gt;
&lt;h3 class=&quot;flex scroll-m-28 flex-row items-center gap-2&quot;&gt;Example query&lt;/h3&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;query CustomQuery {
  ExternalProduct {
    items {
      Id
      Name
      Price
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And you&#39;ll get:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;{
  &quot;data&quot;: {
    &quot;ExternalProduct&quot;: {
      &quot;items&quot;: [
        {
          &quot;Id&quot;: &quot;SKU-001&quot;,
          &quot;Name&quot;: &quot;Custom Running Shoes&quot;,
          &quot;Price&quot;: 129.99
        }
      ]
    }
  },
  &quot;extensions&quot;: {
    &quot;correlationId&quot;: &quot;998b0a360df95906&quot;,
    &quot;cost&quot;: 23,
    &quot;costSummary&quot;: [
      &quot;ExternalProduct(23) = limit(20) + 3*fields(1)&quot;
    ]
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 class=&quot;flex scroll-m-28 flex-row items-center gap-2&quot;&gt;Key takeaway&lt;/h3&gt;
&lt;p&gt;Use the Graph Source SDK when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Your data lives outside CMS,&lt;/li&gt;
&lt;li&gt;You want total control over schemas,&lt;/li&gt;
&lt;li&gt;You&#39;re integrating Graph as part of a larger headless ecosystem.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It&#39;s the most advanced option &amp;mdash; but it also opens up the most possibilities.&lt;/p&gt;
&lt;h2 class=&quot;flex scroll-m-28 flex-row items-center gap-2&quot;&gt;Wrapping Up&lt;/h2&gt;
&lt;p&gt;In a headless setup,&amp;nbsp;&lt;strong&gt;Optimizely Graph&lt;/strong&gt;&amp;nbsp;becomes much more than a content delivery layer.&lt;br /&gt;It can act as a&amp;nbsp;&lt;strong&gt;centralized data hub&lt;/strong&gt;&amp;nbsp;&amp;mdash; combining CMS content, computed properties, and external datasets under one unified GraphQL API.&lt;/p&gt;
&lt;p&gt;To recap:&lt;/p&gt;
&lt;div class=&quot;relative overflow-auto prose-no-margin my-6&quot;&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;th&gt;Level of Effort&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Conventions API&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Shaping existing CMS data&lt;/td&gt;
&lt;td&gt;&#128994; Low&lt;/td&gt;
&lt;td&gt;Ideal for renaming, excluding, or slightly enriching data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;IContentApiModelProperty&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Injecting dynamic or computed data&lt;/td&gt;
&lt;td&gt;&#128992; Medium&lt;/td&gt;
&lt;td&gt;Great for adding calculated or external fields&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Graph Source SDK&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Indexing non-CMS data sources&lt;/td&gt;
&lt;td&gt;&#128308; High&lt;/td&gt;
&lt;td&gt;Full control over schema and indexing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Choosing the right approach depends on your architectural goals &amp;mdash; and how &amp;ldquo;headless&amp;rdquo; you really want to go.&lt;/p&gt;
&lt;p&gt;In most projects, a combination works best:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use&amp;nbsp;&lt;strong&gt;Conventions&lt;/strong&gt;&amp;nbsp;for quick tweaks,&lt;/li&gt;
&lt;li&gt;Add&amp;nbsp;&lt;strong&gt;custom properties&lt;/strong&gt;&amp;nbsp;for dynamic logic, and&lt;/li&gt;
&lt;li&gt;Extend with the&amp;nbsp;&lt;strong&gt;SDK&lt;/strong&gt; when you need full control.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That&#39;s it for this installment of&amp;nbsp;&lt;em&gt;Going Headless&lt;/em&gt;!&lt;br /&gt;In the next part, we&#39;ll look at how to query your combined datasets efficiently &amp;mdash; and how to design your schema for real-world performance.&lt;/p&gt;
&lt;h3 class=&quot;flex scroll-m-28 flex-row items-center gap-2&quot;&gt;Further Reading&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.developers.optimizely.com/graph/docs&quot;&gt;Optimizely Graph Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.developers.optimizely.com/content-management-system/docs/conventions-api&quot;&gt;Conventions API Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/episerver/graph-source-sdk&quot;&gt;Graph Source SDK on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/link/e41f24d11ff7488db0b8ccc26b9711a7.aspx&quot;&gt;Going Headless: Making the Right Architectural Choices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/link/c4887f08a93144b19377f27068985f71.aspx&quot;&gt;Going Headless: Optimizely Graph vs Content Delivery API&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</id><updated>2025-11-03T10:23:28.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Going Headless - 3 Ways to Store Custom Data in Optimizely Graph</title><link href="https://michalmitas.com/blog/custom-graph-data" /><id>Three different approaches to storing custom data in Optimizely Graph.</id><updated>2025-11-03T00:00:00.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Going Headless: Making the Right Architectural Choices</title><link href="https://world.optimizely.com/blogs/micha-mitas/dates/2025/10/going-headless-making-the-right-architectural-choices/" /><id>&lt;p&gt;Earlier this year, I began a series of articles about headless architecture:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/link/c4887f08a93144b19377f27068985f71.aspx&quot;&gt;Going Headless: Optimizely Graph vs Content Delivery API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/link/425ad6b768e94410ab706643559c4f96.aspx&quot;&gt;Going Headless: On-Page Editing with Optimizely Graph and Next.js&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Today&#39;s post continues that series. Having just completed the launch of a new website built with a headless architecture, I&#39;d like to share key observations, strategic considerations, and my own perspective on the strengths and challenges of the headless model. My goal is to help you make confident, informed choices for your own future project architecture.&lt;/p&gt;
&lt;h2 class=&quot;flex scroll-m-28 flex-row items-center gap-2&quot;&gt;What This Article Covers&lt;/h2&gt;
&lt;p&gt;In today&#39;s post, we&#39;ll start with the fundamentals:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What &amp;ldquo;headless&amp;rdquo; really means in the Optimizely context&lt;/li&gt;
&lt;li&gt;Why some choose it (and why some shouldn&#39;t)&lt;/li&gt;
&lt;li&gt;Choosing the right architecture for your headless Optimizely setup&lt;/li&gt;
&lt;li&gt;A high-level architecture overview to ground future discussions&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 class=&quot;flex scroll-m-28 flex-row items-center gap-2&quot;&gt;What &amp;ldquo;Headless&amp;rdquo; Means in the Optimizely World&lt;/h2&gt;
&lt;p&gt;In a headless Optimizely setup, the CMS focuses on storing and structuring content and exposing it via APIs. Whether you use a&amp;nbsp;&lt;a href=&quot;/link/c4887f08a93144b19377f27068985f71.aspx&quot;&gt;Content Delivery API or Optimizely Graph&lt;/a&gt;, the &amp;ldquo;head&amp;rdquo; (the UI) is a separate app that fetches the content and renders it.&lt;/p&gt;
&lt;h2 class=&quot;flex scroll-m-28 flex-row items-center gap-2&quot;&gt;When Headless Might Not Be the Right Choice&lt;/h2&gt;
&lt;p&gt;The headless separation lets the front-end team ship without touching the CMS, and enables you to serve multiple channels from a single content source. While this flexibility is appealing, headless isn&#39;t always the right fit for every scenario.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;When headless may not be necessary:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Simple site needs&lt;/strong&gt;: If you only require a basic site, headless can introduce unnecessary complexity.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Limited developer capacity&lt;/strong&gt;: Headless architectures require a skilled front-end team comfortable with API-driven development.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Budget constraints&lt;/strong&gt;: Having two applications means you need to host two applications. This can impact your infrastructure or require additional licences and potentially higher costs.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Key challenges of going headless:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Routing and URL management&lt;/strong&gt;: You are responsible for route generation and resolution (slugs, dynamic routes, canonical URLs, redirects, and localization-aware paths). With Optimizely Graph, there&#39;s no built-in URL resolver, so you must design and maintain this logic on the front end.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;On-page editing and preview&lt;/strong&gt;: Traditional on-page editing from MVC is not available by default. You&#39;ll need to implement preview rendering of drafts, signed preview tokens, and live refresh hooks. If editors require a visual page preview, you&#39;ll have to build or integrate that experience. As a starting point you can follow my guide -&amp;nbsp;&lt;a href=&quot;/link/425ad6b768e94410ab706643559c4f96.aspx&quot;&gt;Going Headless: On-Page Editing with Optimizely Graph and Next.js&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CI/CD complexity&lt;/strong&gt;: Headless usually means separate pipelines for the CMS and front end, so you need to coordinate deployments and keep everything in sync. Additionally, managing schema changes in Optimizely Graph introduces further challenges, as updates must be reflected and tested across both applications.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Authentication and authorization&lt;/strong&gt;: You&#39;ll need to handle user logins, editor previews, and protect your APIs. This means managing secrets and reviewing security.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;As you can see, adopting a headless approach involves trade-offs and added complexity. Make sure your decision is guided by genuine business requirements, rather than simply following industry trends.&lt;/p&gt;
&lt;h2 class=&quot;flex scroll-m-28 flex-row items-center gap-2&quot;&gt;When Headless Is a Way To Go&lt;/h2&gt;
&lt;p&gt;Opting for a headless architecture can be a game changer for organizations that need agility, omnichannel delivery, and modern development workflows. If your content needs to be reused across multiple front ends&amp;mdash;such as websites, mobile apps, IoT devices, or digital kiosks &amp;mdash; headless streamlines the process and reduces duplication. It enables independent development and deployment of the frontend and backend, letting teams iterate quickly and select best-in-class technologies for each layer. For companies with demanding editorial workloads, headless can provide enhanced flexibility, customizable authoring experiences, and the ability to scale globally by leveraging CDN-backed static sites or API-first delivery. If you need powerful integrations, advanced personalization, or the latest in frontend performance tooling, headless often opens more doors than traditional approaches. Ultimately, headless is especially valuable when your project requires future-proofing, composability, and the freedom to evolve both your frontend and backend at their own pace.&lt;/p&gt;
&lt;h2 class=&quot;flex scroll-m-28 flex-row items-center gap-2&quot;&gt;Choosing the Right Architecture for Your Headless Optimizely&lt;/h2&gt;
&lt;p&gt;When picking your setup, balance short-term delivery with long-term flexibility. Consider who you&#39;re building for, which channels you must support, your team&#39;s skills, latency/scale needs, security/compliance, and total cost of ownership. Write these down up front-they&#39;ll keep trade-offs honest.&lt;/p&gt;
&lt;h1 class=&quot;flex scroll-m-28 flex-row items-center gap-2&quot;&gt;Technical decisions&lt;/h1&gt;
&lt;p&gt;These days, the variety of tools and architectural options can feel overwhelming, so it&#39;s impossible to cover every scenario here. Instead, I&#39;ll highlight some key decisions I&#39;ve faced recently and briefly explain each. Hopefully, these insights will be helpful.&lt;/p&gt;
&lt;h3 class=&quot;flex scroll-m-28 flex-row items-center gap-2&quot;&gt;CMS PaaS vs SaaS&lt;/h3&gt;
&lt;p&gt;PaaS offers greater control over your codebase and infrastructure, but comes with increased operational responsibilities. SaaS minimizes maintenance and accelerates upgrades, though it restricts server-side customization and hosting flexibility. Your choice should depend on your need for custom server logic and regional deployment control. For our project, we selected PaaS to retain full control over our source code and to support future integrations.&lt;/p&gt;
&lt;h3 class=&quot;flex scroll-m-28 flex-row items-center gap-2&quot;&gt;Optimizely DXP vs self-hosted&lt;/h3&gt;
&lt;p&gt;DXP gives you managed hosting, a global CDN, built-in security, and guaranteed SLAs right from the start. So a lot of the heavy lifting is taken care of for you. On the other hand, self-hosting might be the way to go if you have strict data residency requirements or unique networking needs, but just keep in mind that you&#39;ll be responsible for monitoring, scaling, patching, and handling any incidents yourself. Be sure to realistically weigh the operational costs of each option before making your choice.&lt;/p&gt;
&lt;h3 class=&quot;flex scroll-m-28 flex-row items-center gap-2&quot;&gt;Graph (GraphQL) vs CDA (Content Delivery API)&lt;/h3&gt;
&lt;p&gt;Optimizely Graph shifts content delivery to a managed service, enabling precise GraphQL queries-ideal for retrieving deeply nested or specific content-and offers advanced search capabilities. In contrast, the Content Delivery API (CDA) is simpler, REST-based, and provides built-in URL resolution, but may require additional effort for complex content structures or scaling needs. For a detailed comparison, see my article:&amp;nbsp;&lt;a href=&quot;/link/c4887f08a93144b19377f27068985f71.aspx&quot;&gt;Going Headless: Optimizely Graph vs Content Delivery API&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;For our project, we chosed Optimizely Graph because its data residency met our requirements and it provided greater flexibility for content delivery.&lt;/p&gt;
&lt;h3 class=&quot;flex scroll-m-28 flex-row items-center gap-2&quot;&gt;Next.js vs other front-end frameworks&lt;/h3&gt;
&lt;p&gt;Next.js offers SSR, SSG, and ISR out of the box, making it a strong choice for content-rich sites. Alternatives like React with Vite or Angular are also solid. Choose the framework your team is most comfortable with and that aligns with your hosting environment (Node, Edge, or serverless).&lt;/p&gt;
&lt;p&gt;For our project, rapid time to market was essential. We selected Next.js together with Vercel for its seamless support of SSR/SSG/ISR, integrated Turbo monorepo tooling, and a deployment workflow that enabled us to iterate quickly, gather feedback, and launch efficiently.&lt;/p&gt;
&lt;p&gt;If you&#39;re using the SaaS version of Optimizely CMS, you may want to explore the newly introduced DXP hosting for front-end applications. Learn more in the official documentation:&amp;nbsp;&lt;a href=&quot;https://docs.developers.optimizely.com/content-management-system/v1.0.0-CMS-SaaS/docs/host-a-front-end-with-optimizely&quot;&gt;https://docs.developers.optimizely.com/content-management-system/v1.0.0-CMS-SaaS/docs/host-a-front-end-with-optimizely&lt;/a&gt;&lt;/p&gt;
&lt;h3 class=&quot;flex scroll-m-28 flex-row items-center gap-2&quot;&gt;GitHub vs Azure DevOps&lt;/h3&gt;
&lt;p&gt;Pick the platform your team knows and that fits your cloud targets. GitHub pairs nicely with Actions and Vercel; Azure DevOps integrates tightly with Azure and enterprise governance. Both support trunk-based development, protected branches, and good CI/CD.&lt;/p&gt;
&lt;p&gt;We chose GitHub because we didn&#39;t need the additional features offered by Azure DevOps, and hosting our app in DXP meant Azure integrations were minimal. GitHub&#39;s seamless integration with Vercel was also a significant advantage for our workflow.&lt;/p&gt;
&lt;h3 class=&quot;flex scroll-m-28 flex-row items-center gap-2&quot;&gt;Opti ID vs other SSO providers&lt;/h3&gt;
&lt;p&gt;Use Opti ID where it makes sense within the Optimizely ecosystem. For your site/app users and internal editors, standard OpenID Connect/SAML providers (Azure AD, Okta, Auth0, etc.) work well. Confirm MFA, provisioning, and claim mapping early.&lt;/p&gt;
&lt;p&gt;We selected Opti ID because it met all our requirements and enabled us to implement authentication quickly and efficiently. For more details, refer to the official documentation:&amp;nbsp;&lt;a href=&quot;https://support.optimizely.com/hc/en-us/articles/12613241464461-Get-started-with-Opti-ID&quot;&gt;https://support.optimizely.com/hc/en-us/articles/12613241464461-Get-started-with-Opti-ID&lt;/a&gt;&lt;/p&gt;
&lt;h3 class=&quot;flex scroll-m-28 flex-row items-center gap-2&quot;&gt;Common Repository vs Separate Repositories&lt;/h3&gt;
&lt;p&gt;In a headless setup, the frontend and the CMS/backend are independent. You can keep them in one repository (monorepo) or split them into separate repositories.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Monorepo (one repository):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pros: single place for code and tooling, atomic changes across front end and CMS, easier end-to-end testing and shared packages&lt;/li&gt;
&lt;li&gt;Cons: more complex pipelines, risk of unnecessary deploys without proper path filters, larger repo to maintain&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Separate repositories:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pros: clear ownership and access control, simpler pipelines per app, easier to replace one part without touching the other&lt;/li&gt;
&lt;li&gt;Cons: coordination overhead (shared contracts, schemas, SDK versions), more cross-repo communication&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Opt for a single repository when teams need to collaborate closely, share code, or coordinate changes across frontend and backend. Separate repositories are preferable when independent deployments and isolated responsibilities are a priority.&lt;/p&gt;
&lt;p&gt;We chose a monorepo approach because our team size makes close collaboration easier, and it gives us greater control and visibility over the entire CI/CD process.&lt;/p&gt;
&lt;h1 class=&quot;flex scroll-m-28 flex-row items-center gap-2&quot;&gt;Summary&lt;/h1&gt;
&lt;p&gt;In summary, successful modern web architecture is about making the right choices for your team, business needs, and long-term maintainability. Each architectural decision, whether it&#39;s PaaS vs SaaS, managed services vs custom hosting, or selecting the front-end framework should reflect your unique requirements in performance, scalability, flexibility, and collaboration. By evaluating trade-offs at each level and prioritizing seamless integration, you can build solutions that are robust, adaptable, and future-proof.&lt;/p&gt;</id><updated>2025-10-17T07:19:44.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Going Headless - Making the Right Architectural Choices</title><link href="https://michalmitas.com/blog/architecture-overview" /><id>A high-level overview of the architectural decisions made for the new website built with a headless architecture.</id><updated>2025-10-17T00:00:00.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Going Headless: On-Page Editing with Optimizely Graph and Next.js</title><link href="https://world.optimizely.com/blogs/micha-mitas/dates/2025/9/draft/" /><id>&lt;div class=&quot;prose&quot;&gt;
&lt;h2 class=&quot;flex scroll-m-28 flex-row items-center gap-2&quot;&gt;Introduction&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;On-page editing&lt;/strong&gt; is one of the standout features of Optimizely CMS, giving editors the &lt;strong&gt;power to update&lt;/strong&gt; &lt;strong&gt;content directly on the site as they see it.&lt;/strong&gt; But when you move to a headless architecture, bringing that seamless editing experience to your custom frontend can seem daunting. The good news? It&#39;s absolutely possible and not as complicated as you might think!&lt;/p&gt;
&lt;p&gt;In this article, I&#39;ll walk you through how to enable on-page editing in your own solution using Next.js, so your editors can enjoy the best of both worlds: &lt;strong&gt;modern headless flexibility&lt;/strong&gt; and intuitive&lt;strong&gt; in-context editing.&lt;/strong&gt;&lt;/p&gt;
&lt;h2 class=&quot;flex scroll-m-28 flex-row items-center gap-2&quot;&gt;How does On-Page Editing work in a headless setup?&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/link/13c2a20625f14bc7ad8c5a88898bfd37.aspx&quot; width=&quot;896&quot; height=&quot;634&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Here&#39;s how the process typically works:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Optimizely CMS loads your frontend site&amp;nbsp;&lt;strong&gt;inside an iframe&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Your&amp;nbsp;&lt;strong&gt;frontend detects that it&#39;s running within the CMS iframe&lt;/strong&gt;&amp;nbsp;and automatically switches to&amp;nbsp;&lt;strong&gt;draft mode&lt;/strong&gt;, displaying preview versions of pages or blocks.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By including the&amp;nbsp;&lt;span style=&quot;color: rgb(224, 62, 45);&quot;&gt;&lt;em&gt;communicationinjector.js&lt;/em&gt;&lt;/span&gt;&amp;nbsp;script (provided by your Optimizely CMS app), your frontend can listen for and respond to events triggered from the CMS editor.&lt;/p&gt;
&lt;h2 class=&quot;flex scroll-m-28 flex-row items-center gap-2&quot;&gt;Step 1: Display the frontend in an iframe&lt;/h2&gt;
&lt;p&gt;There are a few things we need to do to display our frontend app in an iframe inside the CMS.&lt;/p&gt;
&lt;p&gt;First things first-tell Optimizely Graph to i&lt;strong&gt;nclude all draft content&lt;/strong&gt; by adding&amp;nbsp;&lt;em&gt;&lt;span style=&quot;color: rgb(224, 62, 45);&quot;&gt;&quot;AllowSyncDraftContent&quot;: true&lt;/span&gt;. &lt;/em&gt;When enabled, Graph indexes draft content and makes it available to the frontend. Let&#39;s also add&amp;nbsp;&lt;span style=&quot;color: rgb(224, 62, 45);&quot;&gt;&lt;em&gt;Headless:FrontEndUri&lt;/em&gt; &lt;/span&gt;so we can configure CORS for the frontend site.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;//appsettings.json
{
  &quot;Optimizely&quot;: {
    &quot;ContentGraph&quot;: {
      &quot;GatewayAddress&quot;: &quot;https://cg.optimizely.com&quot;,
      &quot;AppKey&quot;: &quot;YOUR_APP_KEY&quot;,
      &quot;Secret&quot;: &quot;YOUR_SECRET_KEY&quot;,
      &quot;SingleKey&quot;: &quot;YOUR_SINGLE_KEY&quot;,
      &quot;AllowSyncDraftContent&quot;: true
    }
  },
  &quot;Headless&quot;: {
    &quot;FrontEndUri&quot;: &quot;https://localhost:3000&quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To keep things clean, add an extension we&#39;ll use in &lt;span style=&quot;color: rgb(224, 62, 45);&quot;&gt;&lt;em&gt;Startup.cs&lt;/em&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;// HeadlessExtensions.cs
using EPiServer.ContentApi.Core.DependencyInjection;
using EPiServer.Web;

public static class HeadlessExtensions
{
    public static IServiceCollection AddHeadlessOnPageEditing(this IServiceCollection serviceCollection)
    {
        serviceCollection
            // Configure our CMS to use External Templates and to OptimizeForDelivery.
            // This will allow the Edit UI to load the decoupled delivery site into On Page Edit.
            // It will also instruct the overlay&#39;s bounding boxes to include floating instead of inline editors.
            // This is required to avoid cross-origin issues between the domain of the iframe and of the CMS itself.
            .ConfigureForExternalTemplates()
            .Configure&amp;lt;ExternalApplicationOptions&amp;gt;(options =&amp;gt; options.OptimizeForDelivery = true)

        return serviceCollection;
    }

    public static IApplicationBuilder AddCorsForFrontendApp(this IApplicationBuilder app, string? frontendUri)
    {
        if (!string.IsNullOrWhiteSpace(frontendUri))
        {
            app.UseCors(b =&amp;gt; b
                .WithOrigins($&quot;{frontendUri}&quot;, &quot;*&quot;)
                .WithExposedContentDeliveryApiHeaders()
                .WithHeaders(&quot;Authorization&quot;)
                .AllowAnyMethod()
                .AllowCredentials());
        }
        return app;
    }

    public static IApplicationBuilder AddRedirectToCms(this IApplicationBuilder app)
    { 
        // Since it&#39;s a headless setup we can redirect user from &quot;/&quot; directly to CMS
        app.UseStatusCodePages(context =&amp;gt;
        {
            if (context.HttpContext.Response.HasStarted == false &amp;amp;&amp;amp;
                context.HttpContext.Response.StatusCode == StatusCodes.Status404NotFound &amp;amp;&amp;amp;
                context.HttpContext.Request.Path == &quot;/&quot;)
            {
                context.HttpContext.Response.Redirect(&quot;/episerver/cms&quot;);                
                // or /ui/cms if you use OptiID or have changed path to your CMS               
                //context.HttpContext.Response.Redirect(&quot;/ui/cms&quot;)
            }

            return Task.CompletedTask;
        });

        return app;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now use this extension in&amp;nbsp;&lt;em&gt;&lt;span style=&quot;color: rgb(224, 62, 45);&quot;&gt;Startup.cs&lt;/span&gt;&lt;/em&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;// Startup.cs
public class Startup
{
    private readonly IWebHostEnvironment _webHostingEnvironment;
    private readonly IConfiguration _configuration;
    private readonly string? _frontendUri;

    public Startup(IWebHostEnvironment webHostingEnvironment, IConfiguration configuration)
    {
        // ...
        _frontendUri = _configuration.GetValue&amp;lt;string&amp;gt;(&quot;Headless:FrontEndUri&quot;);
    }

    public void ConfigureServices(IServiceCollection services)
    {
        // ...         
        services.AddHeadlessOnPageEditing();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        // ... 
        app.AddCorsForFrontendApp(_frontendUri);
        app.AddRedirectToCms();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Neat and clean. Next, enable the frontend to be loaded in an iframe. Add an environment variable:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;// .env.local
NEXT_PUBLIC_CMS_URL=&quot;https://localhost:5096&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and use it in the Next.js config:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;// next.config.mjs
async headers() {
    return [
      {
        source: &#39;/:path*&#39;,
        headers: [
          { key: &#39;X-Frame-Options&#39;, value: &#39;SAMEORIGIN&#39; },
          {
            key: &#39;Content-Security-Policy&#39;,
            value: `frame-ancestors &#39;self&#39; ${process.env.NEXT_PUBLIC_CMS_URL || &#39;localhost:5096&#39;}`,
          },
        ],
      },
    ]
  },&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally, go to&lt;strong&gt; &quot;Manage Websites&quot;&lt;/strong&gt; and &lt;strong&gt;define&lt;/strong&gt; &lt;strong&gt;hosts &lt;/strong&gt;for both the CMS and the frontend apps. Set this up correctly or the CMS won&#39;t be able to display previews from a different host.&lt;/p&gt;
&lt;p&gt;In example below:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CMS app:&amp;nbsp;https://localhost:5096/&lt;/li&gt;
&lt;li&gt;Frontend client:&amp;nbsp;https://localhost:3000/&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;flex gap-2 my-4 rounded-xl border bg-fd-card p-3 ps-1 text-sm text-fd-card-foreground shadow-md&quot;&gt;
&lt;div class=&quot;flex flex-col gap-2 min-w-0 flex-1&quot;&gt;
&lt;div class=&quot;text-fd-muted-foreground prose-no-margin empty:hidden&quot;&gt;
&lt;p&gt;&lt;strong&gt;Run both CMS and frontend on the same protocol, preferably HTTPS. Otherwise the CMS won&#39;t load the iframe.&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;img src=&quot;/link/474260c1ab4c44819d3edcc71b1c8ea3.aspx&quot; width=&quot;1019&quot; height=&quot;1030&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Now when you edit a page, the CMS should load the frontend, likely showing a 404. That&#39;s because Optimizely opens the iframe with a predefined URL that we need to handle.&lt;/p&gt;
&lt;h2 class=&quot;flex scroll-m-28 flex-row items-center gap-2&quot;&gt;Step 2: Draft mode and page preview&lt;/h2&gt;
&lt;p&gt;To make &lt;strong&gt;On-Page Editing&lt;/strong&gt; work in our Next.js app we need to &lt;strong&gt;use draft mode&lt;/strong&gt;. Draft Mode allows you to preview draft content from your headless CMS in your Next.js application. This is useful for static pages that are generated at build time as it allows you to switch to dynamic rendering and see the draft changes without having to rebuild your entire site.&lt;/p&gt;
&lt;p&gt;When the CMS loads a preview in an iframe it will hit one of two URLs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span style=&quot;color: rgb(224, 62, 45);&quot;&gt;https://localhost:3000/episerver/CMS/Content/en/,,5_8?epieditmode=true&amp;nbsp;&lt;/span&gt;for pages&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: rgb(224, 62, 45);&quot;&gt;https://localhost:3000/episerver/CMS/contentassets/en/77364cdc2802407b8b8cae45b65bc16e/,,7_10?epieditmode=true&lt;/span&gt;&amp;nbsp;for blocks&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These URLs contain the following information:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span style=&quot;color: rgb(224, 62, 45);&quot;&gt;en&amp;nbsp;&lt;/span&gt;- content locale&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: rgb(224, 62, 45);&quot;&gt;5_8&amp;nbsp;&lt;/span&gt;or&amp;nbsp;&lt;span style=&quot;color: rgb(224, 62, 45);&quot;&gt;7_10&amp;nbsp;&lt;/span&gt;- ID (5 for page, 7 for block) and workId (8 for page, 10 for block).&amp;nbsp;&lt;span style=&quot;color: rgb(224, 62, 45);&quot;&gt;id&lt;/span&gt;&amp;nbsp;identifies the content;&amp;nbsp;&lt;span style=&quot;color: rgb(224, 62, 45);&quot;&gt;workId&lt;/span&gt;&amp;nbsp;identifies the version.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: rgb(224, 62, 45);&quot;&gt;epieditmode&amp;nbsp;&lt;/span&gt;- distinguishes page preview (&lt;span style=&quot;color: rgb(224, 62, 45);&quot;&gt;epieditmode=false&lt;/span&gt;) from On-Page Editing (&lt;span style=&quot;color: rgb(224, 62, 45);&quot;&gt;epieditmode=true&lt;/span&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Knowing these URLs, set up redirects in your Next.js app:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;// next.config.mjs
async redirects() {
    return [
      { 
        source: &#39;/episerver/CMS/Content/:slug*&#39;,
        destination: &#39;/api/draft/:slug*&#39;,
        permanent: false,
      }
    ]
  },&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;flex gap-2 my-4 rounded-xl border bg-fd-card p-3 ps-1 text-sm text-fd-card-foreground shadow-md&quot;&gt;
&lt;div class=&quot;flex flex-col gap-2 min-w-0 flex-1&quot;&gt;
&lt;div class=&quot;text-fd-muted-foreground prose-no-margin empty:hidden&quot;&gt;
&lt;p&gt;&lt;strong&gt;If you&#39;re using OptiID or changed the CMS URL, you might need to replace &lt;/strong&gt;&lt;span style=&quot;color: rgb(224, 62, 45);&quot;&gt;episerver&lt;/span&gt;&lt;strong&gt;&amp;nbsp;with&amp;nbsp;&lt;/strong&gt;&lt;span style=&quot;color: rgb(224, 62, 45);&quot;&gt;ui&lt;/span&gt;&lt;strong&gt;&amp;nbsp;or another prefix.&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Now all requests to&amp;nbsp;&lt;span style=&quot;color: rgb(224, 62, 45);&quot;&gt;/episerver/CMS/Content/*&lt;/span&gt;&amp;nbsp;and&amp;nbsp;&lt;span style=&quot;color: rgb(224, 62, 45);&quot;&gt;/episerver/CMS/contentassets/*&lt;/span&gt;&amp;nbsp;will be redirected to our draft API route. Here we need to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Enable draft mode by calling&amp;nbsp;&lt;span style=&quot;color: rgb(224, 62, 45);&quot;&gt;(await draftMode()).enable()&lt;/span&gt;. This allows bypassing static generation and rendering on-demand at request time&lt;/li&gt;
&lt;li&gt;Extract parameters from URL&lt;/li&gt;
&lt;li&gt;Redirect to a draft (for page or block) and pass extracted parameters&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;// src/app/api/draft/[...slug]/route.ts&quot;
import { redirect, notFound } from &#39;next/navigation&#39;;
import { NextRequest } from &#39;next/server&#39;;
import { draftMode } from &#39;next/headers&#39;

export async function GET(request: NextRequest) {

  const url = new URL(request.url);
  const { searchParams, pathname } = url;

  // There are 2 different type of URLs to handle here...
  // For Page:
  // /api/draft/en/,,5_8?epieditmode=true
  // For Blocks:
  // /api/draft/contentassets/en/77364cdc2802407b8b8cae45b65bc16e/,,7_10?epieditmode=true

  // To get the value of a search param, use .get(&#39;&amp;lt;paramName&amp;gt;&#39;)
  const epiEditMode = searchParams.get(&#39;epieditmode&#39;);

  // Remove leading &#39;/api/draft/&#39; from pathname
  let path = pathname.replace(/^\/api\/draft\//, &#39;&#39;);

  // Assess type of content
  const isBlock = path.startsWith(&#39;contentassets&#39;);
  path = isBlock ? path.replace(&quot;contentassets/&quot;, &quot;&quot;) : path;
  
  // Extract locale + ids (Optimizely-specific pattern: /en/,,5_8)
  const segments = path.split(&quot;/&quot;);

  const locale = segments[0] || &quot;en&quot;;
  const idsPart = segments[segments.length - 1] || &quot;&quot;;

  const [, ids = &quot;&quot;] = idsPart.split(&quot;,,&quot;);
  const [id = &quot;0&quot;, workId = &quot;0&quot;] = ids.split(&quot;_&quot;);
  
  // Validate all required fields
  if (!locale || !id || !workId || !epiEditMode) {
    return notFound();
  }
  
  // Enable draft mode
  (await draftMode()).enable()
  
  const newSearchParams = new URLSearchParams({
    locale,
    id,
    workId,
    epiEditMode,
  });
  
  const newUrl = isBlock 
    ? `/draft/block?${newSearchParams.toString()}`
    : `/draft?${newSearchParams.toString()}`;

    redirect(`${newUrl}`);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Step 3: Include the communicationinjector.js script&lt;/h2&gt;
&lt;p&gt;To enable seamless communication between your draft site and the CMS for on-page editing, you must add the&amp;nbsp;&lt;em&gt;&lt;span style=&quot;color: rgb(224, 62, 45);&quot;&gt;communicationinjector.js&lt;/span&gt;&lt;/em&gt;&amp;nbsp;script to your draft layout. This script, served from your CMS instance, is required for listening to content save events and enabling real-time editing capabilities.&lt;/p&gt;
&lt;p&gt;Include it in your layout file:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;&amp;lt;Script src={`${process.env.NEXT_PUBLIC_CMS_URL}/util/javascript/communicationinjector.js`} /&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Step 4: Display the draft for a page.&lt;/h2&gt;
&lt;p&gt;A key aspect is the&amp;nbsp;&lt;span style=&quot;color: rgb(224, 62, 45);&quot;&gt;data-epi-edit&lt;/span&gt;&amp;nbsp;attribute, which tells Optimizely which field is editable on the page. In the example above, we set its value to&amp;nbsp;&lt;span style=&quot;color: rgb(224, 62, 45);&quot;&gt;&quot;title&quot;&lt;/span&gt;, matching the property name in the CMS.&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;// src/app/draft/page.tsx
import { renderBlocks } from &#39;@repo/helpers&#39;;
import { draftMode } from &#39;next/headers&#39;;
import { notFound } from &#39;next/navigation&#39;;
import * as blockComponents from &#39;~/blocks&#39;;
import { getPageContentById } from &#39;~/cms/helpers&#39;;
import OnPageEdit from &#39;~/components/draft/OnPageEdit&#39;;
import type { AllBlocks } from &#39;~/types&#39;;
import type { OptiPreviewPageProps } from &#39;~/types-legacy&#39;;

// Ensure this page is always rendered on-demand and never statically cached
export const revalidate = 0;
export const dynamic = &#39;force-dynamic&#39;;

export default async function PreviewPage({
  searchParams,
}: OptiPreviewPageProps) {
  const { isEnabled: isDraftModeEnabled } = await draftMode();
  if (!isDraftModeEnabled) {
    return notFound();
  }

  // Fetch the page content in draft mode using the provided search parameters  
  const page = await getPageContentById({
    id: searchParams.id,
    workId: searchParams.workId,
    locale: searchParams.locale,
    preview: true,
  });

  if (!page) return notFound();

  return (
    &amp;lt;&amp;gt;
      &amp;lt;OnPageEdit
        currentRoute={`/draft?${new URLSearchParams(searchParams).toString()}`}
        workId={searchParams.workId}
      /&amp;gt;
      
      {/* Render the page title with Optimizely&#39;s on-page editing attribute */}
      {page.title &amp;amp;&amp;amp; &amp;lt;h2 data-epi-edit=&quot;title&quot;&amp;gt;{page.title}&amp;lt;/h2&amp;gt;}
      
      &amp;lt;div data-epi-edit=&quot;mainContentArea&quot; is-on-page-editing-block-container=&quot;true&quot;&amp;gt;
        {renderBlocks&amp;lt;AllBlocks&amp;gt;(page.blocks, blockComponents, {
          showDataOnMissingBlock: true,
          arbitraryData: { searchParams },
          weave: [&#39;backgroundColor&#39;],
        })}
      &amp;lt;/div&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ensure that your &lt;span style=&quot;color: rgb(224, 62, 45);&quot;&gt;getPageContentById&lt;/span&gt;&amp;nbsp;function correctly handles the&amp;nbsp;&lt;span style=&quot;color: rgb(224, 62, 45);&quot;&gt;preview&lt;/span&gt;&amp;nbsp;flag. When&amp;nbsp;&lt;span style=&quot;color: rgb(224, 62, 45);&quot;&gt;preview&lt;/span&gt;&amp;nbsp;is&amp;nbsp;&lt;span style=&quot;color: rgb(224, 62, 45);&quot;&gt;true&lt;/span&gt;, you must use a different authentication method for Optimizely Graph to access draft content. Without this, fetching draft content will not work.&lt;/p&gt;
&lt;p&gt;For details on authenticating with Optimizely Graph, see the&amp;nbsp;&lt;a href=&quot;https://docs.developers.optimizely.com/platform-optimizely/docs/basic-auth&quot;&gt;official documentation&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This page layout uses the&amp;nbsp;&lt;span style=&quot;color: rgb(224, 62, 45);&quot;&gt;OnPageEdit &lt;/span&gt;component - the cherry on top that handles communication with the CMS.&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;// src/components/draft/OnPageEdit.tsx
&#39;use client&#39;;

import { useRouter } from &#39;next/navigation&#39;;
import { useEffect, useRef } from &#39;react&#39;;

interface EpiAPI {
  subscribe: (event: string, handler: (event: any) =&amp;gt; void) =&amp;gt; void;
  unsubscribe?: (event: string, handler: (event: any) =&amp;gt; void) =&amp;gt; void;
}

interface WindowWithEpi extends Window {
  epi?: EpiAPI;
}

interface ContentSavedProperty {
  name: string;
  value: string;
  successful: boolean;
  validationErrors: string;
}

interface ContentSavedEventArgs {
  contentLink: string;
  previewUrl: string;
  editUrl: string;
  properties: Array&amp;lt;ContentSavedProperty&amp;gt;;
}

interface OnPageEditProps {
  workId: string;
  currentRoute: string;
}

// This component listens for changes made in the Optimizely CMS editor and updates the preview of a page or block accordingly.
// When a change is made in the CMS, Optimizely emits a &quot;contentSaved&quot; event via the communicationinjector.js script.
// The event includes a list of updated properties. For significant changes, Optimizely may increment the workId of the edited content,
// returning the new workId as part of the content URL in the &quot;contentSaved&quot; event.

const OnPageEdit = ({
  workId,
  currentRoute,
}: OnPageEditProps) =&amp;gt; {
  const router = useRouter();

  // Listen for &quot;contentSaved&quot; events from the CMS editor, which may fire multiple times per change.
  // To avoid processing duplicate events, keep track of the last handled event and only process new ones.
  const prevMessageRef = useRef&amp;lt;ContentSavedEventArgs | null&amp;gt;(null);

  useEffect(() =&amp;gt; {
    // Handle content saved events from Optimizely CMS
    const handleContentSaved = (event: any) =&amp;gt; {
      const message = event as ContentSavedEventArgs;

      // Check if the event is a duplicate
      const prevMessage = prevMessageRef.current;
      if (
        prevMessage &amp;amp;&amp;amp;
        JSON.stringify(prevMessage) === JSON.stringify(message)
      ) {
        // Skip the event
        return;
      }
      prevMessageRef.current = message;

      // Since Optimizely Graph is external service, it may take some time to propagate the changes.
      // That&#39;s why simple &quot;refresh&quot; is not sufficient to provide seamless editing experience.
      // We need to look inside &quot;contentSaved&quot; event and update the DOM with the new content.
      message.properties?.forEach((prop) =&amp;gt; {
        if (!prop.successful) return;

        // Find matching elements in the DOM
        const elements = document.querySelectorAll&amp;lt;HTMLElement&amp;gt;(
          `[data-epi-edit=&quot;${prop.name}&quot;]`,
        );

        elements.forEach((el) =&amp;gt; {
          // Skip elements inside content areas that are marked as &quot;is-on-page-editing-block-container&quot;
          if (el.closest(&#39;[is-on-page-editing-block-container]&#39;)) {
            return;
          }
          
          // Replace text content
          el.textContent = prop.value;
        });
      });

      // When a major content change occurs, Optimizely increments the workId and provides a new draft URL.
      // If the workId has changed, update the URL to fetch the latest draft content from Graph.
      // Extract the new workId from the contentLink (format: &quot;id_workId&quot;).
      const [, newWorkId] = message?.contentLink?.split(&#39;_&#39;);

      if (newWorkId &amp;amp;&amp;amp; newWorkId !== workId) {
      
        const newUrl = currentRoute?.replace(
          `workId=${workId}`,
          `workId=${newWorkId}`,
        );
        router.push(newUrl);
      }
    };

    // -----------------------------------------------
    // Subscribe for Optimizely CMS events
    // -----------------------------------------------
    // Next.js pages are first server-rendered and then hydrated on the client.
    // window.epi (Optimizely API) may not exist immediately on hydration,
    // so we poll every 500ms until it becomes available.
    let interval: NodeJS.Timeout;

    const trySubscribe = () =&amp;gt; {
      const win = window as WindowWithEpi;
      if (win.epi) {
        win.epi.subscribe(&#39;contentSaved&#39;, handleContentSaved);
        clearInterval(interval); // Stop polling once subscribed
      }
    };

    interval = setInterval(trySubscribe, 500);
    trySubscribe(); // try immediately too

    // -----------------------------------------------
    // Cleanup
    // -----------------------------------------------
    // Avoid duplicate subscriptions or memory leaks
    return () =&amp;gt; {
      clearInterval(interval);

      const win = window as WindowWithEpi;
      if (win.epi) {
        win.epi.unsubscribe?.(&#39;contentSaved&#39;, handleContentSaved);
      }
    };
  }, [currentRoute, router, workId]);

  return null;
};

export default OnPageEdit;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;I hope this article has helped clarify how to implement on-page editing in your own headless Next.js solution. With these steps, you should be well-equipped to bring a seamless editing experience to your editors, combining the flexibility of headless architecture with the intuitive in-context editing of Optimizely CMS.&lt;/p&gt;
&lt;p&gt;Thanks for following along - happy building!&lt;/p&gt;
&lt;/div&gt;</id><updated>2025-09-10T09:52:20.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Going Headless - On-Page Editing with Optimizely Graph and Next.js</title><link href="https://michalmitas.com/blog/on-page-editing" /><id>How to enable on-page editing in your own solution using Next.js.</id><updated>2025-09-10T00:00:00.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Common Pitfalls with Search in Optimizely Graph - and How to Avoid Them</title><link href="https://world.optimizely.com/blogs/micha-mitas/dates/2025/6/common-pitfalls-with-search-in-optimizely-graph---and-how-to-avoid-them/" /><id>&lt;p&gt;Optimizely Graph offers powerful, flexible search capabilities out of the box, making it a popular choice for headless implementations. However, like any robust system, it comes with nuances that can trip you up if you&amp;rsquo;re not careful.&lt;/p&gt;
&lt;p&gt;In this post, I&amp;rsquo;ll try to cover&lt;strong&gt; common pitfalls developers may encounter when working with search in Optimizely Graph&lt;/strong&gt;, especially around content duplication, unintentional exposure, and content provider handling - and show you how to avoid them.&lt;/p&gt;
&lt;h2&gt;Duplicate Content in Search Results&lt;/h2&gt;
&lt;p&gt;One of the most frustrating issues is seeing the &lt;strong&gt;same content item indexed multiple times&lt;/strong&gt; or search results returning unexpected duplicates.&lt;/p&gt;
&lt;h3&gt;Incorrect Authentication Mode&lt;/h3&gt;
&lt;p&gt;Optimizely Graph exposes not only the published versions of your content but also includes &lt;strong&gt;drafts and other unpublished versions&lt;/strong&gt;. This is particularly useful for building custom on-page editing experiences in headless setups. However, it also introduces a potential risk:&lt;strong&gt; unpublished content may unintentionally appear in search results&lt;/strong&gt; if access controls are not correctly configured.&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;Recommended Solution&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;To mitigate this, it&#39;s crucial to separate authentication modes between editing and search functionalities:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;On-page editing should use authenticated access to retrieve drafts and work-in-progress content.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Public search should be limited strictly to published content and operate under a separate, read-only context.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Reusing tokens or authentication flows between these use cases should be strictly avoided.&lt;/p&gt;
&lt;p&gt;This may also be a potential security concern if not handled properly - but that&amp;rsquo;s a topic worthy of a separate article.&lt;/p&gt;
&lt;p&gt;For implementation guidance and best practices, refer to the official documentation:&lt;br /&gt;&lt;a href=&quot;https://docs.developers.optimizely.com/platform-optimizely/docs/authentication&quot;&gt;Authentication &amp;ndash; Optimizely Graph&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Shortcut Pages Misindexed&lt;/h3&gt;
&lt;p&gt;In Optimizely CMS, pages using &lt;strong&gt;Shortcuts&lt;/strong&gt; (e.g., &amp;ldquo;Fetch content from another page&amp;rdquo;, see&lt;a href=&quot;https://support.optimizely.com/hc/en-us/articles/4413192312077-All-Properties-editing-view#h_01HBH992DX0NCZW2W8SF86X50Z&quot;&gt; documentation&lt;/a&gt;) are &lt;strong&gt;by default indexed as standalone pages&lt;/strong&gt;, even though they point to other content. This behavior can result in &lt;strong&gt;unexpected duplicate entries &lt;/strong&gt;in your search results, which may negatively affect content relevance and SEO.&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;Recommended Solution&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;To prevent this, it&#39;s important to &lt;strong&gt;filter out shortcut pages&lt;/strong&gt; at the query level during indexing or search execution.&lt;/p&gt;
&lt;p&gt;A more robust and maintainable solution is to:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Introduce a custom property like ExcludeFromSearch&lt;strong&gt; &lt;/strong&gt;(boolean).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Automatically set this property to true for shortcut pages (e.g., in content events).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Respect this property in your search queries by excluding any pages where &lt;strong&gt;ExcludeFromSearch == true.&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This approach not only handles shortcut pages gracefully but also gives editors fine-grained control over which pages should be searchable.&lt;/p&gt;
&lt;h2&gt;Exposing Technical Content That Shouldn&#39;t Be Indexed&lt;/h2&gt;
&lt;p&gt;It&amp;rsquo;s not uncommon for &lt;strong&gt;non-public content &lt;/strong&gt;- such as settings pages, container pages, or system-level content-to inadvertently appear in Graph search results. These items were never intended to be publicly accessible and can clutter search experiences or expose internal information.&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;Common Causes&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Missing exclusion flags on special or restricted content.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Lack of visibility review during the Graph schema setup.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Overly broad queries that don&amp;rsquo;t filter by content purpose or type.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;How to Prevent It&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;Use Graph filters to explicitly exclude:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Content in designated folders (e.g. &lt;strong&gt;/settings&lt;/strong&gt;, &lt;strong&gt;/utility&lt;/strong&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Content with a flag like ExcludeFromSearch = true&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Specific content types not meant for search (e.g. &lt;strong&gt;SiteSettingsPage&lt;/strong&gt;, &lt;strong&gt;RedirectPage&lt;/strong&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;Best Practice&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;Implement an ExcludeFromSearch property on all content types. This offers &lt;strong&gt;low coupling &lt;/strong&gt;between search logic and CMS structure, and allows editors or developers to easily manage visibility without tightly binding filtering rules to folder paths or content type checks.&lt;/p&gt;
&lt;h2&gt;Multilingual Content in Optimizely Graph&lt;/h2&gt;
&lt;p&gt;In most cases, our content exists in multiple languages. However, a basic language filter in a Graph query may unintentionally exclude content that lacks a translation in the currently selected site language.&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;Example Scenario:&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;A user is browsing the site in German (&quot;de&quot;).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The CMS has a fallback language set to English (&quot;en&quot;).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Some product pages have not yet been translated into German.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;A basic Graph filter like Language: { Name: { eq: &quot;de&quot; } } will exclude these untranslated pages, even if fields like the product name are language-agnostic and would otherwise match the search criteria.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;Why This Matters:&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;Depending on your business needs, showing fallback content might be acceptable or preferred. However, even if you choose not to display it, it&amp;rsquo;s important to make that decision consciously, not by default behavior.&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;Recommended Approach:&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;To support fallback languages in search results, leverage the following Graph fields:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;MasterLanguage: Indicates the original language of the content.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ExistingLanguages: Lists all languages the content has been translated into.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By using these properties, you can intelligently include fallback content in queries-ensuring a better user experience while respecting localization strategy.&lt;/p&gt;
&lt;h2&gt;Poor Relevance and Ranking in Search Results&lt;/h2&gt;
&lt;p&gt;Even with a clean index and a well-designed Graph schema, search results can appear&lt;strong&gt; noisy, irrelevant, or misleading&lt;/strong&gt; if queries aren&amp;rsquo;t thoughtfully constructed. Common issues like &lt;strong&gt;missing field boosting, unfiltered related content&lt;/strong&gt;, and &lt;strong&gt;generic query logic&lt;/strong&gt; can cause even high-quality content to be buried beneath less relevant matches.&lt;/p&gt;
&lt;h3&gt;Missing field boosting&lt;/h3&gt;
&lt;p&gt;By default, all fields are treated equally in Optimizely Graph unless explicitly boosted. This often leads to situations where low-priority fields (e.g., descriptions) match as strongly as high-priority ones (e.g., titles or product names).&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;Recommendation:&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;Use field-level boosting to give higher weight to meaningful fields like title, name, or tags. Learn more:&lt;a href=&quot;https://docs.developers.optimizely.com/platform-optimizely/docs/boosting&quot;&gt; Boosting in Optimizely Graph&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Unintended indexing of related content&lt;/h3&gt;
&lt;p&gt;Product pages or articles often embed&lt;strong&gt; related content&lt;/strong&gt; (e.g., &quot;Related Products&quot; blocks or references). If this content is indexed directly with the main page, it can &lt;strong&gt;bleed into the searchable content &lt;/strong&gt;of the parent - leading to &lt;strong&gt;false positives&lt;/strong&gt;, confusing matches, and degraded search relevance.&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;Example:&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;A product page for a wireless mouse might appear in search results for a mechanical keyboard if a related keyboard product is embedded and indexed as part of the same content.&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;Recommendation:&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;Ensure related items are either:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Excluded from the index&lt;/strong&gt;, or&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Indexed separately and referenced cleanly&lt;/strong&gt;, rather than merged into the searchable body of the main item.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Access-Controlled Content Leaking into Public Search&lt;/h2&gt;
&lt;p&gt;In many CMS implementations, certain content such as &lt;strong&gt;gated resources, subscriber-only pages&lt;/strong&gt;, or&lt;strong&gt; internal documentation &lt;/strong&gt;should only be accessible to specific user groups. A &lt;strong&gt;critical security risk&lt;/strong&gt; occurs when this restricted content unintentionally appears in &lt;strong&gt;public search results&lt;/strong&gt;, exposing sensitive or paid information to unauthorized users.&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;Common Cause:&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Incorrect authentication mode&lt;/strong&gt; used when querying Optimizely Graph, resulting in unrestricted access to protected content.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;How to Prevent It:&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;To ensure proper access control:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;When authenticating to Optimizely Graph, always include the &lt;strong&gt;current user context&lt;/strong&gt; and their &lt;strong&gt;roles/permissions&lt;/strong&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Avoid using system-wide tokens or anonymous access for features that should reflect user-level content restrictions.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Optimizely Graph enforces access control only when explicitly requested via authenticated queries. If omitted, all content-including restricted items-may be returned.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.developers.optimizely.com/platform-optimizely/docs/basic-auth-from-backend#retrieve-restricted-content&quot;&gt;How to retrieve restricted content securely (Optimizely Docs)&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;Optimizely Graph is a powerful tool but like any flexible system, it needs intentional setup and governance to avoid messy search results and content exposure issues.&lt;/p&gt;
&lt;p&gt;By being aware of the common pitfalls and applying proactive filters, schema hygiene, and provider discipline, you&amp;rsquo;ll set yourself (and your editors!) up for a clean, accurate, and efficient search experience.&lt;/p&gt;
&lt;p&gt;Have you run into a tricky Graph issue in production? Let me know - always keen to learn how others are solving these challenges.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</id><updated>2025-06-18T14:16:05.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Common Pitfalls with Search in Optimizely Graph - and How to Avoid Them</title><link href="https://michalmitas.com/blog/optimizely-graph-search" /><id>Common pitfalls developers encounter when working with search in Optimizely Graph and how to avoid them.</id><updated>2025-06-18T00:00:00.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Going Headless: Optimizely Graph vs Content Delivery API</title><link href="https://world.optimizely.com/blogs/micha-mitas/dates/2025/4/going-headless-optimizely-graph-vs-content-delivery-api/" /><id>&lt;p&gt;As the demand for flexible, multi-channel digital experiences grows, more development teams are turning to&amp;nbsp;&lt;strong&gt;headless CMS architectures&lt;/strong&gt; to future-proof their solutions. Optimizely offers two primary options for headless content delivery: the &lt;strong&gt;Content Delivery API (CDA)&lt;/strong&gt; and the newer &lt;strong&gt;Optimizely Graph&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;But which one should you choose - and why?&lt;/p&gt;
&lt;p&gt;I recently faced this exact question while starting a greenfield project with a headless approach. During my research, I noticed that most resources focused deeply on either CDA or Graph individually, but very few compared the two in a practical, side-by-side way.&lt;/p&gt;
&lt;p&gt;In this article, I&amp;rsquo;ll share what I learned -&amp;nbsp;highlighting the &lt;strong&gt;key differences in performance, scalability, routing, search&lt;/strong&gt;, and more. If you&#39;re planning to build a headless or hybrid app with Optimizely CMS and you&#39;re still unsure which path to take, this guide is for you.&lt;/p&gt;
&lt;h3&gt;What Does &amp;ldquo;Headless&amp;rdquo; mean and why is it such a hot topic recently?&lt;/h3&gt;
&lt;p&gt;Before we dive in, let&amp;rsquo;s quickly define &amp;ldquo;headless&amp;rdquo; and clarify what the fuss is all about!&lt;/p&gt;
&lt;p&gt;In a &lt;strong&gt;non-headless&lt;/strong&gt; CMS, templates, rendering logic, and UI all live in one application:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://lh7-rt.googleusercontent.com/docsz/AD_4nXcbNPCzRCnpcL6yVMwQuMvnr8Z-aM3ZH8JVkHDTwt_QghHJiNn2WY1HbRVTJbzr-AWQ3wuUQpYSWvxrLO1cdrtXjdYZ89YSaVDxMxPjoDKZbfIndtNysR6N6q0FPtKIRrJVp4quEw?key=3mOZlVv2ZwJhbbBj73lCAg&quot; width=&quot;602&quot; height=&quot;281&quot; /&gt;&lt;/p&gt;
&lt;p&gt;In more complex projects, this approach may introduce a high degree of coupling between the back-end and front-end layers, potentially reducing flexibility.&lt;/p&gt;
&lt;p&gt;In a &lt;strong&gt;headless &lt;/strong&gt;CMS, the front-end is handled by a separate application. Content is created and stored in the backend but delivered through APIs:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://lh7-rt.googleusercontent.com/docsz/AD_4nXfeR8j6LwckEhi8lrAw5ONIl3hWKSk7LcAnj63lbaXFatvyU0DWtrQ2m_gA9Ni1nv4i5LL494kzrG4gh5Qi7OF1fp5zRY74irCtQJUJq9qbnj8KiJhbhjAkvHN9g10FMqmtjEdr0Q?key=3mOZlVv2ZwJhbbBj73lCAg&quot; width=&quot;602&quot; height=&quot;476&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This gives us many advantages that we can utilize to develop more scalable and resilient solutions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Front-End Flexibility&lt;/strong&gt;: Use any technology (React, Vue, Next.js, etc.) to build modern, fast, and dynamic user experiences.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Omnichannel Delivery&lt;/strong&gt;: Reuse the same content across websites, mobile apps, kiosks, smart devices, and more.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Scalability &amp;amp; Performance&lt;/strong&gt;: Decoupling content delivery from the CMS backend allows for independent scaling and optimized performance.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Faster Development Cycles&lt;/strong&gt;: Backend and frontend teams can work in parallel and deploy changes separately accelerating time-to-market.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Future-Proof Architecture&lt;/strong&gt;: Easier to swap out or upgrade parts of your stack without replatforming the entire system.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But there&amp;rsquo;s always a second side of a coin. While going &amp;ldquo;headless&amp;rdquo; gives us a great flexibility it also introduces new challenges - especially around content delivery, routing, and performance. Let&amp;rsquo;s look at how &lt;strong&gt;Optimizely Graph&lt;/strong&gt; and &lt;strong&gt;Content Delivery API &lt;/strong&gt;help solve those.&lt;/p&gt;
&lt;h3&gt;Content delivery &amp;amp; performance&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Content Delivery API&lt;/strong&gt; is a well established feature of Optimizely PaaS CMS that can be used to fetch data about content. It&amp;rsquo;s installed as a NuGet package and easily configured - &lt;a href=&quot;https://docs.developers.optimizely.com/content-management-system/v1.5.0-content-delivery-api/docs/quick-start&quot;&gt;Content Delivery API - Quickstart&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://lh7-rt.googleusercontent.com/docsz/AD_4nXf7DLhLfoSlKwP0RjCUC6Zj7YMIn-oUtXeMdqB9jaB83-uRQ3s70X72uXOuvoIbS4h4w49QOtkQUeeO2nPNLpkbUMpgGAtAVVySvyhGATy7OkaEBW9IemV3uVAwhiQHz84t9CJWrA?key=3mOZlVv2ZwJhbbBj73lCAg&quot; width=&quot;602&quot; height=&quot;476&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Optimizely Graph &lt;/strong&gt;extends this concept by introducing an additional layer that operates independently of your CMS instance-similar to how&lt;strong&gt; Optimizely Search &amp;amp; Navigation&lt;/strong&gt; works. It pulls content from your CMS using the Content Delivery API and builds a &lt;strong&gt;GraphQL-powered index&lt;/strong&gt; that serves as the foundation for flexible and efficient content delivery.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://lh7-rt.googleusercontent.com/docsz/AD_4nXd-lkukdRC-fHj-upi0YW_TPFLjyz2Gh4yubEm8sJLkoUTyVJf8g8WC35A4Wp9-etxrlnhD7AyjRtwFqkz3e9YugQZnsj5loEoqJS5Pl-6I-vchaat0oCzBsFV7gtq-JLBF1LQY?key=3mOZlVv2ZwJhbbBj73lCAg&quot; width=&quot;602&quot; height=&quot;660&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This difference affects how data is being fetched and has a &lt;strong&gt;huge impact on scaling possibilities&lt;/strong&gt; of your solution. While using only Content Delivery API your CMS must scale with your traffic. &lt;strong&gt;More frontend traffic = more load on your CMS&lt;/strong&gt;. For more demanding sites this might be very price inefficient from both CMS licensing and infrastructure costs perspective.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://lh7-rt.googleusercontent.com/docsz/AD_4nXee5qNpXTlSDDxXa3wzZrILySd-slqBlvhN7LItWTH-uaW0KkRNsCDXo0suiaBo0j6YEkkQ_pcja2nAEVQTRmNPzskQB2e66VaaTRxiT9tiUWgIHw_biX93DB8RQb-CwZa-CTf7?key=3mOZlVv2ZwJhbbBj73lCAg&quot; width=&quot;602&quot; height=&quot;584&quot; /&gt;&lt;/p&gt;
&lt;p&gt;With &lt;strong&gt;Optimizely Graph&lt;/strong&gt; requests go to an &lt;strong&gt;external, managed service.&lt;/strong&gt; Your CMS publishes content to the Graph service, and that service handles scaling independently. This &lt;strong&gt;offloads traffic from your CMS &lt;/strong&gt;and improves resilience.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://lh7-rt.googleusercontent.com/docsz/AD_4nXcupJT5qQWToIIPdwPxd2-3L9toXp4sxLrEDaKTQensjs7TxObJo77wZjAgivv80v9ADZNHxHOZqzKgbdIfpByhxFK1ABeYH7LOrq52MAD9njgcLB08HMpRh_2TF3CDQS5lXOZK?key=3mOZlVv2ZwJhbbBj73lCAg&quot; width=&quot;602&quot; height=&quot;689&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Avoiding Data Over-Fetching&lt;/h3&gt;
&lt;p&gt;Reducing the size and number of requests matters in any modern frontend. A typical page might include nested blocks and related media - and fetching that efficiently is key.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Content Delivery API&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;In some cases you&amp;rsquo;ll need to &lt;strong&gt;customize your CDA responses&lt;/strong&gt; to expand data structures (&lt;a href=&quot;https://support.optimizely.com/hc/en-us/articles/31961628652173-Expanding-Nested-Block-for-Content-Delivery-API-v3-CMS-12&quot;&gt;Expanding Nested Blocks &amp;ndash; Optimizely Docs&lt;/a&gt;) or enrich data being returned&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;It works, but requires effort and custom configuration.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Optimizely Graph&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;GraphQL&amp;rsquo;s design shines here.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;You can define exactly &lt;strong&gt;which fields, blocks, and media&lt;/strong&gt; you want in one query.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Great for reducing payloads and avoiding over-fetching.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Routing&lt;/h3&gt;
&lt;p&gt;Regardless of the front-end framework you&amp;rsquo;re responsible for routing in a headless setup. You need to map frontend routes to your Optimizely content structure.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Content Delivery API&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Supports built-in URL resolution via:&lt;br /&gt;&lt;br /&gt;&lt;code&gt;/api/episerver/v3.0/content?contentUrl=/about-us&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This makes it easier to match CMS structure without writing custom logic.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Optimizely Graph&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;You can use the ContentLink property, which is indexed by Optimizely Graph by default, to locate the page that matches a given URL.&lt;/p&gt;
&lt;p&gt;Query:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;SitePageData(
    where: {
      ContentLink: {
        Url: {
          eq: &quot;url-to-your-page&quot;
        }
      }
    }
  )&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Response:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;&quot;data&quot;: {
    &quot;SiteBasePage&quot;: {
      &quot;items&quot;: [
        {
          &quot;ContentLink&quot;: {
            &quot;Url&quot;: &quot;/en/exploring-the-future-of-education-the-rise-of-online-learning-platforms/&quot;
          }
        }
      ]
    }
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Alternatively you could implement similar solution as in Optimizely SaaS where each page contains a metadata Url property:&lt;/p&gt;
&lt;p&gt;Query:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;BlogPostPage {
  items {
    _metadata {
      url {
        default
        base
        graph
        hierarchical
        internal
        type
      }
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Response:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;&quot;data&quot;: {
    &quot;BlogPostPage&quot;: {
      &quot;items&quot;: [
        {
          &quot;_metadata&quot;: {
            &quot;url&quot;: {
              &quot;default&quot;: &quot;/insights/the-benefits-of-downsizing-in-retirement/&quot;,
              &quot;base&quot;: &quot;https://&amp;lt;your-site-base-url&amp;gt;&quot;,
              &quot;graph&quot;: &quot;graph://cms/BlogPostPage/5964702069e64c42ae151861858f0aa7&quot;,
              &quot;hierarchical&quot;: &quot;/insights/the-benefits-of-downsizing-in-retirement/&quot;,
              &quot;internal&quot;: &quot;cms://content/5964702069e64c42ae151861858f0aa7?loc=en&amp;amp;ver=20&quot;,
              &quot;type&quot;: &quot;HIERARCHICAL&quot;
            }
          }
        }
      ]
    }
  }&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;On-Page Editing&lt;/h3&gt;
&lt;p&gt;One of the standout features of Optimizely CMS is its on-page editing, which allows content authors to see changes live as they work. However, in a fully headless setup, this editing experience doesn&amp;rsquo;t come for free.&lt;/p&gt;
&lt;p&gt;In both &lt;strong&gt;Content Delivery API &lt;/strong&gt;and &lt;strong&gt;Optimizely Graph &lt;/strong&gt;approaches,&lt;strong&gt; on-page editing requires custom implementation&lt;/strong&gt; to integrate with the CMS editor and preview modes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Content Delivery API&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Using CDA, you can support on-page editing by configuring your front-end app to detect Optimizely&amp;rsquo;s Edit Mode and handle it accordingly. Key requirements include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Supporting query parameters like &lt;code&gt;?epieditmode=true and ?id=123&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Resolving content in preview or draft mode&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Ensuring that URLs resolve properly to content in CMS edit view&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A helpful reference implementation can be found here: &lt;a href=&quot;https://hacksbyme.net/2023/06/13/on-page-editing-with-optimizely-cms-on-an-externally-hosted-site&quot;&gt;On-page editing with Optimizely CMS on an externally hosted site&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This setup allows content editors to use the familiar CMS UI while seeing a live rendering of content changes on your front-end app - provided it&#39;s properly connected via CDA.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Optimizely Graph&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Optimizely Graph also supports on-page editing, but in a slightly different way. The key concept is that &lt;strong&gt;Graph receives published and preview content from the CMS&lt;/strong&gt;, and you can query that using specific parameters or endpoints.&lt;/p&gt;
&lt;p&gt;To enable on-page editing, you&amp;rsquo;ll need to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Configure your front-end to recognize CMS editor states&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Support preview tokens or versions (e.g., previewing unpublished content)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Adjust GraphQL queries to fetch content in preview mode&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Detailed documentation and setup guide available here:&amp;nbsp;&lt;a href=&quot;https://docs.developers.optimizely.com/platform-optimizely/v1.4.0-optimizely-graph/docs/on-page-editing-using-content-graph&quot;&gt;On-page editing using Optimizely Graph&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;While Graph adds more flexibility in querying and rendering, it also requires a deeper understanding of how preview states are handled across the CMS and Graph layers.&lt;strong&gt;&amp;nbsp;&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;Search Capabilities&lt;/h3&gt;
&lt;p&gt;Both Optimizely Graph and the Content Delivery API support search, but the &lt;strong&gt;mechanisms and features differ significantly&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Content Delivery API&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Built on &lt;strong&gt;Search &amp;amp; Navigation&lt;/strong&gt; (formerly Find)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Supports &lt;strong&gt;free-text search&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Doesn&amp;rsquo;t support semantic search&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Enables &lt;strong&gt;personalized search results&lt;/strong&gt; (visitor groups)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;API offers only&lt;strong&gt; basic filtering and ordering.&lt;/strong&gt; To utilize more sophisticated features like boosting a custom implementation of search API and &lt;strong&gt;Search &amp;amp; Navigation&lt;/strong&gt; on the BE side is needed.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Optimizely Graph&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Doesn&amp;rsquo;t require Search &amp;amp; Navigation&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Supports free-text and semantic search&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Offers advanced search out-of-the-box (boosting, filters, autocomplete, and more)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;No personalization support &lt;/strong&gt;(as of now)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Offers greater flexibility, as search can be implemented on either the front-end or back-end, depending on your architecture.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Licensing&lt;/h3&gt;
&lt;p&gt;Licensing costs will ultimately depend on your Optimizely contract, but there are some &lt;strong&gt;structural differences&lt;/strong&gt; worth noting when choosing between CDA and Graph.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Content Delivery API&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;May require a &lt;strong&gt;separate license &lt;/strong&gt;for &lt;strong&gt;Search &amp;amp; Navigation&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;More frontend traffic usually means &lt;strong&gt;more CMS app instances&lt;/strong&gt; &amp;rarr; &lt;strong&gt;higher infrastructure costs &lt;/strong&gt;for self-hosted solutions&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;More CMS instances = potentially more base licenses required&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Optimizely Graph&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;May require a &lt;strong&gt;separate license&lt;/strong&gt; for &lt;strong&gt;Optimizely Graph&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Content is delivered via a &lt;strong&gt;scalable managed service&lt;/strong&gt;, reducing the load (and cost) on the CMS&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;No need for Search &amp;amp; Navigation license&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Can reduce the number of required CMS licenses, especially in high-traffic apps&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Summary&lt;/h3&gt;
&lt;div align=&quot;left&quot;&gt;
&lt;table style=&quot;width: 43.3987%; height: 439.532px;&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 47.5938px;&quot;&gt;
&lt;td style=&quot;width: 26.2349%; height: 47.5938px;&quot;&gt;
&lt;p&gt;&lt;strong&gt;Feature&lt;/strong&gt;&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 41.5832%; height: 47.5938px;&quot;&gt;
&lt;p&gt;&lt;strong&gt;Content Delivery API (CDA)&lt;/strong&gt;&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 32.1243%; height: 47.5938px;&quot;&gt;
&lt;p&gt;&lt;strong&gt;Optimizely Graph&lt;/strong&gt;&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 47.5938px;&quot;&gt;
&lt;td style=&quot;width: 26.2349%; height: 47.5938px;&quot;&gt;
&lt;p&gt;API Type&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 41.5832%; height: 47.5938px;&quot;&gt;
&lt;p&gt;REST&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 32.1243%; height: 47.5938px;&quot;&gt;
&lt;p&gt;GraphQL&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 47.5938px;&quot;&gt;
&lt;td style=&quot;width: 26.2349%; height: 47.5938px;&quot;&gt;
&lt;p&gt;Scaling&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 41.5832%; height: 47.5938px;&quot;&gt;
&lt;p&gt;Tied to CMS instance&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 32.1243%; height: 47.5938px;&quot;&gt;
&lt;p&gt;External managed service&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 47.5938px;&quot;&gt;
&lt;td style=&quot;width: 26.2349%; height: 47.5938px;&quot;&gt;
&lt;p&gt;Routing&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 41.5832%; height: 47.5938px;&quot;&gt;
&lt;p&gt;Built-in URL resolution&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 32.1243%; height: 47.5938px;&quot;&gt;
&lt;p&gt;Custom slug/url-based&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 67.1875px;&quot;&gt;
&lt;td style=&quot;width: 26.2349%; height: 67.1875px;&quot;&gt;
&lt;p&gt;Nested Content Fetch&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 41.5832%; height: 67.1875px;&quot;&gt;
&lt;p&gt;Custom config required&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 32.1243%; height: 67.1875px;&quot;&gt;
&lt;p&gt;Native via query&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 47.5938px;&quot;&gt;
&lt;td style=&quot;width: 26.2349%; height: 47.5938px;&quot;&gt;
&lt;p&gt;Search&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 41.5832%; height: 47.5938px;&quot;&gt;
&lt;p&gt;Search &amp;amp; Navigation (optional)&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 32.1243%; height: 47.5938px;&quot;&gt;
&lt;p&gt;Built-in, semantic&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 47.5938px;&quot;&gt;
&lt;td style=&quot;width: 26.2349%; height: 47.5938px;&quot;&gt;
&lt;p&gt;Personalization&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 41.5832%; height: 47.5938px;&quot;&gt;
&lt;p&gt;Supported&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 32.1243%; height: 47.5938px;&quot;&gt;
&lt;p&gt;Not supported&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 67.1875px;&quot;&gt;
&lt;td style=&quot;width: 26.2349%; height: 67.1875px;&quot;&gt;
&lt;p&gt;On-page Editing&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 41.5832%; height: 67.1875px;&quot;&gt;
&lt;p&gt;Supported with setup&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 32.1243%; height: 67.1875px;&quot;&gt;
&lt;p&gt;Supported with more setup&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Content Delivery API&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Pros:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Popular and well known REST approach&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Easier routing with built-in URL resolution&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Doesn&amp;rsquo;t require additional licenses&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Cons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Need to scale with your CMS&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Custom configuration required for some data fetching scenarios&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Optimizely Graph&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Pros:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Query exactly what you need&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;One request for nested blocks, media, etc.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;More efficient, flexible, and scalable&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Great for modern front-end frameworks&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;Cons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;More complex routing based on content url or slug.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;May require separate license&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Final Thoughts&lt;/h3&gt;
&lt;p&gt;Going headless with Optimizely unlocks a lot of freedom and flexibility. Whether you choose CDA or Optimizely Graph - you&#39;re in a great place to build fast, modern, and scalable experiences.&lt;/p&gt;
&lt;p&gt;Got questions or experiences with either? I&amp;rsquo;d love to hear your take!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;br /&gt;&lt;br /&gt;&lt;/strong&gt;&lt;/p&gt;</id><updated>2025-06-02T09:15:50.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Going Headless - Optimizely Graph vs Content Delivery API</title><link href="https://michalmitas.com/blog/graph-vs-cda" /><id>Comparison of Optimizely Graph vs Content Delivery API for headless CMS delivery.</id><updated>2025-06-02T00:00:00.0000000Z</updated><summary type="html">Blog post</summary></entry></feed>