<?xml version="1.0" encoding="utf-8"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/"><channel><language>en</language><title>Blog posts by Mardy Abdelkareem</title> <link>https://world.optimizely.com/blogs/mardy-abdelkareem/</link><description></description><ttl>60</ttl><generator>Optimizely World</generator><item> <title>Introducing Publishing Timeline for Optimizely CMS 13</title>            <link>https://world.optimizely.com/blogs/mardy-abdelkareem/dates/2026/10/introducing-publishing-timeline-for-optimizely-cms-13/</link>            <description>&lt;p&gt;When working with scheduled content in Optimizely CMS, having a clear overview of what&#39;s going live and when can be really helpful, especially when managing content across multiple languages and timezones.&lt;/p&gt;
&lt;p&gt;Following my &lt;a style=&quot;color: #0066cc; text-decoration: underline;&quot; href=&quot;/link/ac89958d132547f9bf5ebfb16e4de0b4.aspx&quot;&gt;previous add-on for scheduling content across different timezones&lt;/a&gt;, I wanted to explore another part of the publishing experience: making it easier for editors to see what&#39;s coming up.&lt;/p&gt;
&lt;p&gt;That led me to build &lt;strong&gt;Publishing Timeline&lt;/strong&gt;, a free, open-source add-on for Optimizely CMS 13.&lt;/p&gt;
&lt;h2&gt;What does Publishing Timeline do?&lt;/h2&gt;
&lt;p&gt;Publishing Timeline adds a panel below the native &lt;strong&gt;Workflow Status&lt;/strong&gt; gadget on the CMS Dashboard. It appears when the &lt;strong&gt;Scheduled&lt;/strong&gt; tab is selected, without replacing or modifying the existing gadget.&lt;/p&gt;
&lt;p&gt;It provides two ways to explore upcoming publications.&lt;/p&gt;
&lt;h3&gt;Table view&lt;/h3&gt;
&lt;p&gt;The table provides a list of scheduled content, including the publishing date, language, and who last modified each item. Editors can filter the results and open content directly in the CMS editor.&lt;/p&gt;
&lt;figure style=&quot;max-width: 900px; margin: 20px auto; text-align: center;&quot;&gt;&lt;img style=&quot;display: block; width: 100%; height: auto; border: 1px solid #e2e2e2; border-radius: 6px;&quot; src=&quot;/link/98e878a2e8814d1ba6ae63a22a45cafb.aspx&quot; alt=&quot;Publishing Timeline table showing scheduled content, languages, publishing dates, modified by, and edit links.&quot; /&gt;
&lt;figcaption style=&quot;margin-top: 8px; color: #666; font-size: 13px;&quot;&gt;Publishing Timeline &amp;mdash; Table view&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h3&gt;Calendar view&lt;/h3&gt;
&lt;p&gt;The calendar displays scheduled publications in a monthly view, making it easier to see what&#39;s planned for a particular day or across the month. When multiple items are scheduled for the same day, the &lt;strong&gt;+N more&lt;/strong&gt; option opens a popup showing additional publications and their scheduled times.&lt;/p&gt;
&lt;figure style=&quot;max-width: 900px; margin: 20px auto; text-align: center;&quot;&gt;&lt;img style=&quot;display: block; width: 100%; height: auto; border: 1px solid #e2e2e2; border-radius: 6px;&quot; src=&quot;/link/d59c9a8a43b645e29e968324710c4e68.aspx&quot; alt=&quot;Publishing Timeline monthly calendar showing scheduled publications and a popup with additional scheduled items.&quot; /&gt;
&lt;figcaption style=&quot;margin-top: 8px; color: #666; font-size: 13px;&quot;&gt;Publishing Timeline &amp;mdash; Calendar view with additional scheduled items&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Both views share the same language, date, and timezone filters, so editors can switch between them without losing their selections.&lt;/p&gt;
&lt;h2&gt;Working across timezones&lt;/h2&gt;
&lt;p&gt;One of the features I wanted to include was the ability to view scheduled publishing times in different timezones.&lt;/p&gt;
&lt;p&gt;For example, if you&#39;re coordinating content across Cairo, London, and New York, you can switch the timeline to see when each item is scheduled to go live in the selected timezone.&lt;/p&gt;
&lt;p&gt;This only changes how the publishing times are displayed. The actual publishing schedule remains unchanged.&lt;/p&gt;
&lt;h2&gt;Getting started&lt;/h2&gt;
&lt;p&gt;Publishing Timeline supports &lt;strong&gt;Optimizely CMS 13.4.0 and later (below 14.0.0)&lt;/strong&gt; and .NET 10.&lt;/p&gt;
&lt;p&gt;The add-on is now available on the &lt;a style=&quot;color: #0066cc; text-decoration: underline;&quot; href=&quot;https://nuget.optimizely.com/packages/mardy.optimizely.publishingtimeline/1.0.0&quot;&gt;Optimizely NuGet feed&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Install the package using the .NET CLI:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dotnet add package Mardy.Optimizely.PublishingTimeline --version 1.0.0&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Make sure the Optimizely NuGet feed is configured as a package source in your project or development environment.&lt;/p&gt;
&lt;p&gt;No additional startup configuration is required. After installation, open the CMS Dashboard and navigate to &lt;strong&gt;Workflow Status &amp;rarr; Scheduled&lt;/strong&gt;. The Publishing Timeline panel will appear below the native gadget.&lt;/p&gt;
&lt;p&gt;The add-on is read-only and doesn&#39;t change how Optimizely schedules or publishes content.&lt;/p&gt;
&lt;h2&gt;Source code and feedback&lt;/h2&gt;
&lt;p&gt;The project is open source under the MIT license.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a style=&quot;color: #0066cc; text-decoration: underline;&quot; href=&quot;https://github.com/mardy-awd/Optimizely.PublishingTimeline&quot;&gt;Mardy.Optimizely.PublishingTimeline&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;NuGet package:&lt;/strong&gt; &lt;a style=&quot;color: #0066cc; text-decoration: underline;&quot; href=&quot;https://nuget.optimizely.com/packages/mardy.optimizely.publishingtimeline/1.0.0&quot;&gt;Optimizely NuGet feed (v1.0.0)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I&#39;d be happy to hear your feedback or ideas for future improvements!&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/mardy-abdelkareem/dates/2026/10/introducing-publishing-timeline-for-optimizely-cms-13/</guid>            <pubDate>Mon, 02 Nov 2026 09:36:42 GMT</pubDate>           <category>Blog post</category></item><item> <title>Time Zone-Aware Scheduled Publishing in Optimizely CMS 13</title>            <link>https://world.optimizely.com/blogs/mardy-abdelkareem/dates/2026/10/time-zone-aware-scheduled-publishing-in-optimizely-cms-13/</link>            <description>&lt;p&gt;Scheduling content for audiences in different time zones is a common requirement, especially for websites managed by editors across multiple regions.&lt;/p&gt;
&lt;p&gt;Optimizely CMS already supports scheduled publishing, but editors working across time zones may need to calculate the equivalent time in their own time zone before scheduling content.&lt;/p&gt;
&lt;p&gt;For example, an editor in Cairo might want a page to go live at 5:00 PM in London. Without a target time-zone selector, the editor needs to calculate the corresponding local time, taking daylight saving time changes into account.&lt;/p&gt;
&lt;p&gt;To simplify this workflow, I built &lt;strong&gt;Time Zone Publishing&lt;/strong&gt;, a lightweight, open-source add-on for Optimizely CMS 13.&lt;/p&gt;
&lt;h2&gt;How it works&lt;/h2&gt;
&lt;p&gt;The add-on introduces a new &lt;strong&gt;Schedule Publish by Time Zone&lt;/strong&gt; command alongside the existing scheduling option in the CMS Publish menu.&lt;/p&gt;
&lt;figure style=&quot;margin: 24px auto; text-align: center;&quot;&gt;
&lt;figcaption style=&quot;margin-top: 8px; font-size: 14px; color: #666; line-height: 1.5;&quot;&gt;&lt;img src=&quot;/link/0df4bf9ef957473d91f8607906568429.aspx&quot; /&gt;&lt;br /&gt;Figure 1: The new scheduling command in the native CMS Publish menu.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Editors can access the feature directly from the familiar publishing interface without navigating to a separate tool.&lt;/p&gt;
&lt;p&gt;Selecting the command opens a scheduling dialog where editors can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Search for and select an IANA time zone.&lt;/li&gt;
&lt;li&gt;Choose the intended publication date and time in that zone.&lt;/li&gt;
&lt;li&gt;See the equivalent time in their browser&#39;s local time zone.&lt;/li&gt;
&lt;li&gt;Confirm the schedule using the existing CMS publishing mechanism.&lt;/li&gt;
&lt;/ul&gt;
&lt;figure style=&quot;margin: 24px auto; text-align: center;&quot;&gt;
&lt;figcaption style=&quot;margin-top: 8px; font-size: 14px; color: #666; line-height: 1.5;&quot;&gt;&lt;img src=&quot;/link/4422c9d809354239aa0f2fa5bee5a52c.aspx&quot; /&gt;&lt;br /&gt;Figure 2: Scheduling publication in London with a local-time preview for Cairo.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;In this example, the page is scheduled for &lt;strong&gt;October 30, 2026, at 5:00 PM in London&lt;/strong&gt;, which corresponds to &lt;strong&gt;7:00 PM in Cairo&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Once the editor confirms the schedule, the add-on converts the selected local date and time to UTC and uses the native CMS scheduled publishing functionality.&lt;/p&gt;
&lt;p&gt;This keeps the scheduling workflow aligned with the platform rather than introducing a separate background job or publishing service.&lt;/p&gt;
&lt;h2&gt;Handling daylight saving time&lt;/h2&gt;
&lt;p&gt;One important consideration when scheduling across time zones is daylight saving time (DST).&lt;/p&gt;
&lt;p&gt;During DST transitions, certain local times may be:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Nonexistent:&lt;/strong&gt; When clocks move forward and skip a period of time.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ambiguous:&lt;/strong&gt; When clocks move backward and a local time occurs twice.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The add-on validates both scenarios and prevents editors from confirming a schedule with an invalid or ambiguous local time.&lt;/p&gt;
&lt;h2&gt;Key features&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Schedule publishing using a selected target time zone.&lt;/li&gt;
&lt;li&gt;Searchable IANA time-zone selection.&lt;/li&gt;
&lt;li&gt;Automatic conversion to UTC.&lt;/li&gt;
&lt;li&gt;Preview of the equivalent browser-local time.&lt;/li&gt;
&lt;li&gt;Validation of daylight saving time transitions.&lt;/li&gt;
&lt;li&gt;Integration with the native CMS Publish menu.&lt;/li&gt;
&lt;li&gt;No additional scheduling service or background job required.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Compatibility and installation&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Time Zone Publishing v0.1.1&lt;/strong&gt; targets &lt;strong&gt;.NET 10&lt;/strong&gt; and supports &lt;strong&gt;Optimizely CMS 13.0.0 or later&lt;/strong&gt;. The add-on has been successfully tested with &lt;strong&gt;CMS 13.0.0 and 13.4.0&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The add-on is available on the &lt;strong&gt;Optimizely NuGet feed&lt;/strong&gt; and can be installed directly into an existing CMS 13 application.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Installation:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;dotnet add package Mardy.Optimizely.TimeZonePublishing --version 0.1.1&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Source code&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a style=&quot;color: #0066cc; text-decoration: underline;&quot; href=&quot;https://github.com/mardy-awd/Optimizely.TimeZonePublishing&quot;&gt;Optimizely.TimeZonePublishing&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;NuGet package:&lt;/strong&gt; &lt;a style=&quot;color: #0066cc; text-decoration: underline;&quot; href=&quot;https://nuget.optimizely.com/packages/mardy.optimizely.timezonepublishing/0.1.1&quot;&gt;Optimizely NuGet feed&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The add-on is designed for CMS 13 and is not intended for direct installation into CMS 12 applications.&lt;/p&gt;
&lt;p&gt;Feedback, suggestions, and contributions are welcome.&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/mardy-abdelkareem/dates/2026/10/time-zone-aware-scheduled-publishing-in-optimizely-cms-13/</guid>            <pubDate>Mon, 26 Oct 2026 07:02:50 GMT</pubDate>           <category>Blog post</category></item><item> <title>Exploring Cached Templates and Cursor Pagination in the Optimizely Graph C# SDK</title>            <link>https://world.optimizely.com/blogs/mardy-abdelkareem/dates/2026/10/exploring-cached-templates-and-cursor-pagination-in-the-optimizely-graph-c-sdk/</link>            <description>&lt;p&gt;While experimenting with the&amp;nbsp;&lt;strong&gt;Optimizely Graph C# SDK (13.3.0)&lt;/strong&gt; in my local &lt;strong&gt;Optimizely CMS 13 Alloy&lt;/strong&gt; project, I came across two interesting findings: how cached templates are handled by the SDK, and how cursor-based pagination differs between typed and untyped queries.&lt;/p&gt;
&lt;p&gt;I used &lt;strong&gt;ILSpy&lt;/strong&gt; to inspect the SDK implementation and &lt;strong&gt;Fiddler&lt;/strong&gt; to verify the outgoing HTTP requests.&lt;/p&gt;
&lt;p&gt;Here are my findings.&lt;/p&gt;
&lt;h2&gt;1. Cached Templates: What Does Enabled by Default Mean?&lt;/h2&gt;
&lt;p&gt;The &lt;a href=&quot;https://docs.optimizely.com/graph/docs/cached-templates&quot;&gt;Optimizely Graph cached templates documentation&lt;/a&gt; includes the following note:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Cached templates are enabled by default for queries created on or after January 1, 2025.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This caught my attention while testing queries through the C# SDK.&lt;/p&gt;
&lt;p&gt;For example, consider this query:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var query = _graphClient
    .QueryContent&amp;lt;ISearchableContent&amp;gt;()
    .Limit(5)
    .IncludeTotal()
    .Fields&amp;lt;SearchResult&amp;gt;(x =&amp;gt; x.SearchTitle);

var result = await query.GetAsync();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After inspecting the outgoing request using Fiddler, I noticed that the SDK sends a request to:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;POST https://cg.optimizely.com/content/v2&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;However, the request didn&#39;t include either of the parameters documented for explicitly enabling cached templates:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;?stored=true
cg-stored-query: template&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Looking Inside the SDK&lt;/h3&gt;
&lt;p&gt;Using ILSpy, I inspected the request construction inside &lt;span style=&quot;font-family: Consolas, Monaco, monospace; background-color: #f2f3f5; padding: 2px 4px; border-radius: 3px;&quot;&gt;Optimizely.Graph.Core.Client.GraphClient&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;The relevant code looked like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var fullQuery = new { query, variables };

var request = new HttpRequestMessage(
    HttpMethod.Post,
    allowCachedResults
        ? &quot;content/v2&quot;
        : &quot;content/v2?cache=false&quot;)
{
    Content = JsonContent.Create(fullQuery)
};

AddCustomHeaders(request, customHeaders);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the execution path I inspected, the SDK didn&#39;t explicitly append &lt;span style=&quot;font-family: monospace; background-color: #f2f3f5; padding: 2px 4px;&quot;&gt;stored=true&lt;/span&gt; or add the &lt;span style=&quot;font-family: monospace; background-color: #f2f3f5; padding: 2px 4px;&quot;&gt;cg-stored-query&lt;/span&gt; header.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Does this mean cached templates aren&#39;t enabled?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Not necessarily.&lt;/p&gt;
&lt;p&gt;The documentation describes default behavior, which may be handled by the Graph service itself without requiring these parameters from the client.&lt;/p&gt;
&lt;p&gt;My observation confirms only that the SDK doesn&#39;t explicitly send them in this scenario.&lt;/p&gt;
&lt;p&gt;Since the SDK communicates with the hosted Graph service, I wouldn&#39;t assume that running CMS 13 locally explains the difference.&lt;/p&gt;
&lt;p&gt;This remains an interesting point to clarify.&lt;/p&gt;
&lt;h2&gt;2. Explicitly Enabling Cached Templates with a DelegatingHandler&lt;/h2&gt;
&lt;p&gt;To experiment with explicit cached-template requests without modifying the SDK package, I used a custom &lt;span style=&quot;font-family: monospace; background-color: #f2f3f5; padding: 2px 4px;&quot;&gt;DelegatingHandler&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;The idea was straightforward: intercept outgoing Graph requests and add the required query parameter and header.&lt;/p&gt;
&lt;h3&gt;Custom HTTP Handler&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;public sealed class GraphCachedTemplateHandler : DelegatingHandler
{
    protected override Task&amp;lt;HttpResponseMessage&amp;gt; SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        if (request.Method == HttpMethod.Post &amp;amp;&amp;amp;
            request.RequestUri?.AbsolutePath.TrimEnd(&#39;/&#39;) == &quot;/content/v2&quot;)
        {
            var uri = new UriBuilder(request.RequestUri);

            var query = uri.Query.TrimStart(&#39;?&#39;);

            uri.Query = string.IsNullOrEmpty(query)
                ? &quot;stored=true&quot;
                : $&quot;{query}&amp;amp;stored=true&quot;;

            request.RequestUri = uri.Uri;

            request.Headers.TryAddWithoutValidation(
                &quot;cg-stored-query&quot;, &quot;template&quot;);
        }

        return base.SendAsync(request, cancellationToken);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Registering the Handler&lt;/h3&gt;
&lt;p&gt;After registering the Graph SDK, I attached the handler to its named HTTP client:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services.AddContentGraph();

services.AddTransient&amp;lt;GraphCachedTemplateHandler&amp;gt;();

services
    .AddHttpClient(&quot;Optimizely.Graph.Core.Client.Authenticated&quot;)
    .AddHttpMessageHandler&amp;lt;GraphCachedTemplateHandler&amp;gt;();&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Verifying with Fiddler&lt;/h3&gt;
&lt;p&gt;After executing the same Graph query, I confirmed the outgoing request:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;POST https://cg.optimizely.com/content/v2?stored=true
cg-stored-query: template&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This demonstrated that I could explicitly request cached-template behavior without changing the SDK source code.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; This handler applies to matching requests made through the configured HTTP client. It is not a per-query opt-in.&lt;/p&gt;
&lt;h3&gt;What About a Fluent WithCachedTemplate() Extension?&lt;/h3&gt;
&lt;p&gt;I also explored adding a custom extension method to support syntax such as:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var query = _graphClient
    .QueryContent&amp;lt;ISearchableContent&amp;gt;()
    .Limit(5)
    .WithCachedTemplate()
    .Fields&amp;lt;SearchResult&amp;gt;(x =&amp;gt; x.SearchTitle);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;However, there&#39;s an important limitation.&lt;/p&gt;
&lt;p&gt;The SDK&#39;s existing fluent methods, such as &lt;span style=&quot;font-family: monospace; background-color: #f2f3f5; padding: 2px 4px;&quot;&gt;WithoutCache()&lt;/span&gt;, update internal query state that is later used during execution.&lt;/p&gt;
&lt;p&gt;Although a custom &lt;span style=&quot;font-family: monospace; background-color: #f2f3f5; padding: 2px 4px;&quot;&gt;WithCachedTemplate()&lt;/span&gt; extension can store metadata against a query, the SDK&#39;s existing &lt;span style=&quot;font-family: monospace; background-color: #f2f3f5; padding: 2px 4px;&quot;&gt;GetAsync()&lt;/span&gt; doesn&#39;t automatically read that metadata or propagate it to the HTTP layer.&lt;/p&gt;
&lt;p&gt;As a result, the extension method can compile and fit naturally into the fluent API, &lt;strong&gt;but it doesn&#39;t change the outgoing HTTP request&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The custom &lt;span style=&quot;font-family: monospace; background-color: #f2f3f5; padding: 2px 4px;&quot;&gt;DelegatingHandler&lt;/span&gt; demonstrated earlier works at the HTTP-client level, but it cannot determine which individual query opted into cached templates without additional integration.&lt;/p&gt;
&lt;p&gt;A proper per-query implementation would therefore require an execution wrapper or, ideally, native SDK support to propagate the setting through the execution pipeline.&lt;/p&gt;
&lt;h2&gt;3. Cursor-Based Pagination: Typed vs. Untyped Queries&lt;/h2&gt;
&lt;p&gt;The second finding came while experimenting with cursor-based pagination.&lt;/p&gt;
&lt;p&gt;Initially, I tried to use &lt;span style=&quot;font-family: monospace; background-color: #f2f3f5; padding: 2px 4px;&quot;&gt;SetCursor()&lt;/span&gt; with the strongly typed query API:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var query = _graphClient
    .QueryContent&amp;lt;ISearchableContent&amp;gt;()
    .SetCursor(null);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;However, this doesn&#39;t compile because &lt;span style=&quot;font-family: monospace; background-color: #f2f3f5; padding: 2px 4px;&quot;&gt;SetCursor()&lt;/span&gt; isn&#39;t exposed through the typed &lt;span style=&quot;font-family: monospace; background-color: #f2f3f5; padding: 2px 4px;&quot;&gt;ISearchableContentQuery&amp;lt;T&amp;gt;&lt;/span&gt; interface in the SDK version I tested.&lt;/p&gt;
&lt;p&gt;After inspecting the implementation, I found that &lt;span style=&quot;font-family: monospace; background-color: #f2f3f5; padding: 2px 4px;&quot;&gt;SetCursor()&lt;/span&gt; is available through the untyped query API:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;_graphClient.Query(&quot;ISearchableContent&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;First Page&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;var query = _graphClient
    .Query(&quot;ISearchableContent&quot;)
    .SetCursor(null)
    .Limit(5)
    .IncludeTotal()
    .Fields(&quot;SearchTitle&quot;);

var result = await query.GetAsync();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Passing &lt;span style=&quot;font-family: monospace; background-color: #f2f3f5; padding: 2px 4px;&quot;&gt;null&lt;/span&gt; enables the cursor field without specifying a previous cursor.&lt;/p&gt;
&lt;h3&gt;Next Page&lt;/h3&gt;
&lt;p&gt;For subsequent pages, the cursor returned by the previous response can be passed to &lt;span style=&quot;font-family: monospace; background-color: #f2f3f5; padding: 2px 4px;&quot;&gt;SetCursor()&lt;/span&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var query = _graphClient
    .Query(&quot;ISearchableContent&quot;)
    .SetCursor(nextCursor)
    .Limit(5)
    .Fields(&quot;SearchTitle&quot;);

var result = await query.GetAsync();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, &lt;span style=&quot;font-family: monospace; background-color: #f2f3f5; padding: 2px 4px;&quot;&gt;nextCursor&lt;/span&gt; represents the cursor obtained from the previous response.&lt;/p&gt;
&lt;p&gt;Both requests use the same page size and content selection. The cursor value is &lt;span style=&quot;font-family: monospace; background-color: #f2f3f5; padding: 2px 4px;&quot;&gt;null&lt;/span&gt; for the initial request and the cursor returned by the previous response for subsequent requests.&lt;/p&gt;
&lt;h3&gt;Looking Inside SetCursor()&lt;/h3&gt;
&lt;p&gt;The implementation is straightforward:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public IQuery SetCursor(string? cursor)
{
    _queryExpression.IncludeCursor = true;
    _queryExpression.Cursor = cursor;

    return this;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It performs two operations:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Enables inclusion of the cursor in the GraphQL response.&lt;/li&gt;
&lt;li&gt;Assigns the supplied cursor for pagination.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This explains why &lt;span style=&quot;font-family: monospace; background-color: #f2f3f5; padding: 2px 4px;&quot;&gt;SetCursor(null)&lt;/span&gt; is useful for the initial request.&lt;/p&gt;
&lt;h3&gt;An SDK API Difference&lt;/h3&gt;
&lt;p&gt;The distinction is worth highlighting:&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;border: 1px solid #dddddd; padding: 8px; text-align: left;&quot;&gt;Query API&lt;/th&gt;
&lt;th style=&quot;border: 1px solid #dddddd; padding: 8px; text-align: left;&quot;&gt;Cursor support&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #dddddd; padding: 8px; text-align: left;&quot;&gt;&lt;span style=&quot;font-family: monospace; background-color: #f2f3f5; padding: 2px 4px;&quot;&gt;QueryContent&amp;lt;T&amp;gt;()&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #dddddd; padding: 8px; text-align: left;&quot;&gt;&lt;span style=&quot;font-family: monospace; background-color: #f2f3f5; padding: 2px 4px;&quot;&gt;SetCursor()&lt;/span&gt; not exposed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #dddddd; padding: 8px; text-align: left;&quot;&gt;&lt;span style=&quot;font-family: monospace; background-color: #f2f3f5; padding: 2px 4px;&quot;&gt;Query(&quot;TypeName&quot;)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #dddddd; padding: 8px; text-align: left;&quot;&gt;&lt;span style=&quot;font-family: monospace; background-color: #f2f3f5; padding: 2px 4px;&quot;&gt;SetCursor()&lt;/span&gt; available&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;This is a difference in the SDK&#39;s public fluent interfaces, rather than evidence that the Graph service doesn&#39;t support cursor pagination for typed content.&lt;/p&gt;
&lt;h2&gt;4. Final Thoughts&lt;/h2&gt;
&lt;p&gt;These experiments helped me better understand how the Optimizely Graph C# SDK builds and executes queries.&lt;/p&gt;
&lt;p&gt;A few takeaways:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The SDK doesn&#39;t explicitly send cached-template parameters in the execution path I inspected, even though the documentation describes cached templates as enabled by default for newer queries.&lt;/li&gt;
&lt;li&gt;A custom &lt;span style=&quot;font-family: monospace; background-color: #f2f3f5; padding: 2px 4px;&quot;&gt;DelegatingHandler&lt;/span&gt; can add the documented parameters to outgoing Graph requests.&lt;/li&gt;
&lt;li&gt;A fluent per-query &lt;span style=&quot;font-family: monospace; background-color: #f2f3f5; padding: 2px 4px;&quot;&gt;WithCachedTemplate()&lt;/span&gt; extension requires more than storing a flag; the setting must reach the HTTP execution layer.&lt;/li&gt;
&lt;li&gt;Cursor pagination is exposed through the untyped &lt;span style=&quot;font-family: monospace; background-color: #f2f3f5; padding: 2px 4px;&quot;&gt;Query(&quot;TypeName&quot;)&lt;/span&gt; API, while the typed &lt;span style=&quot;font-family: monospace; background-color: #f2f3f5; padding: 2px 4px;&quot;&gt;QueryContent&amp;lt;T&amp;gt;()&lt;/span&gt; interface doesn&#39;t expose &lt;span style=&quot;font-family: monospace; background-color: #f2f3f5; padding: 2px 4px;&quot;&gt;SetCursor()&lt;/span&gt; in version 13.3.0.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These are relatively small observations, but they illustrate why exploring the SDK implementation and inspecting actual HTTP traffic can be useful when working with Graph.&lt;/p&gt;
&lt;p&gt;I&#39;d be interested to hear from the Optimizely Graph team how the documented default behavior for cached templates applies to queries executed through the C# SDK, particularly when the outgoing requests don&#39;t explicitly include the cached-template parameters.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.optimizely.com/graph/docs/cached-templates&quot;&gt;Optimizely Graph &amp;ndash; Cached Templates&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.optimizely.com/graph/docs/cursor-based-pagination&quot;&gt; Optimizely Graph &amp;ndash; Cursor-Based Pagination &lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>            <guid>https://world.optimizely.com/blogs/mardy-abdelkareem/dates/2026/10/exploring-cached-templates-and-cursor-pagination-in-the-optimizely-graph-c-sdk/</guid>            <pubDate>Mon, 19 Oct 2026 04:22:59 GMT</pubDate>           <category>Blog post</category></item><item> <title>Computed Fields in CMS 13 Content Contracts with Optimizely Graph</title>            <link>https://world.optimizely.com/blogs/mardy-abdelkareem/dates/2026/10/computed-fields-in-cms-13-content-contracts-a-practical-guide-with-optimizely-graph-c-sdk/</link>            <description>&lt;p&gt;Content Contracts in Optimizely CMS 13 allow different content types to share a common structure when querying Optimizely Graph.&lt;/p&gt;
&lt;p&gt;But what if the shared field doesn&#39;t exist as a stored CMS property, and its value needs to be calculated differently depending on the content type?&lt;/p&gt;
&lt;p&gt;For example, imagine a search feature that includes articles, events, and images:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Articles use &lt;strong&gt;Heading&lt;/strong&gt; as their search title.&lt;/li&gt;
&lt;li&gt;Events use &lt;strong&gt;EventName&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Images use &lt;strong&gt;ImageTitle&lt;/strong&gt;, falling back to their name.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We want all three to expose a common &lt;strong&gt;SearchTitle&lt;/strong&gt; field in Graph, without adding another persisted property to each content type.&lt;/p&gt;
&lt;p&gt;Here&#39;s a working approach tested with &lt;strong&gt;Optimizely CMS 13.3.0&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;1. Define a Content Contract&lt;/h2&gt;
&lt;p&gt;First, define a Content Contract with a method that each implementing content type uses to provide its search title.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[ContentType]
public interface ISearchableContent : IContentData
{
    string GetSearchTitle();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each content type implements the same contract but calculates the search title using its own properties. For example, an article page uses &lt;strong&gt;Heading&lt;/strong&gt;, while an event page uses &lt;strong&gt;EventName&lt;/strong&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class ArticlePage : PageData, ISearchableContent
{
    public virtual string Heading { get; set; }

    public string GetSearchTitle() =&amp;gt;
        string.IsNullOrWhiteSpace(Heading) ? Name : Heading;
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class EventPage : PageData, ISearchableContent
{
    public virtual string EventName { get; set; }

    public string GetSearchTitle() =&amp;gt;
        string.IsNullOrWhiteSpace(EventName) ? Name : EventName;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Both content types implement the same contract but calculate &lt;strong&gt;SearchTitle&lt;/strong&gt; differently. The same approach works for media types, such as images, using their own properties.&lt;/p&gt;
&lt;p&gt;Make sure the relevant content types implement or are associated with the Content Contract in CMS so they can be queried together in Graph.&lt;/p&gt;
&lt;p&gt;This keeps the calculation close to each content type and avoids maintaining a central switch statement.&lt;/p&gt;
&lt;h2&gt;2. Expose the Computed Field in Graph&lt;/h2&gt;
&lt;p&gt;Next, create an extension method and register it using Graph indexing conventions.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static class SearchExtensions
{
    public static string SearchTitle(this ISearchableContent content)
        =&amp;gt; content.GetSearchTitle();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Register the computed field:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services.AddContentGraph(configureConventions: conventions =&amp;gt;
{
    conventions.ForInstancesOf&amp;lt;ISearchableContent&amp;gt;()
        .IncludeField(
            x =&amp;gt; x.SearchTitle(),
            IndexingType.Searchable);
});&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The important detail is that &lt;strong&gt;SearchTitle is computed during indexing&lt;/strong&gt;. We don&#39;t need to add a persisted SearchTitle property to our CMS models.&lt;/p&gt;
&lt;p&gt;The resulting Graph schema exposes the field through &lt;strong&gt;ISearchableContent&lt;/strong&gt;, regardless of the underlying content type.&lt;/p&gt;
&lt;h2&gt;3. Query It with the Graph C# SDK&lt;/h2&gt;
&lt;p&gt;The interesting part is that the computed field can also be used in strongly typed SDK queries.&lt;/p&gt;
&lt;p&gt;First, define a lightweight result model:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class SearchResult
{
    public string SearchTitle { get; set; }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then query the Content Contract:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var query = graphClient
    .QueryContent&amp;lt;ISearchableContent&amp;gt;()
    .Where(x =&amp;gt; x.SearchTitle().Contains(&quot;Optimizely&quot;))
    .Limit(20)
    .IncludeTotal()
    .Fields&amp;lt;SearchResult&amp;gt;(x =&amp;gt; x.SearchTitle);

var result = await query.GetAsync();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In our test, the SDK translated the filtering expression into GraphQL:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;query ISearchableContentQuery {
  ISearchableContent(
    where: { SearchTitle: { contains: &quot;Optimizely&quot; } }
    limit: 20
  ) {
    items {
      SearchTitle
    }
    total(all: true)
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The query returned one matching event with &lt;strong&gt;SearchTitle&lt;/strong&gt; equal to &lt;strong&gt;Optimizely Developer Webinar&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Here&#39;s the interesting detail: &lt;strong&gt;SearchTitle()&lt;/strong&gt; is a C# extension method, not a stored CMS property. Graph indexing conventions expose it as a searchable field, and the Graph C# SDK translates the expression into a server-side GraphQL filter rather than filtering an in-memory collection.&lt;/p&gt;
&lt;p&gt;We also verified sorting by the computed field, language-specific values, and recalculation after updating and publishing the underlying content.&lt;/p&gt;
&lt;h2&gt;Key Takeaway&lt;/h2&gt;
&lt;p&gt;By combining &lt;strong&gt;CMS 13 Content Contracts, Graph indexing conventions, and the Graph C# SDK&lt;/strong&gt;, we can expose a shared searchable field across different content types while keeping the calculation specific to each type.&lt;/p&gt;
&lt;p&gt;No additional persisted CMS properties are required.&lt;/p&gt;
&lt;p&gt;Keep computed field calculations lightweight and deterministic, since they run as part of the indexing process.&lt;/p&gt;
&lt;h3&gt;References&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.optimizely.com/cms-13/docs/indexing-conventions&quot;&gt;Optimizely CMS 13 &amp;mdash; Indexing Conventions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/link/5ffa0c0d7b184f049358e62d7fcad94f.aspx&quot;&gt;Introducing Optimizely CMS 13 Graph SDK &amp;mdash; Jake Minard&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>            <guid>https://world.optimizely.com/blogs/mardy-abdelkareem/dates/2026/10/computed-fields-in-cms-13-content-contracts-a-practical-guide-with-optimizely-graph-c-sdk/</guid>            <pubDate>Wed, 14 Oct 2026 05:01:48 GMT</pubDate>           <category>Blog post</category></item><item> <title>Setting Up an Optimizely CMS 13 MCP Server Locally with Codex</title>            <link>https://world.optimizely.com/blogs/mardy-abdelkareem/dates/2026/10/getting-started-with-optimizely-cms-13-mcp-server-in-vs-code-using-codex/</link>            <description>&lt;p&gt;Optimizely CMS 13 supports &lt;a href=&quot;https://modelcontextprotocol.io/docs/2026-07-28/getting-started/intro&quot;&gt;Model Context Protocol (MCP)&lt;/a&gt;, allowing AI tools such as Claude and Codex to interact with CMS content and metadata through an MCP server.&lt;/p&gt;
&lt;p&gt;In this post, I&#39;ll show how I configured the MCP server in my local CMS 13 Alloy project, connected it to Codex in Visual Studio Code, and used it to explore content types, create pages, and build a Visual Builder experience.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Optimizely CMS 13 (I&#39;m using Alloy with CMS 13.3.0).&lt;/li&gt;
&lt;li&gt;An MCP-compatible AI client (I&#39;m using Codex in VS Code).&lt;/li&gt;
&lt;li&gt;A local CMS instance running over HTTPS, for example, https://localhost:5000.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Configure the MCP Server&lt;/h2&gt;
&lt;p&gt;Install the &lt;strong&gt;Optimizely.Cms.Opal.Tools&lt;/strong&gt; NuGet package (version 13.2.0 or later):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dotnet add package Optimizely.Cms.Opal.Tools --version 13.2.0&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Register the MCP server in &lt;strong&gt;Startup.cs&lt;/strong&gt; using client-secret authentication:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services.AddCmsMcpServer()
    .WithClientSecretAuthentication();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then map the MCP endpoint and require client-secret authorization:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;app.UseEndpoints(endpoints =&amp;gt;
{
    endpoints.MapContent();

    endpoints.MapMcp(&quot;/mcp&quot;)
        .RequireClientSecretAuthorization();
});&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The MCP endpoint is now available at &lt;strong&gt;https://localhost:5000/mcp&lt;/strong&gt;, or your application&#39;s configured HTTPS address.&lt;/p&gt;
&lt;h2&gt;Configure Client-Secret Authentication&lt;/h2&gt;
&lt;p&gt;Make sure a client secret is configured for your local CMS application. We&#39;ll use it to authenticate Codex when connecting to the MCP server.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Security note:&lt;/strong&gt; Never commit your client secret to source control or expose it in screenshots.&lt;/p&gt;
&lt;h2&gt;Connect Codex in VS Code&lt;/h2&gt;
&lt;p&gt;Open &lt;strong&gt;Codex Settings &amp;rarr; MCP servers &amp;rarr; Add server&lt;/strong&gt; and select &lt;strong&gt;Streamable HTTP&lt;/strong&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Name:&lt;/strong&gt; optimizely-cms&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;URL:&lt;/strong&gt; https://localhost:5000/mcp&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bearer token env var:&lt;/strong&gt; Leave empty.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Header:&lt;/strong&gt; Authorization, with your configured client secret as the value.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Alternatively, configure the server in Codex&#39;s &lt;strong&gt;config.toml&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[mcp_servers.optimizely-cms]
type = &quot;http&quot;
url = &quot;https://localhost:5000/mcp&quot;

[mcp_servers.optimizely-cms.headers]
Authorization = &quot;your-secret-value&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Replace the placeholder with your actual secret and enable the server in Codex Settings. The secret is passed directly in the Authorization header, without a Bearer prefix.&lt;img style=&quot;display: block; margin-left: auto; margin-right: auto;&quot; src=&quot;/link/d7116162f002422b9d8f3db79abaa3c6.aspx&quot; /&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot;&gt;&lt;em&gt;Configuring the local Optimizely MCP server in Codex.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Verify the Integration&lt;/h2&gt;
&lt;p&gt;With the CMS running, open a new Codex chat in VS Code and try:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;List the available content types in my local Optimizely CMS instance using the MCP server.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Codex retrieved the registered content types, including &lt;strong&gt;StandardPage&lt;/strong&gt; and &lt;strong&gt;StandardExperienceData&lt;/strong&gt;, and could inspect their properties, locales, and available Visual Builder components.&lt;/p&gt;
&lt;p&gt;You can also retrieve existing content:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Find the published content items of type ProductTestPage and show their properties.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;These read-only operations confirm that Codex can access CMS content and metadata through MCP.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot;&gt;&lt;img src=&quot;/link/cf929eeea2744b6b8add67a0e7ba366d.aspx&quot; /&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot;&gt;&lt;em&gt;Codex retrieving content types from the local CMS instance.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;Create Content Using Codex&lt;/h3&gt;
&lt;p&gt;Next, I asked Codex to create an unpublished page:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Create &quot;MCP Demo Page&quot; under Start using StandardPage. Add a heading and paragraph to MainBody, and save it as an English draft.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Codex created the page and updated its content through the MCP server.&lt;/p&gt;
&lt;p&gt;I also tested Visual Builder:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Create an unpublished &quot;Alloy Demo Experience&quot; using StandardExperienceData, with a BlankSection containing a VisualBuilderTestBlock.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Codex created the experience using the existing CMS content types and Visual Builder components. Both items were saved as drafts without publishing.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot;&gt;&lt;img src=&quot;/link/aca32396a13449b6adaed88c40386a73.aspx&quot; /&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot;&gt;&lt;em&gt;The MCP Demo Page created through Codex and saved as a draft.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;Troubleshooting Content Permissions&lt;/h3&gt;
&lt;p&gt;During content creation, I encountered an authorization error:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Access denied: You do not have permission to create content in this location.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The MCP client authenticated successfully with a &lt;strong&gt;CmsAdmins&lt;/strong&gt; role claim, which was mapped to &lt;strong&gt;WebAdmins&lt;/strong&gt; in the application configuration. Although the Start page granted Create permission to WebAdmins, the MCP client was still denied access.&lt;/p&gt;
&lt;p&gt;The issue was resolved by explicitly granting the &lt;strong&gt;MCP Client&lt;/strong&gt; permissions on the Start page through CMS access rights. Afterward, Codex could create and update content.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot;&gt;&lt;img src=&quot;/link/078e0a84916a4ca9b772d1ef610bfa5e.aspx&quot; /&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot;&gt;&lt;em&gt;Granting the MCP Client access on the Start page resolved the create denial.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Security note:&lt;/strong&gt; For this local demo, all permissions were enabled, including Publish and Administer. In a real environment, grant only the permissions needed for the intended operations.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;With Codex connected to Optimizely CMS 13 through MCP, we were able to explore content types, create pages, and work with Visual Builder directly from VS Code.&lt;/p&gt;
&lt;p&gt;One thing worth noting is that connecting successfully doesn&#39;t necessarily mean the MCP client has permission to create or update content. You&#39;ll still need to configure the appropriate access rights in CMS.&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/mardy-abdelkareem/dates/2026/10/getting-started-with-optimizely-cms-13-mcp-server-in-vs-code-using-codex/</guid>            <pubDate>Mon, 12 Oct 2026 04:41:05 GMT</pubDate>           <category>Blog post</category></item></channel>
</rss>