<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom"><title type="text">Blog posts by Mardy Abdelkareem</title><link href="https://world.optimizely.com/blogs/mardy-abdelkareem/" /><updated>2026-11-02T09:36:42.0000000Z</updated><id>https://world.optimizely.com/blogs/mardy-abdelkareem/</id><generator uri="https://world.optimizely.com/blogs/mardy-abdelkareem/" version="2.0">Optimizely World</generator><entry><title>Introducing Publishing Timeline for Optimizely CMS 13</title><link href="https://world.optimizely.com/blogs/mardy-abdelkareem/dates/2026/10/introducing-publishing-timeline-for-optimizely-cms-13/" /><id>https://world.optimizely.com/blogs/mardy-abdelkareem/dates/2026/10/introducing-publishing-timeline-for-optimizely-cms-13/</id><published>2026-11-02T09:36:42.0000000Z</published><updated>2026-11-02T09:36:42.0000000Z</updated><author><name>mardy.abdelkareem@firstlinesoftware.com</name></author><category term="Blog post" /><summary type="html">&lt;p&gt;When working with scheduled content in Optimizely CMS, having a clear overview of what'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="color: #0066cc; text-decoration: underline;" href="/link/ac89958d132547f9bf5ebfb16e4de0b4.aspx"&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'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="max-width: 900px; margin: 20px auto; text-align: center;"&gt;&lt;img style="display: block; width: 100%; height: auto; border: 1px solid #e2e2e2; border-radius: 6px;" src="/link/98e878a2e8814d1ba6ae63a22a45cafb.aspx" alt="Publishing Timeline table showing scheduled content, languages, publishing dates, modified by, and edit links." /&gt;
&lt;figcaption style="margin-top: 8px; color: #666; font-size: 13px;"&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'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="max-width: 900px; margin: 20px auto; text-align: center;"&gt;&lt;img style="display: block; width: 100%; height: auto; border: 1px solid #e2e2e2; border-radius: 6px;" src="/link/d59c9a8a43b645e29e968324710c4e68.aspx" alt="Publishing Timeline monthly calendar showing scheduled publications and a popup with additional scheduled items." /&gt;
&lt;figcaption style="margin-top: 8px; color: #666; font-size: 13px;"&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'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="color: #0066cc; text-decoration: underline;" href="https://nuget.optimizely.com/packages/mardy.optimizely.publishingtimeline/1.0.0"&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'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="color: #0066cc; text-decoration: underline;" href="https://github.com/mardy-awd/Optimizely.PublishingTimeline"&gt;Mardy.Optimizely.PublishingTimeline&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;NuGet package:&lt;/strong&gt; &lt;a style="color: #0066cc; text-decoration: underline;" href="https://nuget.optimizely.com/packages/mardy.optimizely.publishingtimeline/1.0.0"&gt;Optimizely NuGet feed (v1.0.0)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I'd be happy to hear your feedback or ideas for future improvements!&lt;/p&gt;</summary></entry><entry><title>Time Zone-Aware Scheduled Publishing in Optimizely CMS 13</title><link href="https://world.optimizely.com/blogs/mardy-abdelkareem/dates/2026/10/time-zone-aware-scheduled-publishing-in-optimizely-cms-13/" /><id>https://world.optimizely.com/blogs/mardy-abdelkareem/dates/2026/10/time-zone-aware-scheduled-publishing-in-optimizely-cms-13/</id><published>2026-10-26T07:02:50.0000000Z</published><updated>2026-10-26T07:02:50.0000000Z</updated><author><name>mardy.abdelkareem@firstlinesoftware.com</name></author><category term="Blog post" /><summary type="html">&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="margin: 24px auto; text-align: center;"&gt;
&lt;figcaption style="margin-top: 8px; font-size: 14px; color: #666; line-height: 1.5;"&gt;&lt;img src="/link/0df4bf9ef957473d91f8607906568429.aspx" /&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'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="margin: 24px auto; text-align: center;"&gt;
&lt;figcaption style="margin-top: 8px; font-size: 14px; color: #666; line-height: 1.5;"&gt;&lt;img src="/link/4422c9d809354239aa0f2fa5bee5a52c.aspx" /&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="language-bash"&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="color: #0066cc; text-decoration: underline;" href="https://github.com/mardy-awd/Optimizely.TimeZonePublishing"&gt;Optimizely.TimeZonePublishing&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;NuGet package:&lt;/strong&gt; &lt;a style="color: #0066cc; text-decoration: underline;" href="https://nuget.optimizely.com/packages/mardy.optimizely.timezonepublishing/0.1.1"&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;</summary></entry><entry><title>Exploring Cached Templates and Cursor Pagination in the Optimizely Graph C# SDK</title><link href="https://world.optimizely.com/blogs/mardy-abdelkareem/dates/2026/10/exploring-cached-templates-and-cursor-pagination-in-the-optimizely-graph-c-sdk/" /><id>https://world.optimizely.com/blogs/mardy-abdelkareem/dates/2026/10/exploring-cached-templates-and-cursor-pagination-in-the-optimizely-graph-c-sdk/</id><published>2026-10-19T04:22:59.0000000Z</published><updated>2026-10-19T04:22:59.0000000Z</updated><author><name>mardy.abdelkareem@firstlinesoftware.com</name></author><category term="Blog post" /><summary type="html">&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="https://docs.optimizely.com/graph/docs/cached-templates"&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'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="font-family: Consolas, Monaco, monospace; background-color: #f2f3f5; padding: 2px 4px; border-radius: 3px;"&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
        ? "content/v2"
        : "content/v2?cache=false")
{
    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't explicitly append &lt;span style="font-family: monospace; background-color: #f2f3f5; padding: 2px 4px;"&gt;stored=true&lt;/span&gt; or add the &lt;span style="font-family: monospace; background-color: #f2f3f5; padding: 2px 4px;"&gt;cg-stored-query&lt;/span&gt; header.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Does this mean cached templates aren'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't explicitly send them in this scenario.&lt;/p&gt;
&lt;p&gt;Since the SDK communicates with the hosted Graph service, I wouldn'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="font-family: monospace; background-color: #f2f3f5; padding: 2px 4px;"&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('/') == "/content/v2")
        {
            var uri = new UriBuilder(request.RequestUri);

            var query = uri.Query.TrimStart('?');

            uri.Query = string.IsNullOrEmpty(query)
                ? "stored=true"
                : $"{query}&amp;amp;stored=true";

            request.RequestUri = uri.Uri;

            request.Headers.TryAddWithoutValidation(
                "cg-stored-query", "template");
        }

        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("Optimizely.Graph.Core.Client.Authenticated")
    .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's an important limitation.&lt;/p&gt;
&lt;p&gt;The SDK's existing fluent methods, such as &lt;span style="font-family: monospace; background-color: #f2f3f5; padding: 2px 4px;"&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="font-family: monospace; background-color: #f2f3f5; padding: 2px 4px;"&gt;WithCachedTemplate()&lt;/span&gt; extension can store metadata against a query, the SDK's existing &lt;span style="font-family: monospace; background-color: #f2f3f5; padding: 2px 4px;"&gt;GetAsync()&lt;/span&gt; doesn'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't change the outgoing HTTP request&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The custom &lt;span style="font-family: monospace; background-color: #f2f3f5; padding: 2px 4px;"&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="font-family: monospace; background-color: #f2f3f5; padding: 2px 4px;"&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't compile because &lt;span style="font-family: monospace; background-color: #f2f3f5; padding: 2px 4px;"&gt;SetCursor()&lt;/span&gt; isn't exposed through the typed &lt;span style="font-family: monospace; background-color: #f2f3f5; padding: 2px 4px;"&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="font-family: monospace; background-color: #f2f3f5; padding: 2px 4px;"&gt;SetCursor()&lt;/span&gt; is available through the untyped query API:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;_graphClient.Query("ISearchableContent")&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;First Page&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;var query = _graphClient
    .Query("ISearchableContent")
    .SetCursor(null)
    .Limit(5)
    .IncludeTotal()
    .Fields("SearchTitle");

var result = await query.GetAsync();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Passing &lt;span style="font-family: monospace; background-color: #f2f3f5; padding: 2px 4px;"&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="font-family: monospace; background-color: #f2f3f5; padding: 2px 4px;"&gt;SetCursor()&lt;/span&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var query = _graphClient
    .Query("ISearchableContent")
    .SetCursor(nextCursor)
    .Limit(5)
    .Fields("SearchTitle");

var result = await query.GetAsync();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, &lt;span style="font-family: monospace; background-color: #f2f3f5; padding: 2px 4px;"&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="font-family: monospace; background-color: #f2f3f5; padding: 2px 4px;"&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="font-family: monospace; background-color: #f2f3f5; padding: 2px 4px;"&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="border-collapse: collapse; width: 100%;"&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style="border: 1px solid #dddddd; padding: 8px; text-align: left;"&gt;Query API&lt;/th&gt;
&lt;th style="border: 1px solid #dddddd; padding: 8px; text-align: left;"&gt;Cursor support&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style="border: 1px solid #dddddd; padding: 8px; text-align: left;"&gt;&lt;span style="font-family: monospace; background-color: #f2f3f5; padding: 2px 4px;"&gt;QueryContent&amp;lt;T&amp;gt;()&lt;/span&gt;&lt;/td&gt;
&lt;td style="border: 1px solid #dddddd; padding: 8px; text-align: left;"&gt;&lt;span style="font-family: monospace; background-color: #f2f3f5; padding: 2px 4px;"&gt;SetCursor()&lt;/span&gt; not exposed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="border: 1px solid #dddddd; padding: 8px; text-align: left;"&gt;&lt;span style="font-family: monospace; background-color: #f2f3f5; padding: 2px 4px;"&gt;Query("TypeName")&lt;/span&gt;&lt;/td&gt;
&lt;td style="border: 1px solid #dddddd; padding: 8px; text-align: left;"&gt;&lt;span style="font-family: monospace; background-color: #f2f3f5; padding: 2px 4px;"&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's public fluent interfaces, rather than evidence that the Graph service doesn'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'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="font-family: monospace; background-color: #f2f3f5; padding: 2px 4px;"&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="font-family: monospace; background-color: #f2f3f5; padding: 2px 4px;"&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="font-family: monospace; background-color: #f2f3f5; padding: 2px 4px;"&gt;Query("TypeName")&lt;/span&gt; API, while the typed &lt;span style="font-family: monospace; background-color: #f2f3f5; padding: 2px 4px;"&gt;QueryContent&amp;lt;T&amp;gt;()&lt;/span&gt; interface doesn't expose &lt;span style="font-family: monospace; background-color: #f2f3f5; padding: 2px 4px;"&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'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'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="https://docs.optimizely.com/graph/docs/cached-templates"&gt;Optimizely Graph &amp;ndash; Cached Templates&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.optimizely.com/graph/docs/cursor-based-pagination"&gt; Optimizely Graph &amp;ndash; Cursor-Based Pagination &lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</summary></entry><entry><title>Computed Fields in CMS 13 Content Contracts with Optimizely Graph</title><link href="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/" /><id>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/</id><published>2026-10-14T05:01:48.0000000Z</published><updated>2026-10-14T05:01:48.0000000Z</updated><author><name>mardy.abdelkareem@firstlinesoftware.com</name></author><category term="Blog post" /><summary type="html">&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'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'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'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("Optimizely"))
    .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: "Optimizely" } }
    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'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="https://docs.optimizely.com/cms-13/docs/indexing-conventions"&gt;Optimizely CMS 13 &amp;mdash; Indexing Conventions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="/link/5ffa0c0d7b184f049358e62d7fcad94f.aspx"&gt;Introducing Optimizely CMS 13 Graph SDK &amp;mdash; Jake Minard&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</summary></entry><entry><title>Setting Up an Optimizely CMS 13 MCP Server Locally with Codex</title><link href="https://world.optimizely.com/blogs/mardy-abdelkareem/dates/2026/10/getting-started-with-optimizely-cms-13-mcp-server-in-vs-code-using-codex/" /><id>https://world.optimizely.com/blogs/mardy-abdelkareem/dates/2026/10/getting-started-with-optimizely-cms-13-mcp-server-in-vs-code-using-codex/</id><published>2026-10-12T04:41:05.0000000Z</published><updated>2026-10-12T04:41:05.0000000Z</updated><author><name>mardy.abdelkareem@firstlinesoftware.com</name></author><category term="Blog post" /><summary type="html">&lt;p&gt;Optimizely CMS 13 supports &lt;a href="https://modelcontextprotocol.io/docs/2026-07-28/getting-started/intro"&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'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'm using Alloy with CMS 13.3.0).&lt;/li&gt;
&lt;li&gt;An MCP-compatible AI client (I'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("/mcp")
        .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'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'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's &lt;strong&gt;config.toml&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[mcp_servers.optimizely-cms]
type = "http"
url = "https://localhost:5000/mcp"

[mcp_servers.optimizely-cms.headers]
Authorization = "your-secret-value"&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="display: block; margin-left: auto; margin-right: auto;" src="/link/d7116162f002422b9d8f3db79abaa3c6.aspx" /&gt;&lt;/p&gt;
&lt;p style="text-align: center;"&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="text-align: center;"&gt;&lt;img src="/link/cf929eeea2744b6b8add67a0e7ba366d.aspx" /&gt;&lt;/p&gt;
&lt;p style="text-align: center;"&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 "MCP Demo Page" 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 "Alloy Demo Experience" 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="text-align: center;"&gt;&lt;img src="/link/aca32396a13449b6adaed88c40386a73.aspx" /&gt;&lt;/p&gt;
&lt;p style="text-align: center;"&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="text-align: center;"&gt;&lt;img src="/link/078e0a84916a4ca9b772d1ef610bfa5e.aspx" /&gt;&lt;/p&gt;
&lt;p style="text-align: center;"&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't necessarily mean the MCP client has permission to create or update content. You'll still need to configure the appropriate access rights in CMS.&lt;/p&gt;</summary></entry></feed>