<?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 Minesh Shah</title> <link>https://world.optimizely.com/blogs/Minesh-Shah/</link><description></description><ttl>60</ttl><generator>Optimizely World</generator><item> <title>Personalisation in CMS 13 Using Audiences</title>            <link>https://world.optimizely.com/blogs/Minesh-Shah/Dates/2026/2/personalisation-in-cms-13-using-audiences/</link>            <description>&lt;div&gt;
&lt;p&gt;One of the common questions around CMS 13 is whether it still supports personalisation without requiring additional products. The short answer is yes.&lt;/p&gt;
&lt;p&gt;CMS 13 includes built in personalisation using what were previously known as Visitor Groups. In CMS 13 these have been renamed to Audiences (Correction they changed to Audiences in latter versions of CMS12 which somehow I missed, Thanks Scott for pointing this out), but the core concept remains the same. You can define audiences based on visitor behaviour and use them to tailor content across your site.&lt;/p&gt;
&lt;p&gt;Audiences are managed directly within the CMS interface. For example, you might create an audience made up of visitors who have viewed pages with a specific category more than a set number of times. Once an audience is defined, it can be used to control how content is displayed.&lt;/p&gt;
&lt;p&gt;Personalisation is applied at the content level using standard content areas. Editors can choose to show or hide individual components such as a hero or jumbotron based on the selected audience. This allows targeted experiences without introducing additional complexity for editors.&lt;/p&gt;
&lt;p&gt;CMS 13 also supports previewing content as a specific audience. This makes it easy to validate personalisation rules and understand exactly what different users will see before changes are published.&lt;/p&gt;
&lt;p&gt;This approach provides traditional, rules based personalisation out of the box and offers a clear path to more advanced personalisation in the future if needed. For many organisations, it delivers exactly the right level of capability from day one.&lt;/p&gt;
&lt;p&gt;A short video walkthrough is included below to demonstrate this in action.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/u02AxSzp9i0?si=DslEH-KoDO-eoVQH&quot;&gt;&lt;img src=&quot;/link/310aa2aee3a94dfbac79be07a9f90fc8.aspx&quot; alt=&quot;&quot; width=&quot;539&quot; height=&quot;307&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;</description>            <guid>https://world.optimizely.com/blogs/Minesh-Shah/Dates/2026/2/personalisation-in-cms-13-using-audiences/</guid>            <pubDate>Thu, 12 Feb 2026 09:00:01 GMT</pubDate>           <category>Blog post</category></item><item> <title>Experimentation at Speed Using Optimizely Opal and Web Experimentation</title>            <link>https://world.optimizely.com/blogs/Minesh-Shah/Dates/2026/1/experimentation-at-speed-using-optimizely-opal-and-web-experimentation/</link>            <description>&lt;p&gt;If you are working in experimentation, you will know that speed matters. The quicker you can go from idea to implementation, the faster you can learn, optimise, and deliver impact.&lt;/p&gt;
&lt;p&gt;In this short video, I demonstrate just how quick and easy it is to use &lt;strong&gt;Optimizely Opal&lt;/strong&gt; alongside &lt;strong&gt;Optimizely Web Experimentation&lt;/strong&gt; to create and execute an experiment at pace. What normally takes multiple manual steps can be dramatically accelerated, with Opal helping to streamline the entire workflow.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/HqsSn6E6o4E?si=QbAd9f9VElVrZhU9&quot;&gt;&lt;img src=&quot;/link/a910d5f0849c4bfc88133921c6137372.aspx&quot; alt=&quot;&quot; width=&quot;500&quot; height=&quot;333&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;The scenario: make a quick change and run an experiment&lt;/h3&gt;
&lt;p&gt;The goal of the demo is simple. I want to make a small change to a website and show how fast I can turn that into a working experiment using Opal.&lt;/p&gt;
&lt;p&gt;Rather than spending time manually setting everything up, Opal can assist with the process and help generate what you need, quickly and efficiently.&lt;/p&gt;
&lt;h3&gt;Why this matters: speed and experimentation velocity&lt;/h3&gt;
&lt;p&gt;The real value here is the speed.&lt;/p&gt;
&lt;p&gt;Using Optimizely Opal with Web Experimentation means you can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;reduce time spent on setup&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;increase experimentation throughput&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;test more ideas more frequently&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;accelerate learning cycles across your digital estate&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It is genuinely exciting to see experimentation becoming this efficient, and it opens up huge potential for teams who want to move faster without compromising quality.&lt;/p&gt;
&lt;h3&gt;Final thoughts&lt;/h3&gt;
&lt;p&gt;Using &lt;strong&gt;Optimizely Opal and Web Experimentation&lt;/strong&gt; together is an absolute dream. The speed in which you can execute experiments, and have Opal support so much of the process for you, is truly impressive.&lt;/p&gt;
&lt;p&gt;If you are exploring ways to increase experimentation velocity, this is well worth looking at.&lt;/p&gt;
&lt;p&gt;Thank you for watching.&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/Minesh-Shah/Dates/2026/1/experimentation-at-speed-using-optimizely-opal-and-web-experimentation/</guid>            <pubDate>Fri, 30 Jan 2026 10:24:02 GMT</pubDate>           <category>Blog post</category></item><item> <title>Building an Agent Replicator for Optimizely Opal</title>            <link>https://world.optimizely.com/blogs/Minesh-Shah/Dates/2026/1/opal-tool-agent-replicator/</link>            <description>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;A couple of months ago, my team and I took part in the Opal Innovation Challenge (Which we proudly won). We decided to create an agentic AI experience that brings audience insight directly into content workflows, the&amp;nbsp;&lt;strong&gt;Virtual Focus Group&lt;/strong&gt;. By simulating real customer segments and offering instant, nuanced feedback on messaging, this solution helps teams validate messaging in minutes rather than weeks, reduces costly research cycles, and captures behavioural insights that traditional analytics often miss.&lt;/p&gt;
&lt;p&gt;To learn more about exactly what we delivered, please find further information here: &lt;a href=&quot;https://www.netcel.com/insights/virtual-focus-group/&quot;&gt;Virtual Focus Group by Netcel&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;However, whilst building the Virtual Focus Group, we quickly discovered that creating and managing multiple specialised agents through the Opal user interface became time consuming and repetitive. Each customer segment needed its own agent with specific configurations, and manually creating dozens of agents was not scalable. This led to the development of the &lt;strong&gt;Agent Replicator&lt;/strong&gt;, a comprehensive toolkit that enables programmatic agent creation, management, and execution following the official Opal Agent JSON Schema specification.&lt;/p&gt;
&lt;p&gt;Whilst the Opal API does not yet support programmatic agent creation, this implementation is production ready and can be immediately integrated once Optimizely opens those endpoints. The system includes full CRUD operations, parameter validation, prompt template processing, and request context propagation, everything needed to dynamically create and orchestrate AI agents at scale.&lt;/p&gt;
&lt;h2&gt;What the Agent Replicator is in practice&lt;/h2&gt;
&lt;p&gt;In practice, the Agent Replicator is an Opal tool that enables users to create new Virtual Focus Group persona agents quickly and consistently, without leaving Opal.&lt;/p&gt;
&lt;p&gt;Once an Opal administrator has registered the Agent Replicator tool endpoint, the capability becomes available in Opal Chat and Opal Workflows. A user can provide a persona scenario and empathy map, and the tool generates a new specialised persona agent, including a tailored prompt template and the parameters needed to run it. That persona agent can then be used by the Virtual Focus Group to review content from a specific audience perspective.&lt;/p&gt;
&lt;p&gt;This shifts persona creation from a manual, repetitive task into a fast, repeatable workflow that teams can run whenever new customer segments are needed. It also keeps persona definitions consistent by generating them in a schema aligned format, which makes them easier to manage and evolve over time.&lt;/p&gt;
&lt;h2&gt;How it works&lt;/h2&gt;
&lt;p&gt;At a high level, the implementation follows a simple flow.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;You deploy the Agent Replicator as a small web service.&lt;/li&gt;
&lt;li&gt;The service exposes Opal compatible tool endpoints.&lt;/li&gt;
&lt;li&gt;An Opal administrator registers the endpoint inside Opal, which makes the tools available in Chat and Workflows.&lt;/li&gt;
&lt;li&gt;Users call the tool to generate persona agents, and then use those persona agents as part of the Virtual Focus Group experience.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;The core implementation&lt;/h2&gt;
&lt;p&gt;The Agent Replicator is built around three pieces:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Schema aligned agent models that match the Opal Agent JSON Schema&lt;/li&gt;
&lt;li&gt;Opal tool methods that create and retrieve agent definitions&lt;/li&gt;
&lt;li&gt;A small host application that exposes tool endpoints for Opal to register&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The sections below show the key parts of the implementation.&lt;/p&gt;
&lt;h2&gt;1. Adding the Opal tools package&lt;/h2&gt;
&lt;p&gt;This project references the &lt;span style=&quot;color: rgb(224, 62, 45);&quot;&gt;Optimizely.Opal.Tools&lt;/span&gt; package, which provides the infrastructure for exposing tools in a way that Opal can discover and invoke.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;ItemGroup&amp;gt;
  &amp;lt;PackageReference Include=&quot;Optimizely.Opal.Tools&quot; Version=&quot;0.4.0&quot; /&amp;gt;
&amp;lt;/ItemGroup&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. Hosting the tools and exposing endpoints for Opal&lt;/h2&gt;
&lt;p&gt;This is the important wiring in &lt;span style=&quot;color: rgb(224, 62, 45);&quot;&gt;Program.cs&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;It registers the Opal tool service, registers your tool class, and maps the tool endpoints. Once deployed, the mapped routes are what an Opal administrator registers inside Opal.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Register Opal Tool Service and Tools
builder.Services.AddOpalToolService();

// Register Agent Management Tools
builder.Services.AddOpalTool&amp;lt;AgentManagementTools&amp;gt;();

// ...

app.MapOpalTools();&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. Creating the tool that generates a new persona agent&lt;/h2&gt;
&lt;p&gt;The Agent Replicator tool methods live in &lt;span style=&quot;color: rgb(224, 62, 45);&quot;&gt;AgentManagementTools.cs&lt;/span&gt;. Each method that Opal can call is decorated with &lt;span style=&quot;color: rgb(224, 62, 45);&quot;&gt;[OpalTool]&lt;/span&gt; and has a description that Opal can surface in the interface.&lt;/p&gt;
&lt;p&gt;The method below creates a new agent definition that matches the Opal Agent JSON Schema fields, stores it for the proof of concept, and returns the created agent back to Opal.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[OpalTool(Name = &quot;create-opal-agent&quot;)]
[Description(&quot;Creates a new Opal agent within the virtual focus group. This agent can be assigned specific roles, tools, and a prompt template that defines its behavior.&quot;)]
public Task&amp;lt;object&amp;gt; CreateOpalAgent(CreateAgentParameters parameters)
{
    var agentId = Guid.NewGuid().ToString();

    var agent = new AgentDefinition
    {
        // Internal tracking fields
        Id = agentId,
        DisplayName = parameters.DisplayName,
        Role = parameters.Role,
        CreatedAt = DateTime.UtcNow,
        UpdatedAt = DateTime.UtcNow,

        // Opal schema fields
        SchemaVersion = &quot;1.0&quot;,
        AgentId = parameters.AgentId,
        AgentType = parameters.AgentType,
        Name = parameters.DisplayName,
        Description = parameters.Description,
        Version = parameters.Version,
        PromptTemplate = parameters.PromptTemplate,
        Parameters = parameters.Parameters ?? new List&amp;lt;AgentParameterDefinition&amp;gt;(),
        EnabledTools = parameters.EnabledTools ?? new List&amp;lt;string&amp;gt;(),
        Creativity = parameters.Creativity,
        InferenceType = parameters.InferenceType,
        Output = parameters.Output ?? new AgentOutputDefinition(),
        FileUrls = parameters.FileUrls ?? new List&amp;lt;string&amp;gt;(),
        AgentMetadata = parameters.AgentMetadata,
        IsActive = true,
        IsDeleted = false,
        InternalVersion = 1
    };

    if (!_agents.TryAdd(agentId, agent))
    {
        return Task.FromResult&amp;lt;object&amp;gt;(new
        {
            IsSuccess = false,
            Message = &quot;Failed to create agent due to ID collision. Please try again.&quot;
        });
    }

    return Task.FromResult&amp;lt;object&amp;gt;(new
    {
        IsSuccess = true,
        Message = $&quot;Successfully created agent &#39;{parameters.DisplayName}&#39; with agent_id &#39;{parameters.AgentId}&#39;.&quot;,
        Agent = agent
    });
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;img src=&quot;/link/c611cb43828f48a8a3582e49c163bacf.aspx&quot; /&gt;&lt;/h2&gt;
&lt;h2&gt;4. Listing persona agents inside Opal&lt;/h2&gt;
&lt;p&gt;Once users start generating persona agents, it is useful to list what exists. This tool method returns all agents that have not been deleted, ordered by creation date. This aligns with the experience of listing agents from within Opal.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[OpalTool(Name = &quot;list-opal-agents&quot;)]
[Description(&quot;Lists all available Opal agents in the virtual focus group (excluding deleted agents).&quot;)]
public Task&amp;lt;object&amp;gt; ListOpalAgents(ListAgentParameters parameters)
{
    var agents = _agents.Values
        .Where(a =&amp;gt; !a.IsDeleted)
        .OrderByDescending(a =&amp;gt; a.CreatedAt)
        .ToList();

    return Task.FromResult&amp;lt;object&amp;gt;(new
    {
        IsSuccess = true,
        Message = $&quot;Found {agents.Count} agent(s).&quot;,
        TotalAgents = agents.Count,
        Agents = agents
    });
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;img src=&quot;/link/f290478d8eb04afbb9f3544eeca396c8.aspx&quot; /&gt;&lt;/h2&gt;
&lt;h2&gt;5. Retrieving an agent and its prompt template&lt;/h2&gt;
&lt;p&gt;To support review and reuse, the next tool fetches a specific agent. This is the foundation for showing an agent&amp;rsquo;s prompt template inside Opal, which helps teams understand exactly how the persona will evaluate content.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[OpalTool(Name = &quot;get-opal-agent&quot;)]
[Description(&quot;Retrieves details of a specific Opal agent by ID or agent_id.&quot;)]
public Task&amp;lt;object&amp;gt; GetOpalAgent(GetAgentParameters parameters)
{
    var agent = _agents.Values.FirstOrDefault(a =&amp;gt;
        a.Id == parameters.AgentId ||
        a.AgentId.Equals(parameters.AgentId, StringComparison.OrdinalIgnoreCase));

    if (agent == null)
    {
        return Task.FromResult&amp;lt;object&amp;gt;(new
        {
            IsSuccess = false,
            Message = $&quot;Agent &#39;{parameters.AgentId}&#39; not found.&quot;
        });
    }

    return Task.FromResult&amp;lt;object&amp;gt;(new
    {
        IsSuccess = true,
        Message = $&quot;Successfully retrieved agent &#39;{agent.DisplayName}&#39;.&quot;,
        Agent = agent
    });
}&lt;/code&gt;&lt;br /&gt;&lt;br /&gt;&lt;img src=&quot;/link/918adb73ec3f446a9ff9d222af7dfe9d.aspx&quot; /&gt;&lt;/pre&gt;
&lt;h2&gt;6. Defining tool parameters so Opal can guide users&lt;/h2&gt;
&lt;p&gt;Tool parameter models use &lt;span style=&quot;color: rgb(224, 62, 45);&quot;&gt;[Required]&lt;/span&gt; and &lt;span style=&quot;color: rgb(224, 62, 45);&quot;&gt;[Description]&lt;/span&gt; attributes. This makes the tool self describing, helps validation, and helps Opal present a clear input contract when the tool is used in Chat or Workflows.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class CreateAgentParameters
{
    [Required]
    [Description(&quot;Human-friendly name of the new agent.&quot;)]
    public string DisplayName { get; set; } = string.Empty;

    [Required]
    [Description(&quot;What this agent does and when to use it.&quot;)]
    public string Description { get; set; } = string.Empty;

    [Required]
    [Description(&quot;Unique identifier for the agent (e.g., &#39;brand_lp_research&#39;). Use lowercase with underscores.&quot;)]
    public string AgentId { get; set; } = string.Empty;

    [Description(&quot;Type of agent: &#39;specialized&#39;, &#39;workflow&#39;, or &#39;autonomous&#39;. Default is &#39;specialized&#39;.&quot;)]
    public string AgentType { get; set; } = &quot;specialized&quot;;

    [Required]
    [Description(&quot;Prompt template with agent&#39;s instructions. Use [[parameter_name]] syntax for parameters.&quot;)]
    public string PromptTemplate { get; set; } = string.Empty;

    [Description(&quot;Agent parameters that can be passed at execution time.&quot;)]
    public List&amp;lt;AgentParameterDefinition&amp;gt;? Parameters { get; set; }

    [Description(&quot;List of tool names the agent can call (e.g., &#39;browse_web&#39;, &#39;search_web&#39;).&quot;)]
    public List&amp;lt;string&amp;gt;? EnabledTools { get; set; }

    [Description(&quot;Creativity level from 0.0 (deterministic) to 1.0 (creative). Default is 0.5.&quot;)]
    public double Creativity { get; set; } = 0.5;

    [Description(&quot;Inference type: &#39;simple&#39; or &#39;complex&#39;. Default is &#39;complex&#39;.&quot;)]
    public string InferenceType { get; set; } = &quot;complex&quot;;

    [Description(&quot;Agent version (e.g., &#39;1.0.0&#39;). Default is &#39;1.0.0&#39;.&quot;)]
    public string Version { get; set; } = &quot;1.0.0&quot;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Registering the Agent Replicator in Opal&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/link/15468a15399f429f92220ab7f2d319f9.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Once the service is deployed, an Opal administrator registers the tool provider inside Opal using the URL exposed by &lt;span style=&quot;color: rgb(224, 62, 45);&quot;&gt;MapOpalTools()&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;After registration, the tools become available within Opal:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;In Opal Chat, a user can invoke the tool to generate a new persona agent.&lt;/li&gt;
&lt;li&gt;In Opal Workflows, a step can call the same tool to create personas dynamically as part of a workflow.&lt;/li&gt;
&lt;li&gt;The newly created persona agents can then be used by the Virtual Focus Group experience to evaluate content from a specific audience perspective.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;The Agent Replicator makes it practical to scale Virtual Focus Group personas directly within Opal.&lt;/p&gt;
&lt;p&gt;By deploying a small tool service and registering it in Opal, teams can generate persona agents quickly and consistently, inspect the prompt templates those agents use, and reuse the agents across Workflows and Chat. This reduces the manual effort of building and maintaining dozens of audience segments and provides a stable foundation for future integration when Optimizely opens official agent creation endpoints.&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/Minesh-Shah/Dates/2026/1/opal-tool-agent-replicator/</guid>            <pubDate>Wed, 21 Jan 2026 11:46:47 GMT</pubDate>           <category>Blog post</category></item><item> <title>Cleaning Up Content Graph Webhooks in PaaS CMS: Scheduled Job</title>            <link>https://world.optimizely.com/blogs/Minesh-Shah/Dates/2025/12/cleaning-up-content-graph-webhooks-in-paas-cms-scheduled-job/</link>            <description>&lt;h2&gt;The Problem&lt;/h2&gt;
&lt;p&gt;Bit of a niche issue, but we are building a headless solution where the presentation layer is hosted on Netlify, when in a regular delivery cycle and work has to be tested a deployment preview is generated with a unique url, as part of the frontend deployment a new Web Hook is registered in Optimizely Content Graph so when content is published the frontend cache can be invalidated. The problem here is that with so many deployments occuring the list of web hooks can become quite long. While we could have configured deployment previews to skip webhook registration, that would have meant cached content on those environments, preventing proper testing of real-time content updates.&lt;/p&gt;
&lt;h2&gt;The Solution&lt;/h2&gt;
&lt;p&gt;Instead of compromising our testing workflow, we built an automated cleanup solution using Optimizely&#39;s Scheduled Jobs feature. This job periodically scans all registered webhooks and removes any that don&#39;t belong to our production environments, keeping the list clean and manageable.&lt;/p&gt;
&lt;h2&gt;Implementation&lt;/h2&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;using EPiServer.PlugIn;
using EPiServer.Scheduler;
using RestSharp;
using System.Text;
using System.Text.Json;

namespace Client.Cms.Infrastructure.Jobs
{
    [ScheduledPlugIn(DisplayName = &quot;Clear Graph Webhooks&quot;, GUID = &quot;D4F5A6B7-C8D9-4E0F-AB12-3456789ABCDE&quot;)]
    public class ClearGraphWebhooksJob : ScheduledJobBase
    {
        private readonly IConfiguration _configuration;
        private readonly IContentLoader _contentLoader;
        private bool _stopSignaled;

        public ClearGraphWebhooksJob(IConfiguration configuration, IContentLoader contentLoader)
        {
            _configuration = configuration;
            _contentLoader = contentLoader;
            IsStoppable = true;
        }

        public override string Execute()
        {
            try
            {
                // Fetch Content Graph Gateway Address, AppKey and Secret from appSettings
                var gatewayAddress = _configuration[&quot;Optimizely:ContentGraph:GatewayAddress&quot;];
                var appKey = _configuration[&quot;Optimizely:ContentGraph:AppKey&quot;];
                var appSecret = _configuration[&quot;Optimizely:ContentGraph:Secret&quot;];

                if (string.IsNullOrEmpty(gatewayAddress) || string.IsNullOrEmpty(appKey) || string.IsNullOrEmpty(appSecret))
                {
                    return &quot;Failed: Missing configuration values for ContentGraph&quot;;
                }

                var auth = Convert.ToBase64String(Encoding.UTF8.GetBytes($&quot;{appKey}:{appSecret}&quot;));
                var baseUrl = $&quot;{gatewayAddress}/api/webhooks&quot;;

                var client = new RestClient(new RestClientOptions(baseUrl));
                var request = new RestRequest();
                request.AddHeader(&quot;Accept&quot;, &quot;application/json&quot;);
                request.AddHeader(&quot;Authorization&quot;, $&quot;Basic {auth}&quot;);

                var response = client.GetAsync(request).GetAwaiter().GetResult();

                if (!response.IsSuccessful)
                {
                    OnStatusChanged($&quot;Failed to fetch webhooks: {response.StatusCode} {response.ErrorMessage}&quot;);
                    return $&quot;Failed: {response.StatusCode} - {response.ErrorMessage}&quot;;
                }

                var json = response.Content;
                var webhooks = JsonSerializer.Deserialize&amp;lt;List&amp;lt;WebhookItem&amp;gt;&amp;gt;(json, new JsonSerializerOptions
                {
                    PropertyNameCaseInsensitive = true
                });

                if (webhooks == null || webhooks.Count == 0)
                {
                    OnStatusChanged(&quot;No webhooks found or failed to deserialize response.&quot;);
                    return &quot;Success: No webhooks to process&quot;;
                }

                int deletedCount = 0;
                int skippedCount = 0;

                foreach (var webhook in webhooks)
                {
                    if (_stopSignaled)
                    {
                        return $&quot;Stopped: Processed {deletedCount} deletions, {skippedCount} skipped&quot;;
                    }

                    var urls = GetUrls().ToList();
                    if (urls.Any(url =&amp;gt; webhook.Request.Url.Contains(url)))
                    {
                        OnStatusChanged($&quot;Skipping webhook {webhook.Id} with URL {webhook.Request.Url}&quot;);
                        skippedCount++;
                        continue;
                    }

                    DeleteWebhook(webhook.Id.ToString(), auth, baseUrl);
                    deletedCount++;
                }

                return $&quot;Success: Deleted {deletedCount} webhooks, skipped {skippedCount}&quot;;
            }
            catch (Exception ex)
            {
                OnStatusChanged($&quot;Error: {ex.Message}&quot;);
                return $&quot;Failed: {ex.Message}&quot;;
            }
        }

        public override void Stop()
        {
            _stopSignaled = true;
            base.Stop();
        }

        private void DeleteWebhook(string id, string auth, string baseUrl)
        {
            var client = new RestClient(new RestClientOptions($&quot;{baseUrl}/{id}&quot;));
            var request = new RestRequest();
            request.AddHeader(&quot;Authorization&quot;, $&quot;Basic {auth}&quot;);

            var response = client.DeleteAsync(request).GetAwaiter().GetResult();

            if (!response.IsSuccessful)
            {
                OnStatusChanged($&quot;Failed to delete webhook {id}: {response.StatusCode} {response.ErrorMessage}&quot;);
            }
            else
            {
                OnStatusChanged($&quot;Deleted webhook {id}&quot;);
            }
        }

        // Fetch URLs from Site Config Block
        private IEnumerable&amp;lt;string&amp;gt; GetUrls()
        {
            var globalSettingsBlock = _contentLoader
                .GetChildren&amp;lt;OptimizelySettingsBlock&amp;gt;(ContentReference.GlobalBlockFolder)
                .FirstOrDefault();

            return globalSettingsBlock?.FrontendUrls ?? Enumerable.Empty&amp;lt;string&amp;gt;;
        }
    }

    public class WebhookRequest
    {
        public string Url { get; set; }
        public string Method { get; set; }
        public Dictionary&amp;lt;string, string&amp;gt; Headers { get; set; }
    }

    public class WebhookItem
    {
        public Guid Id { get; set; }
        public WebhookRequest Request { get; set; }
        public string Preset { get; set; }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;How It Works&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Authentication: The job authenticates with Content Graph using your AppKey and Secret from configuration&lt;/li&gt;
&lt;li&gt;Fetch Webhooks: It retrieves all registered webhooks via the Content Graph API&lt;/li&gt;
&lt;li&gt;Filter by URL: The job checks each webhook URL against a configurable list of production URLs stored in a global settings block&lt;/li&gt;
&lt;li&gt;Smart Deletion: Webhooks matching production URLs are preserved; all others are deleted&lt;/li&gt;
&lt;li&gt;Progress Tracking: The job reports its progress and provides a summary of deletions and skips&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Key Features&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Content-Driven Configuration: Production URLs are stored in an Optimizely block, making them editable by content editors without code changes&lt;/li&gt;
&lt;li&gt;Stoppable: The job implements IsStoppable = true and properly handles stop signals for graceful cancellation&lt;/li&gt;
&lt;li&gt;Comprehensive Logging: Uses OnStatusChanged() to provide detailed progress updates visible in the Scheduled Jobs interface&lt;/li&gt;
&lt;li&gt;Error Handling: Robust try-catch blocks and validation ensure the job fails gracefully with meaningful error messages&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Benefits&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Deployment previews can register webhooks and test real-time content updates&lt;/li&gt;
&lt;li&gt;The webhook list stays clean through automated maintenance&lt;/li&gt;
&lt;li&gt;No manual intervention required&lt;/li&gt;
&lt;li&gt;Content editors control which URLs to preserve through the CMS interface&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;By leveraging Optimizely&#39;s Scheduled Jobs framework, we&#39;ve created an automated solution that maintains a clean webhook registry while preserving full functionality across all environments. This approach is far more maintainable than manually managing webhooks or compromising testing capabilities on deployment previews.&lt;/p&gt;
&lt;p&gt;The scheduled job can run daily, weekly, or at any interval that suits your deployment frequency, ensuring your Content Graph webhook list remains lean and manageable.&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/Minesh-Shah/Dates/2025/12/cleaning-up-content-graph-webhooks-in-paas-cms-scheduled-job/</guid>            <pubDate>Wed, 17 Dec 2025 12:42:30 GMT</pubDate>           <category>Blog post</category></item><item> <title>Building a Lightweight Optimizely SaaS CMS Solution with 11ty</title>            <link>https://world.optimizely.com/blogs/Minesh-Shah/Dates/2025/12/building-a-lightweight-optimizely-saas-cms-solution-with-11ty/</link>            <description>&lt;figure&gt;&lt;img src=&quot;/link/68c9d668701f42a49f0f5d684cf2374b.aspx&quot; alt=&quot;&quot; width=&quot;1919&quot; height=&quot;707&quot; /&gt;&lt;/figure&gt;
&lt;p&gt;Modern web development often requires striking a difficult balance between site performance and the flexibility needed by content editors. To address this, myself and fellow Optimizely OMVP &lt;strong&gt;Graham Carr&lt;/strong&gt; embarked on a Proof of Concept (POC) to build a solution that prioritises speed and simplicity, while fully leveraging the powerful capabilities of the Optimizely SaaS CMS.&lt;/p&gt;
&lt;h2&gt;The Challenge: Performance vs. Preview&lt;/h2&gt;
&lt;p&gt;Our Frontend Architect / Lead, &lt;strong&gt;Allyn Thomas&lt;/strong&gt;, set a clear challenge: find a solution with minimal bloat that is statically generated but still fully compatible with the CMS&#39;s live preview capabilities. While frameworks like Next.js are popular, they often come with a significant amount of client-side JavaScript (hydration) that isn&#39;t always necessary for content-heavy sites. We wanted something leaner.&lt;/p&gt;
&lt;p&gt;The goal was to build a site that is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Statically Generated&lt;/strong&gt;: For blazing-fast load times and security.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lightweight&lt;/strong&gt;: Avoiding the &quot;hydration tax&quot; of heavier frameworks.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Editor-Friendly&lt;/strong&gt;: Allowing editors to see live previews of their content in the Optimizely Visual Builder before publishing.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The Solution: 11ty Meets Optimizely SaaS CMS&lt;/h2&gt;
&lt;p&gt;We chose &lt;strong&gt;&lt;a href=&quot;https://www.11ty.dev/&quot;&gt;11ty (Eleventy)&lt;/a&gt;&lt;/strong&gt; as our Static Site Generator (SSG). 11ty is renowned for its simplicity and flexibility. Unlike other frameworks that force you into a specific client-side architecture, 11ty gives you full control over the output. It generates pure HTML, which is exactly what we needed to keep the bloat to a minimum.&lt;/p&gt;
&lt;p&gt;To connect 11ty with Optimizely, we utilised the &lt;strong&gt;&lt;a href=&quot;https://www.optimizely.com/products/content-management/&quot;&gt;Optimizely SaaS CMS&lt;/a&gt;&lt;/strong&gt; and the &lt;strong&gt;&lt;a href=&quot;https://github.com/episerver/content-js-sdk&quot;&gt;Content JS SDK&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;h3&gt;Optimizely SaaS CMS &amp;amp; Content Graph&lt;/h3&gt;
&lt;p&gt;Our solution centers on the Optimizely SaaS CMS as the headless content repository. We use &lt;strong&gt;&lt;a href=&quot;https://docs.developers.optimizely.com/platform-optimizely/docs/introduction-optimizely-graph&quot;&gt;Optimizely Content Graph&lt;/a&gt;&lt;/strong&gt;, a high-performance GraphQL API, to fetch content. This lets us query exactly what we need, when we need it, keeping our build process efficient.&lt;/p&gt;
&lt;h3&gt;The Content JS SDK&lt;/h3&gt;
&lt;p&gt;The &lt;strong&gt;Content JS SDK&lt;/strong&gt; was essential to our architecture. It allowed us to define our content models directly in code using a type-safe builder pattern.&lt;/p&gt;
&lt;p&gt;For example, defining a &quot;Button Block&quot; becomes a straightforward TypeScript definition:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { contentType } from &#39;@optimizely/cms-sdk&#39;;

export const ButtonBlock = contentType({
  key: &#39;ButtonBlock&#39;,
  baseType: &#39;_block&#39;,
  displayName: &#39;Button Block&#39;,
  properties: {
    Text: { type: &#39;string&#39;, displayName: &#39;Text&#39; },
    Url: { type: &#39;url&#39;, displayName: &#39;Url&#39; },
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This approach ensures that our codebase stays in sync with our content models, providing excellent developer ergonomics and reducing runtime errors.&lt;/p&gt;
&lt;h3&gt;Structured for Scalability&lt;/h3&gt;
&lt;p&gt;To maintain sanity as the project grows, we adopted a strict directory structure that mirrors both our content strategy and modern component design principles.&lt;/p&gt;
&lt;h4&gt;The Models Folder&lt;/h4&gt;
&lt;p&gt;Our src/models directory is the source of truth for all content definitions. We organized it to reflect the different types of content in the CMS:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;src/models/pages/&lt;/strong&gt;: Definitions for full pages (e.g., ArticlePage, LandingPage).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;src/models/experiences/&lt;/strong&gt;: Composition-based content types used in the Visual Builder.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;src/models/components/&lt;/strong&gt;: Reusable content blocks (e.g., ButtonBlock, TextBlock).&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Atomic Component Library&lt;/h4&gt;
&lt;p&gt;For the frontend implementation, we embraced &lt;strong&gt;&lt;a href=&quot;https://bradfrost.com/blog/post/atomic-web-design/&quot;&gt;Atomic Design&lt;/a&gt;&lt;/strong&gt;. This methodology breaks the UI down into its smallest fundamental units, ensuring consistency and reusability across the site.&lt;/p&gt;
&lt;p&gt;Our src/components directory is structured as follows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;atoms/&lt;/strong&gt;: Basic building blocks like buttons, inputs, and text blocks.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;molecules/&lt;/strong&gt;: Groups of atoms working together (e.g., a search form).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;organisms/&lt;/strong&gt;: Complex UI sections like headers, footers, and hero banners.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;templates/&lt;/strong&gt;: Page-level layouts that stitch everything together.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This clear separation of concerns allows developers to work on individual components in isolation while ensuring they fit perfectly into the larger system. It also naturally encourages adherence to the &lt;strong&gt;Single Responsibility Principle&lt;/strong&gt;, as each atom, molecule, or organism has a distinct and focused purpose.&lt;/p&gt;
&lt;h3&gt;Dynamic Routing with 11ty Pagination&lt;/h3&gt;
&lt;p&gt;One of the challenges with a static site generator is mapping dynamic content from a CMS to static routes. 11ty handles this elegantly through its &lt;strong&gt;pagination&lt;/strong&gt; feature. We treat our entire content repository as a single &quot;paginated&quot; data set, where each item becomes a page.&lt;/p&gt;
&lt;p&gt;First, we fetch all routable content in a global data file, src/_data/routes.js:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// src/_data/routes.js

module.exports = async function () {
  // ... client setup ...

  // Fetch all content items that have a URL
  const query = getRoutePagesQuery(blockFragments);
  const data = await client.request(query);

  // Filter out items without a default URL
  const pages = data._Content.items.filter(item =&amp;gt;
    item._metadata &amp;amp;&amp;amp;
    item._metadata.url &amp;amp;&amp;amp;
    item._metadata.url.default
  );

  return pages;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then, we create a single template, src/pages.11ty.ts, that iterates over this data. By setting size: 1, 11ty generates a separate HTML file for each item in the routes array. The permalink function ensures the file is saved to the correct path as defined in the CMS.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// src/pages.11ty.ts

export const data = {
  pagination: {
    data: &#39;routes&#39;,
    size: 1,
    alias: &#39;contentItem&#39;,
    addAllPagesToCollections: true,
  },
  layout: &#39;base.11ty.ts&#39;,
  permalink: (data: any) =&amp;gt; {
    const item = data.pagination.items[0];
    return item._metadata.url.default;
  },
  // ...
};

export function render(data: any): string {
  const item = data.pagination.items[0];
  return ComponentFactory(item);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This pattern is incredibly powerful. It means we don&#39;t need to manually configure routes or create separate templates for every page type. As editors add new pages in Optimizely, 11ty automatically discovers them and builds the corresponding static pages.&lt;/p&gt;
&lt;h3&gt;The Component Factory&lt;/h3&gt;
&lt;p&gt;To handle the diverse range of content types returned by the CMS, we implemented a &lt;strong&gt;Component Factory&lt;/strong&gt;. This function acts as a dispatcher, inspecting the __typename or content type of each item and rendering the appropriate component.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// src/components/ComponentFactory.ts

export function ComponentFactory(content: ContentItem): string {
  // Handle CompositionComponentNode wrapper
  if (content.component) {
    return ComponentFactory(content.component);
  }

  const types = content._metadata?.types || [];
  const typename = content.__typename || &#39;&#39;;

  // Dynamic component lookup from registry
  const ComponentView = getView(typename);
  if (ComponentView) {
    return ComponentView(content);
  }

  // Fallback for unknown types
  return `
    &amp;lt;div class=&quot;unknown-component&quot; data-epi-block-id=&quot;${content._metadata?.key}&quot;&amp;gt;
      &amp;lt;h3&amp;gt;${content.Title || content._metadata?.displayName || &#39;Unknown&#39;}&amp;lt;/h3&amp;gt;
      &amp;lt;p&amp;gt;Unknown component type: ${typename}&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
  `;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This architecture adheres to the &lt;strong&gt;Open/Closed Principle&lt;/strong&gt;. We can add new component types to our registry without modifying the core routing or rendering logic.&lt;/p&gt;
&lt;h3&gt;Enabling Live Previews&lt;/h3&gt;
&lt;p&gt;The most critical part of this POC was ensuring that 11ty&#39;s static nature didn&#39;t hinder the editing experience. We implemented a dedicated &lt;strong&gt;Express Preview Server&lt;/strong&gt; that runs alongside our 11ty build.&lt;/p&gt;
&lt;p&gt;This server acts as a dynamic bridge between the Optimizely Visual Builder and our static templates.&lt;/p&gt;
&lt;h4&gt;The Express Server Implementation&lt;/h4&gt;
&lt;p&gt;We chose &lt;strong&gt;&lt;a href=&quot;https://expressjs.com/&quot;&gt;Express&lt;/a&gt;&lt;/strong&gt; for its robustness and ease of use. The server handles several key responsibilities:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;HMAC Authentication&lt;/strong&gt;: Securely fetches &lt;em&gt;draft&lt;/em&gt; content using preview tokens.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Context Awareness&lt;/strong&gt;: Toggles between &quot;Edit Mode&quot; (injecting data-epi-* attributes) and &quot;Preview Mode&quot;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Real-time Updates&lt;/strong&gt;: Listens for webhooks to trigger 11ty rebuilds when content is published.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Here is a glimpse of how we handle preview requests in src/preview/server.ts:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// src/preview/server.ts

app.get(&#39;/preview/:contentKey&#39;, async (req: Request, res: Response) =&amp;gt; {
  const { contentKey } = req.params;
  const { preview_token, ctx } = req.query;

  try {
    // 1. Set context mode for Visual Builder (edit vs preview)
    const contextMode =
      ctx === &#39;edit&#39; ? &#39;edit&#39; :
      ctx === &#39;preview&#39; ? &#39;preview&#39; :
      null;
    setContextMode(contextMode);

    // 2. Set preview token to enable access to draft content
    if (preview_token &amp;amp;&amp;amp; typeof preview_token === &#39;string&#39;) {
      previewClient.setPreviewToken(preview_token);
    }

    // 3. Fetch content dynamically
    const content = await previewClient.getContentByKey(contentKey);

    if (!content) {
      return res.status(404).send(renderNotFound(contentKey));
    }

    // 4. Render using the same shared templates as 11ty
    const html = renderContent(content);
    res.send(html);
  } catch (error) {
    res.status(500).send(renderError(&#39;Server Error&#39;, String(error)));
  }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Handling Webhooks&lt;/h4&gt;
&lt;p&gt;To keep the static site in sync with published content, we also exposed a webhook endpoint. When an editor publishes a page, Optimizely notifies our server, which triggers a debounced rebuild of the 11ty site.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// src/preview/server.ts

app.post(&#39;/webhook/content-published&#39;, (req: Request, res: Response) =&amp;gt; {
  // ... validation ...

  // Debounce rebuilds to handle rapid updates efficiently
  if (rebuildTimeout) {
    clearTimeout(rebuildTimeout);
  }

  rebuildTimeout = setTimeout(() =&amp;gt; {
    triggerRebuild();
  }, REBUILD_DEBOUNCE_MS);

  res.json({ status: &#39;ok&#39;, message: &#39;Rebuild scheduled&#39; });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This dual approach gives us the best of both worlds: a static site for production visitors and a dynamic, interactive preview for editors.&lt;/p&gt;
&lt;h2&gt;Why 11ty Over Next.js?&lt;/h2&gt;
&lt;p&gt;While Next.js is a fantastic framework, for this specific use case, 11ty offered distinct advantages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Zero Client-Side JS by Default&lt;/strong&gt;: 11ty doesn&#39;t assume you want a Single Page Application (SPA). It serves HTML. If you want JavaScript, you add it. This results in significantly smaller bundle sizes and faster Time to Interactive (TTI).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build Speed&lt;/strong&gt;: 11ty is incredibly fast at building static pages, which is crucial when scaling to thousands of pages.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Simplicity&lt;/strong&gt;: The learning curve is gentler, and the architecture is easier to reason about. There is no complex hydration logic to debug.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;This POC has demonstrated that you don&#39;t need a heavy JavaScript framework to build a modern, dynamic, and editor-friendly website with Optimizely SaaS CMS. By combining the raw power and simplicity of 11ty with the robust APIs of Optimizely, we have delivered a solution that delights both developers and content editors.&lt;/p&gt;
&lt;p&gt;We have achieved the best of both worlds: the performance of a static site with the dynamic editing capabilities of a CMS-driven application.&lt;/p&gt;
&lt;p&gt;You can view the final result here: &lt;a href=&quot;https://11ty-netcel-saas.netlify.app/&quot;&gt;11ty-netcel-saas.netlify.app&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The full source code is available on GitHub: &lt;a href=&quot;https://github.com/MineshS/11ty/&quot;&gt;github.com/MineshS/11ty&lt;/a&gt; (Currently a private repo but please do reach out with Git Username so can add)&amp;nbsp;&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/Minesh-Shah/Dates/2025/12/building-a-lightweight-optimizely-saas-cms-solution-with-11ty/</guid>            <pubDate>Wed, 03 Dec 2025 10:59:17 GMT</pubDate>           <category>Blog post</category></item><item> <title>Extending Application Insights in an Optimizely PaaS CMS Solution</title>            <link>https://world.optimizely.com/blogs/Minesh-Shah/Dates/2025/9/extending-application-insights-in-an-optimizely-paas-cms-solution/</link>            <description>&lt;p&gt;As part of a recent Optimizely DXP project, I needed richer telemetry from the Content Delivery API than the default Application Insights integration provides. The aim was to capture custom properties such as content IDs and cache status so that we could slice and query API performance in more detail. Here is the approach I took.&lt;/p&gt;
&lt;h2&gt;Creating a Custom Telemetry Processor&lt;/h2&gt;
&lt;p&gt;The first step was to create a custom &#39;ITelemetryProcessor&#39; implementation to enrich telemetry for Content Delivery API requests. This processor inspects each telemetry item, checks whether it relates to the Content Delivery API, and then adds custom properties such as &#39;ApiType&#39;, &#39;IsOptimizelyApi&#39;, and the extracted &#39;ContentId&#39;.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public class ContentDeliveryApiTelemetryProcessor(ITelemetryProcessor next) : ITelemetryProcessor
{
    private readonly ITelemetryProcessor _next = next;

    public void Process(ITelemetry item)
    {
        if (item is RequestTelemetry requestTelemetry &amp;amp;&amp;amp;
            IsContentDeliveryApiRequest(requestTelemetry.Name))
        {
            requestTelemetry.Properties[&quot;ApiType&quot;] = &quot;ContentDeliveryApi&quot;;
            requestTelemetry.Properties[&quot;IsOptimizelyApi&quot;] = &quot;true&quot;;

            var contentId = ExtractContentIdFromUrl(requestTelemetry.Url?.ToString());
            if (!string.IsNullOrEmpty(contentId))
            {
                requestTelemetry.Properties[&quot;ContentId&quot;] = contentId;
            }
        }

        _next.Process(item);
    }

    // Helper methods to identify API requests and extract IDs omitted for brevity
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Register the processor with Application Insights in &#39;startup.cs&#39; or the Program configuration so that it runs for every request.&lt;/p&gt;
&lt;h2&gt;Adding Request and Response Logging Middleware&lt;/h2&gt;
&lt;p&gt;To go further, I introduced a middleware component to log the full request and response cycle for API calls. This middleware captures request and response bodies, headers, and timing, and sends a single &#39;RequestTelementry&#39; entry to Application Insights with custom properties such as cache status.&lt;/p&gt;
&lt;p&gt;Below is a reduced example focusing on adding custom properties to a tracked request:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public class RequestResponseLoggingMiddleware(RequestDelegate next, TelemetryClient telemetryClient)
{
    private readonly RequestDelegate _next = next;
    private readonly TelemetryClient _telemetryClient = telemetryClient;

    public async Task InvokeAsync(HttpContext context)
    {
        // &amp;hellip; capture request/response details &amp;hellip;

        var telemetry = new RequestTelemetry
        {
            Name = $&quot;{context.Request.Method} {context.Request.Path}&quot;,
            Url = new Uri($&quot;{context.Request.Scheme}://{context.Request.Host}{context.Request.Path}{context.Request.QueryString}&quot;),
            Duration = stopwatch.Elapsed,
            ResponseCode = context.Response.StatusCode.ToString(),
            Success = context.Response.StatusCode &amp;lt; 400
        };

        telemetry.Properties[&quot;ApiType&quot;] = &quot;ContentDeliveryApi&quot;;
        telemetry.Properties[&quot;IsOptimizelyApi&quot;] = &quot;true&quot;;
        telemetry.Properties[&quot;CacheStatus&quot;] = cacheStatus;
        telemetry.Properties[&quot;RequestHeader_UserAgent&quot;] = context.Request.Headers[&quot;User-Agent&quot;];

        _telemetryClient.TrackRequest(telemetry);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Register the middleware in &#39;Startup.cs&#39;:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;app.UseMiddleware&amp;lt;RequestResponseLoggingMiddleware&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And configure logging levels in &#39;appSettings.json:&lt;/p&gt;
&lt;pre class=&quot;language-html&quot;&gt;&lt;code&gt;&quot;Logging&quot;: {
  &quot;LogLevel&quot;: {
    &quot;Default&quot;: &quot;Warning&quot;,
    &quot;Cms.Infrastructure.Middleware.RequestResponseLoggingMiddleware&quot;: &quot;Information&quot;
  },
  &quot;ApplicationInsights&quot;: {
    &quot;LogLevel&quot;: {
      &quot;Default&quot;: &quot;Information&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This approach provides structured, queryable telemetry for each Content Delivery API call.&lt;/p&gt;
&lt;h2&gt;Example Output&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/link/0598b8872d424dbca0c1d0de85e7459a.aspx&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Benefits of Extended Telemetry&lt;/h2&gt;
&lt;p&gt;Extending telemetry offers several practical advantages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Deeper diagnostics&lt;/strong&gt;: You can quickly isolate performance bottlenecks, such as slow content retrieval or cache misses.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Better observability&lt;/strong&gt;: Custom properties allow you to group or filter requests by content item, making it easier to identify patterns.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Operational insight&lt;/strong&gt;: Logging cache status and request headers reveals how your CDN, edge caching, and client applications interact with the CMS.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In large Optimizely solutions, where API calls power multiple front ends, this level of detail is invaluable for proactive monitoring.&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/Minesh-Shah/Dates/2025/9/extending-application-insights-in-an-optimizely-paas-cms-solution/</guid>            <pubDate>Wed, 24 Sep 2025 23:19:07 GMT</pubDate>           <category>Blog post</category></item><item> <title>Dynamic CSP Management for Headless and Hybrid Optimizely CMS with Next.js</title>            <link>https://world.optimizely.com/blogs/Minesh-Shah/Dates/2025/9/dynamic-content-security-policy-management-with-optimizely-cms-in-headless-architecture/</link>            <description>&lt;p&gt;In the evolving realm of web security, Content Security Policy (CSP) is essential for defending against XSS and injection attacks. Traditional approaches often fall short because policies are embedded in code and hard to coordinate across environments.&lt;/p&gt;
&lt;p&gt;Optimizely CMS supports dynamic CSP in a headed setup, where the CMS renders pages. This is straightforward with third-party modules such as the &lt;a href=&quot;https://github.com/GeekInTheNorth/Stott.Security.Optimizely&quot;&gt;Stott Security Optimizely module&lt;/a&gt;. Installation is simple, configuration is clear, and the headers flow with the response.&lt;/p&gt;
&lt;p&gt;Headless and hybrid architectures introduce a gap. The frontend is separate, so CSP headers often end up fixed in the application tier. That limits agility for security and content teams.&lt;/p&gt;
&lt;p&gt;By combining the Stott Security module with Next.js middleware, dynamic CSP becomes possible in headless and hybrid environments. Policies remain CMS-managed, even when the site is delivered through a decoupled frontend.&lt;/p&gt;
&lt;h2&gt;What we&amp;rsquo;re solving&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Policy changes without releases:&lt;/strong&gt; Update CSP in CMS and apply instantly.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Environment variations:&lt;/strong&gt; Manage dev, staging, and production policies centrally.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cross-team flow:&lt;/strong&gt; Security and marketing can adjust policies without blocking frontend teams.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Headed vs headless&lt;/h2&gt;
&lt;p&gt;In headed Optimizely CMS, dynamic CSP is native to the page response. This remains a solid and current pattern. In headless and hybrid setups, the CMS serves content APIs while a separate frontend handles rendering. Without a bridge, CSP is usually hardcoded in that frontend. The approach below restores the same dynamic control you expect in headed.&lt;/p&gt;
&lt;h2&gt;Architecture&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;User Request &amp;rarr; Next.js Middleware &amp;rarr; Stott Security API &amp;rarr; Optimizely CMS &amp;rarr; Dynamic CSP Headers&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;The user requests a page from the Next.js app.&lt;/li&gt;
&lt;li&gt;Middleware intercepts the request.&lt;/li&gt;
&lt;li&gt;Middleware calls the Stott Security endpoint to retrieve current headers.&lt;/li&gt;
&lt;li&gt;Headers are applied to the response before it is returned.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Implementation in Next.js middleware&lt;/h2&gt;
&lt;p&gt;The example below removes development-only branches. It always sources headers from CMS for consistency.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { NextRequest } from &quot;next/server&quot;;

export async function middleware(request: NextRequest) {
  const baseCmsUrl = process.env.DXP_URL || &quot;https://localhost:5000&quot;;
  const headersUrl = `${baseCmsUrl}/stott.security.optimizely/api/compiled-headers/list/`;

  // Create or reuse a Response object from your app logic prior to this point.
  // For illustration we assume you already have a &#39;response&#39; to enrich.
  let response = new Response();

  try {
    const cmsResponse = await fetch(headersUrl, {
      headers: { &quot;Content-Type&quot;: &quot;application/json&quot; },
      // Consider short caching and timeouts at edge to keep latency tight
    });

    if (!cmsResponse.ok) {
      // Replace with your logger
      console.error({
        status: cmsResponse.status,
        statusText: cmsResponse.statusText,
        url: headersUrl,
        path: request.nextUrl.pathname,
        context: &quot;middleware - security headers fetch failed&quot;
      });
      return response; // graceful fallback
    }

    const securityHeaders: Array&amp;lt;{ key: string; value: string }&amp;gt; = await cmsResponse.json();

    securityHeaders.forEach(h =&amp;gt; response.headers.set(h.key, h.value));

    return response;
  } catch (error) {
    // Replace with your logger
    console.error({
      error,
      url: headersUrl,
      path: request.nextUrl.pathname,
      context: &quot;middleware - security headers processing error&quot;
    });
    return response; // fail safely
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In your production app, you&amp;rsquo;ll attach these headers to the actual page or asset response you are returning from Next.js. Keep the fetch lean, and prefer edge runtime where possible.&lt;/p&gt;
&lt;h2&gt;Key benefits&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CMS-managed security:&lt;/strong&gt; Update CSP without code changes. See the effect immediately.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Environment flexibility:&lt;/strong&gt; Dev can be relaxed, staging can mirror production with test tools, production can stay strict.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Operational speed:&lt;/strong&gt; Emergency updates and new integrations go live from CMS.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Resilience:&lt;/strong&gt; If the CMS API is unavailable, the site continues with safe defaults.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Developer experience:&lt;/strong&gt; Clear separation of concerns. No more per-environment hardcoding.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Practical considerations&lt;/h2&gt;
&lt;h3&gt;Performance&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Enable caching at the CMS endpoint for short periods.&lt;/li&gt;
&lt;li&gt;Return compact JSON from the Stott Security module.&lt;/li&gt;
&lt;li&gt;Use edge middleware for minimal latency.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Security&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Rely on the module&amp;rsquo;s validation to prevent invalid CSP syntax.&lt;/li&gt;
&lt;li&gt;Keep an audit trail of edits to support compliance.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Workflow&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Document your policy structure in CMS.&lt;/li&gt;
&lt;li&gt;Align environments through content, not code.&lt;/li&gt;
&lt;li&gt;Test policy variations in staging, then promote.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Why the Stott Security module&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Comfortable CMS UI for policy editing.&lt;/li&gt;
&lt;li&gt;Validation before changes go live.&lt;/li&gt;
&lt;li&gt;CSP violation reporting integration.&lt;/li&gt;
&lt;li&gt;Support for multiple sites and additional security headers.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Explore the module on GitHub: &lt;a href=&quot;https://github.com/GeekInTheNorth/Stott.Security.Optimizely&quot;&gt;GeekInTheNorth/Stott.Security.Optimizely&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Headed Optimizely CMS already delivers dynamic CSP with ease. The approach above brings the same control to headless and hybrid builds. Policies stay in CMS, the frontend stays decoupled, and security remains responsive to change.&lt;/p&gt;
&lt;p&gt;With Next.js middleware, Optimizely CMS, and the Stott Security module, CSP moves from static configuration to a manageable, collaborative capability. It works with the speed of your teams and the realities of modern delivery.&lt;/p&gt;
&lt;h3&gt;Resources&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/GeekInTheNorth/Stott.Security.Optimizely&quot;&gt;Stott Security Optimizely Module&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://nextjs.org/docs/app/building-your-application/routing/middleware&quot;&gt;Next.js Middleware&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP&quot;&gt;MDN: Content Security Policy&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>            <guid>https://world.optimizely.com/blogs/Minesh-Shah/Dates/2025/9/dynamic-content-security-policy-management-with-optimizely-cms-in-headless-architecture/</guid>            <pubDate>Mon, 08 Sep 2025 09:43:10 GMT</pubDate>           <category>Blog post</category></item><item> <title>The Sweet Spot: Hybrid Headless Architecture</title>            <link>https://world.optimizely.com/blogs/Minesh-Shah/Dates/2025/9/the-sweet-spot-hybrid-headless-architecture/</link>            <description>&lt;p&gt;When it comes to content management architecture, the pendulum often swings between tightly coupled &amp;ldquo;headed&amp;rdquo; CMS setups and the flexibility of fully headless. In reality, many organizations are finding the sweet spot in between:&amp;nbsp;&lt;strong&gt;hybrid headless&lt;/strong&gt;. Pair that with &lt;strong&gt;Optimizely PaaS CMS&lt;/strong&gt; and &lt;strong&gt;GraphQL&lt;/strong&gt;, and you get an architecture that balances agility, scalability, and most importantly, content editor empowerment.&lt;/p&gt;
&lt;h2&gt;The Three Approaches: Headed, Headless, and Hybrid&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Traditional headed CMS&lt;/strong&gt;&lt;br /&gt;CMS and presentation are tightly coupled. It is easy for content editors and fast for classic web builds, but it struggles to expand into multi-channel experiences.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fully headless CMS&lt;/strong&gt;&lt;br /&gt;The CMS acts purely as a content repository, serving JSON via APIs. Front-ends can be built with any technology, but editors lose in-context previewing, and development teams bear additional complexity.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hybrid headless CMS&lt;/strong&gt;&lt;br /&gt;With Optimizely, you retain in-context editing while exposing content via APIs. Add &lt;strong&gt;GraphQL&lt;/strong&gt;, and your content architecture gains flexibility, typing, and API efficiency, all while keeping the editorial experience intact.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Why Hybrid Headless with Optimizely PaaS + GraphQL Wins&lt;/h2&gt;
&lt;h3&gt;1. Editor-first without compromise&lt;/h3&gt;
&lt;p&gt;Editors stay in their familiar Optimizely interface, including on-page preview, content workflows, and drag-and-drop editing, without workarounds required in fully headless setups.&lt;/p&gt;
&lt;h3&gt;2. Developer agility with GraphQL&lt;/h3&gt;
&lt;p&gt;GraphQL gives developers precise, efficient data access in a single request. It is strongly typed, self-documenting, and accelerates onboarding as well as development velocity.&lt;/p&gt;
&lt;h3&gt;3. Why Optimizely PaaS CMS&lt;/h3&gt;
&lt;p&gt;One of the main advantages of running on PaaS rather than SaaS is the additional control it gives development teams. With PaaS, you are not limited to purely configuration-driven approaches. You can implement bespoke functionality, advanced business logic, and integrations that go beyond what SaaS-only solutions allow.&lt;/p&gt;
&lt;p&gt;PaaS also provides deeper control over &lt;strong&gt;GraphQL&lt;/strong&gt; and &lt;strong&gt;custom properties&lt;/strong&gt;, which can be crucial when modelling complex content or delivering advanced omnichannel experiences. This level of flexibility ensures you can tailor the platform to your unique business needs rather than shaping your requirements around the platform.&lt;/p&gt;
&lt;p&gt;For organizations with more sophisticated demands, this makes PaaS the better fit. You get the stability and scalability of Optimizely&amp;rsquo;s managed environment while still retaining the freedom to innovate and customize.&lt;/p&gt;
&lt;h3&gt;4. Scale baked in with Optimizely One&lt;/h3&gt;
&lt;p&gt;Optimizely&amp;rsquo;s DXP handles infrastructure, scaling, uptime, and resilience. Crucially, infrastructure is managed directly by Optimizely, so your team does not need to worry about patching servers or handling traffic spikes.&lt;/p&gt;
&lt;p&gt;What is new is that you now also have the option to &lt;strong&gt;host your decoupled presentation layer on Optimizely&lt;/strong&gt;. This means you can run your React, Next.js, or other modern front-end applications side by side with your CMS in a fully managed environment. Optimizely takes care of hosting, CDN, and security, the same way it already does for your CMS (If PaaS CMS, inhouse engineering team will need to handle upgrades, SaaS CMS is fully managed by Optimizely). This makes hybrid headless even more compelling, since both your content layer and your front-end layer can be managed under one roof. For more background, see my earlier take on Frontend Hosting (&lt;a href=&quot;/link/78cf3461994c4df084c5ae460b447ed0.aspx&quot;&gt;early thoughts and key questions&lt;/a&gt;).&lt;/p&gt;
&lt;h3&gt;5. Reduced complexity vs. fully headless&lt;/h3&gt;
&lt;p&gt;Going fully headless often means every template, component, and preview experience has to be engineered from scratch. While that gives developers maximum flexibility, it also creates a lot of operational overhead.&lt;/p&gt;
&lt;p&gt;Hybrid headless takes a more balanced approach. Editors retain control over templates, layouts, and content structures directly in the CMS, without needing to wait on engineering teams for every change. This lets marketing teams spin up new landing pages, adjust layouts, or test different content formats on their own, while still benefiting from in-context previewing.&lt;/p&gt;
&lt;p&gt;When it comes to personalization, hybrid headless works hand in hand with Optimizely&amp;rsquo;s &lt;strong&gt;Personalization Platform&lt;/strong&gt; and &lt;strong&gt;Web Experimentation Platform&lt;/strong&gt;. Editors and marketers can create personalized variants or run experiments without relying heavily on developers to rewire the front end. The CMS ensures content and templates are structured in a way that these platforms can easily consume, so experimentation and targeting become smoother across channels.&lt;/p&gt;
&lt;p&gt;The result is less engineering effort, faster time to market, and greater autonomy for editors and marketers to deliver tailored customer experiences.&lt;/p&gt;
&lt;h3&gt;6. Personalization and future-proofing&lt;/h3&gt;
&lt;p&gt;Optimizely&amp;rsquo;s personalization and experimentation capabilities extend naturally in a hybrid model. Using the &lt;strong&gt;Optimizely Personalization Platform&lt;/strong&gt; and &lt;strong&gt;Web Experimentation Platform&lt;/strong&gt;, marketing teams can deliver tailored content and run experiments across websites, apps, and other channels.&lt;/p&gt;
&lt;p&gt;Because the CMS provides structured content and GraphQL enables efficient delivery, these platforms can target the right segments without requiring heavy developer involvement. As new channels like AR or VR, wearables, or AI-driven assistants emerge, the same approach applies: structured content from the CMS, flexible delivery via GraphQL, and personalization layered on top through Optimizely&amp;rsquo;s dedicated platforms.&lt;/p&gt;
&lt;h2&gt;Where Opal and the JS SDK Fit In&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Opal&lt;/strong&gt;: Generative AI capabilities embedded in the CMS help editors query content, generate ideas, and streamline content-to-experience workflows.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Content JS SDK&lt;/strong&gt;: The &lt;a href=&quot;https://github.com/episerver/content-js-sdk&quot;&gt;Content JS SDK&lt;/a&gt; abstracts GraphQL wiring, giving front-end developers a clean, easy-to-use interface for consuming CMS content. This cuts boilerplate and speeds up delivery.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Conclusion: The Sweet Spot&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Editors stay empowered&lt;/li&gt;
&lt;li&gt;Developers get modern flexibility&lt;/li&gt;
&lt;li&gt;Optimizely manages infrastructure, including the option to host your decoupled front-end alongside your CMS&lt;/li&gt;
&lt;li&gt;Future personalization and multi-channel strategies remain fully open&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It is not about swinging to one extreme. It is about choosing an architecture that enables speed now and adaptability tomorrow. With GraphQL, Opal, the Content JS SDK, and the ability to host both CMS and presentation layer on Optimizely, hybrid headless is the architecture that delivers the sweet spot.&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/Minesh-Shah/Dates/2025/9/the-sweet-spot-hybrid-headless-architecture/</guid>            <pubDate>Thu, 04 Sep 2025 10:54:45 GMT</pubDate>           <category>Blog post</category></item><item> <title>Optimizely Frontend Hosting Beta – Early Thoughts and Key Questions</title>            <link>https://world.optimizely.com/blogs/Minesh-Shah/Dates/2025/4/optimizely-frontend-hosting-beta--early-thoughts-and-key-questions/</link>            <description>&lt;p&gt;Optimizely has opened the waitlist for its new Frontend Hosting capability. I&amp;rsquo;m part of the beta programme, but my invite isn&amp;rsquo;t due until May, while I haven&amp;rsquo;t had hands-on access yet, I&amp;rsquo;ve already been digging into what I&amp;rsquo;ll be looking for once it lands.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.optimizely.com/beta/&quot;&gt;Sign up for the beta&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;What is Frontend Hosting?&lt;/h3&gt;
&lt;p&gt;This new offering is designed to simplify how frontend apps are deployed and managed alongside Optimizely CMS. It&amp;rsquo;s available as an add-on to both CMS SaaS and PaaS, and brings together:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Hosting for headless frontend applications&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;A built-in delivery network&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Web Application Firewall (WAF)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Managed services for build, deploy and performance&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This could offer a tighter, more integrated experience for those already using a headless CMS setup.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re after background on other frontend hosting options, I covered related topics in these earlier blog posts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;/link/6baf1a675602481d812efea991190290.aspx&quot;&gt;Frontend Hosting for SaaS CMS&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;/link/741c17e1162c4428a15b99a7e061b647.aspx&quot;&gt;Self-hosting the Presentation Layer with Coolify&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;What I&amp;rsquo;ll Be Exploring in the Beta&lt;/h3&gt;
&lt;h4&gt;Environment &amp;amp; Compatibility&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Will it work cleanly across CMS SaaS and CMS PaaS?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;What frameworks will be supported out of the box (e.g. Next.js, Astro)?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Will it handle different build strategies?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Static&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Dynamic&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Incremental builds&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Are serverless functions on the roadmap (Lambda, Edge functions)?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Is there a CDN and WAF already in place?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Are preview environments supported (e.g. URLs for pull requests)?&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Developer Experience&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Any limits around build minutes, concurrent builds?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Built-in CI/CD or integrations with GitHub/GitLab?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;How is secret management handled? (Environment Variables)&amp;nbsp;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;What kind of logging and observability is available?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Will it help surface key metrics (e.g. Core Web Vitals, SEO signals)?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Can I get alerts if a deploy fails or performance drops?&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Access &amp;amp; Security&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Can access be restricted (IP allow lists, password protection)?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Is traffic control through WAF configurable?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Can we apply rate limits?&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Commercial Considerations&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;How will pricing work?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Bandwidth&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Seats&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Execution time for serverless&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Multi-project or multi-site support &amp;mdash; all under one roof?&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;Why It&amp;rsquo;s Interesting&lt;/h3&gt;
&lt;p&gt;For teams running hybrid headless setups, this could remove a fair bit of friction. No need to spin up hosting separately or stitch things together manually, this could mean fewer moving parts, more predictable deployment workflows, and a clearer support model.&lt;/p&gt;
&lt;p&gt;And being Optimizely-native, it should also mean closer alignment with the rest of the platform ecosystem.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Discovery &amp;amp; Early Access&lt;/h3&gt;
&lt;p&gt;I was part of an early discovery interview with the team last year, which gave me a first look at the thinking behind this product. Here&amp;rsquo;s part of the follow-up I received:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;As you may recall, you participated in a discovery interview with us about CMS frontend hosting last year. Your feedback back then was extremely helpful! I wanted to let you know that we are now in a beta testing phase of frontend hosting!&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I&amp;rsquo;ve also some early preview screenshots from the discovery. I&amp;rsquo;ll include a few of those in this post to give a sense of what might be coming.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note: The screenshots shown here are from early design mockups shared during the initial discovery phase. The UI and features in the current beta release may differ significantly as the product has continued to evolve.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/bd2927fc5a3349768416e86356bbdf68.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/e635a7e7d9b1472ba76607daac685f46.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/e6925b1bb29643fa902317d7edbeda18.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/5d9d7602a21945bdba63d4c3f24671bc.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Get Involved&lt;/h3&gt;
&lt;p&gt;Optimizely is actively inviting feedback to help shape this feature before full release. If this looks like something you&amp;rsquo;d want to test, you can still register via the waitlist.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.optimizely.com/beta/&quot;&gt;Register for the beta&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ll post more as soon as I&amp;rsquo;m hands-on in May &amp;mdash; including walkthroughs, findings, and observations from real use cases.&lt;/p&gt;
&lt;p&gt;Stay tuned.&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/Minesh-Shah/Dates/2025/4/optimizely-frontend-hosting-beta--early-thoughts-and-key-questions/</guid>            <pubDate>Wed, 23 Apr 2025 13:43:24 GMT</pubDate>           <category>Blog post</category></item><item> <title>Experimentation strategies with Optimizely: Web, Edge, and Feature Experimentation</title>            <link>https://world.optimizely.com/blogs/Minesh-Shah/Dates/2025/4/experimentation-strategies-with-optimizely-web-edge-and-feature-experimentation/</link>            <description>&lt;p class=&quot;MsoNormal&quot;&gt;Experimentation is a cornerstone of data-driven decision-making, and Optimizely offers multiple ways to run experiments across your digital experiences. Whether you are optimising a website, personalising a user journey, or testing new features, choosing the right experimentation strategy can have a significant impact on both user experience and performance.&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;In this post, we&amp;rsquo;ll explore three key experimentation approaches in Optimizely: &lt;strong&gt;Web Experimentation, Edge Experimentation, and Feature Experimentation&lt;/strong&gt;&amp;mdash;highlighting their strengths, trade-offs, and performance considerations.&lt;/p&gt;
&lt;h2 class=&quot;MsoNormal&quot;&gt;&lt;strong&gt;Web Experimentation: Client-Side Testing&lt;/strong&gt;&lt;/h2&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;strong&gt;&lt;img src=&quot;/link/8db1849d0e3743c688ce31b6485ee81c.aspx&quot; /&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;Optimizely Web Experimentation is the most commonly known form of A/B testing, where experiments are executed client-side via JavaScript. This means that changes are applied dynamically in the browser after the page has loaded.&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;strong&gt;Benefits of Web Experimentation&lt;/strong&gt;&lt;/p&gt;
&lt;ul style=&quot;margin-top: 0cm;&quot;&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l0 level1 lfo1; tab-stops: list 36.0pt;&quot;&gt;&lt;strong&gt;Fast iteration:&lt;/strong&gt; Non-technical teams (e.g. marketers, designers) can quickly create and deploy experiments without engineering support.&lt;/li&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l0 level1 lfo1; tab-stops: list 36.0pt;&quot;&gt;&lt;strong&gt;Rich visual editing:&lt;/strong&gt; The WYSIWYG editor enables easy modification of elements without touching code.&lt;/li&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l0 level1 lfo1; tab-stops: list 36.0pt;&quot;&gt;&lt;strong&gt;Wide experimentation scope:&lt;/strong&gt; Ideal for A/B testing landing pages, copy variations, UI elements, and other front-end components.&lt;/li&gt;
&lt;/ul&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;For more information, refer to the official Optimizely Web Experimentation documentation: &lt;a href=&quot;https://docs.developers.optimizely.com/web-experimentation/docs/introduction&quot;&gt;Optimizely Web Experimentation Introduction&lt;/a&gt;&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;strong&gt;Performance considerations&lt;/strong&gt;&lt;/p&gt;
&lt;ul style=&quot;margin-top: 0cm;&quot;&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l1 level1 lfo2; tab-stops: list 36.0pt;&quot;&gt;&lt;strong&gt;Flickering effect:&lt;/strong&gt; Since changes are applied after the page loads, some users may briefly see the original content before the variation is applied.&lt;/li&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l1 level1 lfo2; tab-stops: list 36.0pt;&quot;&gt;&lt;strong&gt;JavaScript overhead:&lt;/strong&gt; As the number of experiments grow, the required JavaScript also increases, which may affect page load times.&lt;/li&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l1 level1 lfo2; tab-stops: list 36.0pt;&quot;&gt;&lt;strong&gt;Execution time:&lt;/strong&gt; Performance can vary depending on the complexity of the variation and network conditions.&lt;/li&gt;
&lt;/ul&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;Despite these considerations, Web Experimentation remains a powerful tool for non-technical teams looking to optimise experiences without needing backend development effort.&lt;/p&gt;
&lt;h2 class=&quot;MsoNormal&quot;&gt;&lt;strong&gt;Edge Experimentation: Server-side, high-performance testing&lt;/strong&gt;&lt;/h2&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;strong&gt;&lt;img src=&quot;/link/3796bfc2b06c46b09517f646d089db25.aspx&quot; /&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;Edge Experimentation moves the execution of experiments from the browser to the edge layer, where content delivery happens closer to the user (e.g. via a CDN or middleware). This approach ensures experiments are applied before the page reaches the end user.&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;strong&gt;Benefits of Edge Experimentation&lt;/strong&gt;&lt;/p&gt;
&lt;ul style=&quot;margin-top: 0cm;&quot;&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l2 level1 lfo3; tab-stops: list 36.0pt;&quot;&gt;&lt;strong&gt;Near-zero performance impact:&lt;/strong&gt; Since variations are applied before the response reaches the client, there&amp;rsquo;s no flickering or delay.&lt;/li&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l2 level1 lfo3; tab-stops: list 36.0pt;&quot;&gt;&lt;strong&gt;More reliable data:&lt;/strong&gt; Eliminates client-side inconsistencies, ensuring accurate measurement of experiment results.&lt;/li&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l2 level1 lfo3; tab-stops: list 36.0pt;&quot;&gt;&lt;strong&gt;Faster experimentation for global users:&lt;/strong&gt; Leveraging CDNs and edge computing reduces latency, making experiences smoother for users worldwide.&lt;/li&gt;
&lt;/ul&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;For more information, refer to the official Optimizely Performance Edge documentation: &lt;a href=&quot;https://docs.developers.optimizely.com/performance-edge/docs/overview&quot;&gt;Optimizely Performance Edge Overview&lt;/a&gt;&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;strong&gt;Use cases&lt;/strong&gt;&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;Edge Experimentation is ideal for personalisation at scale, content experiments, and changes that impact performance-sensitive areas such as eCommerce checkout flows and high-traffic landing pages.&lt;/p&gt;
&lt;h2 class=&quot;MsoNormal&quot;&gt;&lt;strong&gt;Feature Experimentation: Server-side testing for feature rollouts&lt;/strong&gt;&lt;/h2&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;strong&gt;&lt;img src=&quot;/link/aec3a57f068b42c8bb142ac43f56ce00.aspx&quot; /&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;Feature Experimentation allows teams to test and control feature rollouts via &lt;strong&gt;feature flags&lt;/strong&gt; at the server or middleware level. Unlike Web Experimentation, which focuses on front-end changes, this method is geared towards testing new functionalities before full deployment.&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;strong&gt;Benefits of Feature Experimentation&lt;/strong&gt;&lt;/p&gt;
&lt;ul style=&quot;margin-top: 0cm;&quot;&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l3 level1 lfo4; tab-stops: list 36.0pt;&quot;&gt;&lt;strong&gt;No performance trade-offs:&lt;/strong&gt; Since variations are applied server-side, users receive the final content without delay.&lt;/li&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l3 level1 lfo4; tab-stops: list 36.0pt;&quot;&gt;&lt;strong&gt;Safe feature rollouts:&lt;/strong&gt; Teams can progressively roll out features to a subset of users before full release.&lt;/li&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l3 level1 lfo4; tab-stops: list 36.0pt;&quot;&gt;&lt;strong&gt;Deeper experimentation possibilities:&lt;/strong&gt; Beyond UI tests, Feature Experimentation enables testing changes in logic, APIs, and infrastructure.&lt;/li&gt;
&lt;/ul&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;For more information, refer to the official Optimizely Feature Experimentation documentation: &lt;a href=&quot;https://docs.developers.optimizely.com/feature-experimentation/docs/introduction&quot;&gt;Optimizely Feature Experimentation Introduction&lt;/a&gt;&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;strong&gt;Use cases&lt;/strong&gt;&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;This approach is perfect for testing new functionalities, backend optimizations, and product feature toggles while minimising risks.&lt;/p&gt;
&lt;h2 class=&quot;MsoNormal&quot;&gt;&lt;strong&gt;Choosing the right experimentation strategy&lt;/strong&gt;&lt;/h2&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;When deciding between Web, Edge, and Feature Experimentation, consider the following:&lt;/p&gt;
&lt;table class=&quot;MsoNormalTable&quot; style=&quot;mso-cellspacing: 1.5pt; border: solid windowtext 1.0pt; mso-border-alt: solid windowtext .5pt; mso-yfti-tbllook: 1184; mso-border-insideh: .5pt solid windowtext; mso-border-insidev: .5pt solid windowtext;&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;mso-yfti-irow: 0; mso-yfti-firstrow: yes;&quot;&gt;
&lt;td style=&quot;border: solid windowtext 1.0pt; mso-border-alt: solid windowtext .5pt; padding: .75pt .75pt .75pt .75pt;&quot;&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;strong&gt;Factor&lt;/strong&gt;&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;border: solid windowtext 1.0pt; mso-border-alt: solid windowtext .5pt; padding: .75pt .75pt .75pt .75pt;&quot;&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;strong&gt;Web Experimentation&lt;/strong&gt;&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;border: solid windowtext 1.0pt; mso-border-alt: solid windowtext .5pt; padding: .75pt .75pt .75pt .75pt;&quot;&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;strong&gt;Edge Experimentation&lt;/strong&gt;&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;border: solid windowtext 1.0pt; mso-border-alt: solid windowtext .5pt; padding: .75pt .75pt .75pt .75pt;&quot;&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;strong&gt;Feature Experimentation&lt;/strong&gt;&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;mso-yfti-irow: 1;&quot;&gt;
&lt;td style=&quot;border: solid windowtext 1.0pt; mso-border-alt: solid windowtext .5pt; padding: .75pt .75pt .75pt .75pt;&quot;&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;strong&gt;Performance&lt;/strong&gt;&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;border: solid windowtext 1.0pt; mso-border-alt: solid windowtext .5pt; padding: .75pt .75pt .75pt .75pt;&quot;&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;Can introduce flickering and JavaScript overhead&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;border: solid windowtext 1.0pt; mso-border-alt: solid windowtext .5pt; padding: .75pt .75pt .75pt .75pt;&quot;&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;Near-instant, no flickering&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;border: solid windowtext 1.0pt; mso-border-alt: solid windowtext .5pt; padding: .75pt .75pt .75pt .75pt;&quot;&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;No client-side impact&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;mso-yfti-irow: 2;&quot;&gt;
&lt;td style=&quot;border: solid windowtext 1.0pt; mso-border-alt: solid windowtext .5pt; padding: .75pt .75pt .75pt .75pt;&quot;&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;strong&gt;Experiment Scope&lt;/strong&gt;&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;border: solid windowtext 1.0pt; mso-border-alt: solid windowtext .5pt; padding: .75pt .75pt .75pt .75pt;&quot;&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;Front-end UI elements&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;border: solid windowtext 1.0pt; mso-border-alt: solid windowtext .5pt; padding: .75pt .75pt .75pt .75pt;&quot;&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;Content and UI changes at the edge layer&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;border: solid windowtext 1.0pt; mso-border-alt: solid windowtext .5pt; padding: .75pt .75pt .75pt .75pt;&quot;&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;Feature toggles, backend logic&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;mso-yfti-irow: 3;&quot;&gt;
&lt;td style=&quot;border: solid windowtext 1.0pt; mso-border-alt: solid windowtext .5pt; padding: .75pt .75pt .75pt .75pt;&quot;&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;strong&gt;Execution Location&lt;/strong&gt;&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;border: solid windowtext 1.0pt; mso-border-alt: solid windowtext .5pt; padding: .75pt .75pt .75pt .75pt;&quot;&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;Client-side (browser)&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;border: solid windowtext 1.0pt; mso-border-alt: solid windowtext .5pt; padding: .75pt .75pt .75pt .75pt;&quot;&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;CDN/Edge layer&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;border: solid windowtext 1.0pt; mso-border-alt: solid windowtext .5pt; padding: .75pt .75pt .75pt .75pt;&quot;&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;Server-side or middleware&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;mso-yfti-irow: 4;&quot;&gt;
&lt;td style=&quot;border: solid windowtext 1.0pt; mso-border-alt: solid windowtext .5pt; padding: .75pt .75pt .75pt .75pt;&quot;&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;strong&gt;Implementation Effort&lt;/strong&gt;&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;border: solid windowtext 1.0pt; mso-border-alt: solid windowtext .5pt; padding: .75pt .75pt .75pt .75pt;&quot;&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;Low (no engineering needed)&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;border: solid windowtext 1.0pt; mso-border-alt: solid windowtext .5pt; padding: .75pt .75pt .75pt .75pt;&quot;&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;Medium (requires setup)&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;border: solid windowtext 1.0pt; mso-border-alt: solid windowtext .5pt; padding: .75pt .75pt .75pt .75pt;&quot;&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;High (involves development)&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;mso-yfti-irow: 5; mso-yfti-lastrow: yes;&quot;&gt;
&lt;td style=&quot;border: solid windowtext 1.0pt; mso-border-alt: solid windowtext .5pt; padding: .75pt .75pt .75pt .75pt;&quot;&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;strong&gt;Best For&lt;/strong&gt;&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;border: solid windowtext 1.0pt; mso-border-alt: solid windowtext .5pt; padding: .75pt .75pt .75pt .75pt;&quot;&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;Marketing, UI tweaks, messaging tests&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;border: solid windowtext 1.0pt; mso-border-alt: solid windowtext .5pt; padding: .75pt .75pt .75pt .75pt;&quot;&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;Performance-critical content changes&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;border: solid windowtext 1.0pt; mso-border-alt: solid windowtext .5pt; padding: .75pt .75pt .75pt .75pt;&quot;&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;Feature rollouts, backend logic experiments&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;strong&gt;&amp;nbsp;&lt;/strong&gt;&lt;/p&gt;
&lt;h2 class=&quot;MsoNormal&quot;&gt;&lt;strong&gt;Final thoughts&lt;/strong&gt;&lt;/h2&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;Each experimentation approach in Optimizely has its place. &lt;strong&gt;Web Experimentation&lt;/strong&gt; remains a powerful tool for marketers and teams that need agility, despite its minor performance trade-offs. &lt;strong&gt;Edge and Feature Experimentation&lt;/strong&gt;, on the other hand, offer superior performance and scalability by shifting execution away from the client.&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;Ultimately, leveraging a combination of these approaches based on your goals, performance requirements, and team capabilities will lead to the most effective experimentation strategy.&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/Minesh-Shah/Dates/2025/4/experimentation-strategies-with-optimizely-web-edge-and-feature-experimentation/</guid>            <pubDate>Sat, 05 Apr 2025 12:24:37 GMT</pubDate>           <category>Blog post</category></item><item> <title>Exploring Optimizely SaaS CMS – What’s New &amp; How to Accelerate your Build</title>            <link>https://world.optimizely.com/blogs/Minesh-Shah/Dates/2025/2/exploring-optimizely-saas-cms--whats-new--how-to-accelerate-your-build/</link>            <description>&lt;p&gt;In my latest video, I take a fresh look at &lt;strong&gt;Optimizely SaaS CMS&lt;/strong&gt;, covering some of the recent improvements aimed at enhancing the editor experience. Plus, I dive into the fantastic work &lt;strong&gt;Remko&lt;/strong&gt; has done with his &lt;strong&gt;helper packages&lt;/strong&gt; and &lt;strong&gt;starter solutions&lt;/strong&gt;, making development even more seamless.&lt;/p&gt;
&lt;p&gt;&#128313; Learn how to effortlessly create an &lt;strong&gt;Element&lt;/strong&gt; in the UI and scaffold it in the frontend.&lt;br /&gt;&#128313; See how you can generate a &lt;strong&gt;new element&lt;/strong&gt; directly in the frontend and push it to the CMS.&lt;/p&gt;
&lt;p&gt;If you&#39;re working with Optimizely or just curious about headless CMS solutions, this one&#39;s for you!&lt;/p&gt;
&lt;p&gt;&#128250; Watch here: &lt;a href=&quot;https://youtu.be/m8zvDgdYmpk&quot;&gt;https://youtu.be/m8zvDgdYmpk&lt;/a&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Links I used in the video:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Remkos Git Repository: &lt;a href=&quot;https://github.com/remkoj&quot;&gt;https://github.com/remkoj&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Optimizely Mosey Demo: &lt;a href=&quot;https://github.com/episerver/cms-saas-vercel-demo&quot;&gt;https://github.com/episerver/cms-saas-vercel-demo&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Let me know your thoughts in the comments!&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/Minesh-Shah/Dates/2025/2/exploring-optimizely-saas-cms--whats-new--how-to-accelerate-your-build/</guid>            <pubDate>Thu, 27 Feb 2025 11:00:25 GMT</pubDate>           <category>Blog post</category></item><item> <title>SaaS CMS - Pages and Blocks get the Visual Builder Treatment</title>            <link>https://world.optimizely.com/blogs/Minesh-Shah/Dates/2024/12/saas-cms---pages-and-blocks-get-the-visual-builder-treatment/</link>            <description>&lt;p&gt;I&amp;rsquo;m thrilled to see that Optimizely has now enabled Visual Builder for OG Pages and Blocks within SaaS CMS, and I&amp;rsquo;m guessing this will become standard for CMS13. Previously, Visual Builder was only available for Experiences, forcing us to revert to traditional on-page editing which was somewhat clunky or use the &quot;All Properties&quot; view, which didn&amp;rsquo;t provide a great preview experience.&lt;/p&gt;
&lt;p&gt;With Visual Builder, we now get the best of both worlds: an intuitive editor experience combined with the ability to preview our changes in real time. I haven&amp;rsquo;t taken this for a proper spin yet, but the early signs are very promising.&lt;/p&gt;
&lt;p&gt;When editing structured pages, the new interface looks like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/b50c210556874c05bf8a49145b1989f4.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;As shown above, updates can be made in the left panel, and they are instantly reflected in the main preview panel.&lt;/p&gt;
&lt;p&gt;In contrast, the old on-page editing view looked like this and was never as responsive:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/b3e241059b15460bad5da6e83d9f4713.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Additionally, Visual Builder is now available for Blocks as well. The interface for Blocks is similar to Pages, and it looks like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/4bb19a0466b54f7e8eaae66e1b226cff.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Although my templates aren&amp;rsquo;t perfect, this is entirely within our control via the frontend presentation layer (Next.js).&lt;/p&gt;
&lt;p&gt;This is just a quick post to share my initial impressions, but I&amp;rsquo;ll provide more updates as I integrate this into my own templates.&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/Minesh-Shah/Dates/2024/12/saas-cms---pages-and-blocks-get-the-visual-builder-treatment/</guid>            <pubDate>Tue, 17 Dec 2024 16:27:43 GMT</pubDate>           <category>Blog post</category></item><item> <title>SaaS CMS - Self-hosting Presentation Layer with Coolify</title>            <link>https://world.optimizely.com/blogs/Minesh-Shah/Dates/2024/9/saas-cms---self-hosting-presentation-layer-with-coolify/</link>            <description>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;In my previous post, I explored the hosting options for the Hybrid Headless/Fully Headless presentation layer (frontend) using Netlify and Vercel. These platforms are my top recommendations due to their ease of use and feature-rich offerings. However, there are situations where they may not entirely meet your needs, or their pricing models might pose challenges.&lt;/p&gt;
&lt;p&gt;If you&#39;re looking for more control over your hosting costs, self-hosting is a viable third option, whether on physical servers or virtual private servers (VPS). To make the self-hosting process more manageable, I&amp;rsquo;ve created a proof of concept using Coolify. Coolify is an open-source project that simplifies server configuration for hosting your frontends with minimal effort. It offers many of the same features as Vercel and Netlify, but without the associated costs.&lt;/p&gt;
&lt;p&gt;Here are some of Coolify&#39;s standout features:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Language Agnostic&lt;/strong&gt;: Supports a wide range of programming languages and frameworks, including Next.js.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flexible Deployment&lt;/strong&gt;: Can be deployed on any resource, including your own servers, VPS, EC2, and DigitalOcean&amp;mdash;all you need is an SSH connection.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Push to Deploy&lt;/strong&gt;: Easily deploy by pointing to any Git repository.&lt;/li&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;PR/Commit Deployments&lt;/strong&gt;: Automatically deploy new commits and pull requests.&lt;/li&gt;
&lt;/ul&gt;
&lt;li&gt;&lt;strong&gt;Additional Features&lt;/strong&gt;:&lt;/li&gt;
&lt;ul&gt;
&lt;li&gt;SSL certificates&lt;/li&gt;
&lt;li&gt;Automatic backups&lt;/li&gt;
&lt;li&gt;Webhooks&lt;/li&gt;
&lt;li&gt;API-first approach&lt;/li&gt;
&lt;li&gt;Collaboration tools&lt;/li&gt;
&lt;li&gt;Monitoring and notifications&lt;/li&gt;
&lt;/ul&gt;
&lt;/ul&gt;
&lt;h2&gt;Azure Example&lt;/h2&gt;
&lt;p&gt;Coolify offers flexible installation and configuration options, allowing deployment on virtually any environment. For full documentation, you can refer to &lt;a href=&quot;https://coolify.io/docs/installation&quot;&gt;&lt;span&gt;Coolify&#39;s&lt;/span&gt;&lt;span&gt; official&lt;/span&gt;&lt;span&gt; guide&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;For this example, I chose to test Coolify on a &lt;strong&gt;Linux (Ubuntu 24.04) Virtual Machine&lt;/strong&gt;. The setup process in Azure is straightforward, so I won&#39;t dive into each setup step, but here&amp;rsquo;s a quick overview of my configuration below:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/6657cca0cdc74998a64bf70e61f59f6a.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;I also had to open a few ports to allow for communication to and from the virtual machine. This was for both setup purposes and public site access (I was adding these as and when required).&amp;nbsp;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Port 22 for SSH Access&amp;nbsp;&lt;/li&gt;
&lt;li&gt;Port 8000 for initial Coolify server Access&amp;nbsp;&lt;/li&gt;
&lt;li&gt;Port 80 for Public HTTP Access&amp;nbsp;&lt;/li&gt;
&lt;li&gt;Port 443 for Public HTTPS Access&amp;nbsp;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;/link/4c76347d3fa94dceb3535c355d3beae2.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Once my virtual machine was configured, I made an SSH connection. You can connect either via the Azure Portal or using your favorite terminal.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/ca74d51676c24f26892de7343ea12b4c.aspx&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Install and Configure Coolify&lt;/h2&gt;
&lt;p&gt;Once connected via SSH, the installation of Coolify was simple. Running the following command started the process:&lt;/p&gt;
&lt;pre class=&quot;language-c&quot;&gt;&lt;code&gt;curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This installs &lt;strong&gt;Docker&lt;/strong&gt; and runs the Coolify image. The entire process took about 5 minutes. After installation, Coolify provides a URL to complete the configuration. Once I registered, I was taken to the Coolify dashboard, ready to create a new project.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/be347e3621204be9b3337d6f86fcfb03.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/f29bf0f226134be28a8bcba42ac77594.aspx&quot; /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;Domain Configuration&lt;/h2&gt;
&lt;p&gt;To make access easier, I connected a domain to my server. I used an existing domain from &lt;strong&gt;Fasthosts&lt;/strong&gt; and made simple DNS changes by adding two &lt;strong&gt;A Records&lt;/strong&gt; (&lt;code&gt;@&lt;/code&gt; and &lt;code&gt;*&lt;/code&gt;), pointing them to my server&amp;rsquo;s public IP address.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/81133d5676a84627acbdb4da055e9a59.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;In Coolify&amp;rsquo;s settings, I configured the domain name for the control panel at &lt;code&gt;http://coolify.chillicrunch.co.uk&lt;/code&gt;. I also set up &lt;strong&gt;Wildcard Subdomains&lt;/strong&gt; to allow deployments to URLs like &lt;code&gt;https://&amp;lt;my-project-name&amp;gt;.chilicrunch.co.uk/&lt;/code&gt;, similar to how Netlify and Vercel manage deployments.&lt;/p&gt;
&lt;p&gt;This was done by navigating to &lt;strong&gt;Servers&lt;/strong&gt; &amp;gt; &lt;strong&gt;General&lt;/strong&gt; and adjusting the settings.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/b9c4f8455e6f4863b74ce5b51b8acf18.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/b2a7a85fa1904c07b76f7b25f47d44c7.aspx&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Connecting to Git Hub Repo&lt;/h2&gt;
&lt;p&gt;To enable continuous deployment, I connected Coolify to my private &lt;strong&gt;GitHub&lt;/strong&gt; repository. Coolify supports several Git sources, but for this example, I used GitHub. You can add your GitHub repo by navigating to &lt;strong&gt;Sources&lt;/strong&gt; and clicking &lt;strong&gt;Add&lt;/strong&gt;. The process requires setting up &lt;strong&gt;Client ID&lt;/strong&gt;, &lt;strong&gt;Secret&lt;/strong&gt;, and &lt;strong&gt;Webhook Secret&lt;/strong&gt;, which are generated during the setup.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/64546679fd3d480d8a4df8ed48f4b86b.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/f032f9a23bf24aa2a95e5902d55aeb42.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/21741afe51de4b409df632e67f1a81a7.aspx&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Adding a new Project&lt;/h2&gt;
&lt;p&gt;With the Git connection established, I was able to add my first project. In the &lt;strong&gt;Projects&lt;/strong&gt; section, I clicked &lt;strong&gt;New&lt;/strong&gt;, provided a name and description, then selected the Git repo I wanted to deploy.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/eb33c9666590413f8ec044fc22187186.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The deployment process took about 3 minutes. Thanks to the wildcard subdomains setup, the subdomain was automatically configured. I also added the necessary environment variables for my project. You can find more details on this process in &lt;a href=&quot;https://youtu.be/-cFsPUdIVFY?si=FYycbX5v9fny77Xw&quot;&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt; video&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/0017a532a5c54c83a5c61366a1cdada8.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/deb4fb8b440b423a8757cb61faef13c5.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;With everything in place, my website was live: &lt;a href=&quot;https://saas-cms-visual-builder.chilicrunch.co.uk/&quot;&gt;https://saas-cms-visual-builder.chilicrunch.co.uk/&lt;/a&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/8f9595a956d74d2a83a917c904e51a22.aspx&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Setting up Coolify on an Azure Virtual Machine was smooth and straightforward. As you can see, it&#39;s easy to get Coolify up and running on your private servers with minimal effort, making it a cost-effective alternative to traditional hosting platforms like Vercel and Netlify.&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/Minesh-Shah/Dates/2024/9/saas-cms---self-hosting-presentation-layer-with-coolify/</guid>            <pubDate>Mon, 23 Sep 2024 19:00:11 GMT</pubDate>           <category>Blog post</category></item><item> <title>Frontend Hosting for SaaS CMS Solutions</title>            <link>https://world.optimizely.com/blogs/Minesh-Shah/Dates/2024/7/frontend-hosting-for-saas-cms-solutions/</link>            <description>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Now that CMS SaaS Core has gone into general availability, it is a good time to start discussing where to host the head. SaaS Core is natively a Headless CMS, and regardless of whether you plan to use it in a &quot;Pure Headless&quot; or &quot;Hybrid Headless&quot; manner, the presentation layer (Head/Frontend) needs to be hosted somewhere.&lt;/p&gt;
&lt;h2&gt;&lt;/h2&gt;
&lt;h2&gt;Cloud Native Providers: Vercel and Netlify&lt;/h2&gt;
&lt;p&gt;Optimizely has partnered with Vercel and Netlify, both of which offer fantastic services for hosting the frontend of your application. Both platforms support a variety of frontend frameworks and static site generators, making them versatile choices for a project&#39;s hosting needs.&lt;/p&gt;
&lt;p&gt;They provide a seamless experience for developers with continuous deployments, user-friendly CLI tools, unlimited deployment environments, and highly efficient build processes. E&lt;span&gt;very time a pull request is raised, or a commit has been made to a branch, both services automatically build a preview with a unique URL. Like a staging environment for every PR or branch, previews are perfect for testing and collaboration.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;To ensure the frontend is fast and scalable, both platforms offer a CDN to serve requests and support edge or serverless functions to execute any business logic. Serverless functions &lt;span&gt;are a way to deploy server-side code as API endpoints. These will spin up automatically when triggered by an event, handle and process server ran code, and then spin down until the next event. Edge Functions are similar to serverless functions that are generally more efficient and faster than traditional Serverless compute, since they operate within a much leaner runtime. Deployed globally by default, Edge Functions run in the region closest to the request for the lowest latency possible.&lt;/span&gt;&lt;/p&gt;
&lt;h2&gt;&lt;img src=&quot;/link/b00fa58ff48545d8bddda2bd7d94b121.aspx&quot; width=&quot;800&quot; height=&quot;237&quot; /&gt;&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/link/10c259fec61342d289c65ee0e069e0b8.aspx&quot; width=&quot;800&quot; height=&quot;460&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;&lt;/h2&gt;
&lt;h2&gt;&lt;span&gt;Commercial&lt;/span&gt; Models&lt;/h2&gt;
&lt;p&gt;Vercel and Netlify are designed to simplify the deployment and management of your website&amp;rsquo;s frontend, ensuring reliability, speed, and ease of use. Their commercial models are quite similar; both offer customizable enterprise pricing based on the specific needs of the organization. Pricing typically includes multiple developer seats, high bandwidth limits with additional costs for overage, and extensive usage of serverless and edge functions with charges for higher usage. Both platforms provide enterprise-grade support, including dedicated account management and service level agreements (SLAs) to ensure high availability and priority issue resolution.&lt;/p&gt;
&lt;h2&gt;Demonstration&lt;/h2&gt;
&lt;p&gt;Here is a quick video demonstrating the deployment of the Mosey Template to Vercel: &lt;a href=&quot;https://youtu.be/-cFsPUdIVFY?si=ZDOQwslyZIXAr7kH&quot;&gt;Watch the video&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;A similar process was replicated on Netlify with very few changes to the frontend code or configuration.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;By leveraging the robust hosting capabilities of Vercel and Netlify, you can ensure that the frontend of your application is performant, scalable, and easy to manage. Whether you choose Vercel or Netlify, both platforms offer the tools and support needed to seamlessly deploy and maintain your website.&lt;/p&gt;
&lt;h2&gt;But Wait&amp;nbsp;&lt;/h2&gt;
&lt;p&gt;...&amp;nbsp;Oh and for a bit of fun I also looked at self hosting either on prem or on a VPS using Coolify, surprisingly this was also very easy and in less than an hour I had this : &lt;a href=&quot;https://saas-cms-visual-builder.chilicrunch.co.uk/en&quot;&gt;https://saas-cms-visual-builder.chilicrunch.co.uk&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/4c41307ffe2940cf8b573c67f3c8d170.aspx&quot; width=&quot;800&quot; height=&quot;209&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Hopefully have a demo on this early next week :)&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/Minesh-Shah/Dates/2024/7/frontend-hosting-for-saas-cms-solutions/</guid>            <pubDate>Sat, 20 Jul 2024 00:21:31 GMT</pubDate>           <category>Blog post</category></item><item> <title>Exploring SaaS CMS: API Clients and Content Management APIs</title>            <link>https://world.optimizely.com/blogs/Minesh-Shah/Dates/2024/3/exploring-saas-cms-api-clients-and-content-management-apis/</link>            <description>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;In continuation of my &lt;a href=&quot;/link/807ad30b59c140a6baec9c7633a5a256.aspx&quot;&gt;previous post&lt;/a&gt; on leveraging the Content Management API and OpenID Connect Authentication on the PaaS-based Optimizely CMS, I delve into the delivery mechanisms within the SaaS-based CMS Platform. Surprisingly, the majority of functionalities are readily available and seamlessly integrated into the system. In this article, I provide a quick preview of the available features and guide on configuring the exposed API for definition and content management.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; It&#39;s essential to bear in mind that while I explore these features, the SaaS platform is still in its BETA phase, and APIs are currently at version 0.5. Changes might occur as the SaaS CMS transitions into general availability.&lt;/p&gt;
&lt;h2&gt;Configuring the API Client (Equivalent to OpenID Connect Package)&lt;/h2&gt;
&lt;p&gt;The API client functionality comes pre-installed in the SaaS CMS, and the setup is near identical to what we found on the PaaS platform with the Open ID Connect package. The tool can be found within Settings and the Access Rights section as highligted in the image below.&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Once on the API Client interface we can create a Client ID, the Client Secret is automatically generated please store this safely as there is no way of retrieving once you leave the page.&amp;nbsp;The option to &quot;Allow the client to impersonate users&quot; is self-explanatory; enabling this allows the client to function as another user within the CMS.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/2a7d3af9b80749d189b9c3ddc6fa2287.aspx&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Using the credentials&lt;/h3&gt;
&lt;p&gt;I&#39;ll demonstrate how to utilize these credentials using Postman to retrieve a JWT token for subsequent API calls. To obtain a Bearer Auth Token, a GET call needs to be made to the designated URL:&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;https://app-xxxprod.cms.optimizely.com/_cms/v0.5/oauth/token&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span&gt;Sending the required parameters along with the request, including &lt;/span&gt;&lt;code&gt;grant_type&lt;/code&gt;&lt;span&gt;, &lt;/span&gt;&lt;code&gt;client_id&lt;/code&gt;&lt;span&gt;, &lt;/span&gt;&lt;code&gt;client_secret&lt;/code&gt;&lt;span&gt;, and optionally &lt;/span&gt;&lt;code&gt;act_as&lt;/code&gt;&lt;span&gt;, will result in the generation of a token for future requests. Notably, this token expires automatically after 300 seconds.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;Example:&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;img src=&quot;/link/afad4afd59dd4d66b7dc4837d4a088e6.aspx&quot; /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h2&gt;Authorisation to API using the Bearer Token&lt;/h2&gt;
&lt;p&gt;With the Bearer Token generated, subsequent API requests can now be authenticated by passing this token as the &quot;Authorization&quot; Header parameter.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/73a267ca07894182883fda04000e5583.aspx&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Content Definitions API&lt;/h2&gt;
&lt;p&gt;&lt;span&gt;Now armed with the bearer token, we can interact with the Content and Definitions API. The first API we explore is the Content Definitions API. Detailed API reference can be found &lt;/span&gt;&lt;a href=&quot;https://docs.developers.optimizely.com/content-management-system/v1.0.0-SaaS-Core/reference/contenttypes_list&quot;&gt;here&lt;/a&gt;&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;h3&gt;Get Content Types&lt;/h3&gt;
&lt;p&gt;&lt;span&gt;A simple GET request to the following URL provides a list of all content types within the CMS:&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;https://app-xxxprod.cms.optimizely.com/_cms/v0.5/contenttypes&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Example:&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/c9015e217b714b16ba0c466475e46fa2.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;To only get the details of a certain known type we pass in the definition name (key) to the URL:&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;https://app- xxxprod.cms.optimizely.com/_cms/v0.5/contenttypes/articlepage&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Create Content Type&lt;/h3&gt;
&lt;p&gt;To create a content type, a POST request is made to the same URL, passing in the necessary parameters.&lt;/p&gt;
&lt;p&gt;Example:&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/593612ffa8464867aa73b1f11a9f202f.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/1c718e89bd7144c696bb0078b05f4c24.aspx&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Content {Delivery} API&lt;/h2&gt;
&lt;p&gt;The API reference to the Content API can be found here : &lt;a href=&quot;https://docs.developers.optimizely.com/content-management-system/v1.0.0-SaaS-Core/reference/content_create&quot;&gt;Create content (optimizely.com)&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Get Content&lt;/h3&gt;
&lt;p&gt;To retrieve a content item, a GET request is made to the designated URL, using the Guid of the page as the key. The key is in a UUID format so should not include any dashes e.g. 115988243510434482925671c3ee601a&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;https://app- xxxprod.cms.optimizely.com/_cms/v0.5/content/{key}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Example:&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/418614fbd0f245c4b586f1b1ca1ea884.aspx&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;As you can see its very easy to interact with the API&amp;rsquo;s and retrieve the relevant information you may need, as well as programmatically being able to create Content Models and Instances of these models. Its great to see this has all been included from the get go and provides a lot of scope to decide on how we manage the content definition creation process.&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/Minesh-Shah/Dates/2024/3/exploring-saas-cms-api-clients-and-content-management-apis/</guid>            <pubDate>Wed, 06 Mar 2024 20:29:56 GMT</pubDate>           <category>Blog post</category></item><item> <title>A Quick Guide to Using the Content Management API and OpenID Connect Authentication</title>            <link>https://world.optimizely.com/blogs/Minesh-Shah/Dates/2023/11/a-quick-guide-to-using-the-content-management-api-and-openid-connect-authentication/</link>            <description>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Content management in Optimizely CMS becomes more efficient and streamlined with the power of the &lt;strong&gt;Content Management API&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;strong&gt;Optimizely Content Management API&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/strong&gt;adds REST endpoints for basic content management operations such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Create content objects&lt;/li&gt;
&lt;li&gt;Modify existing content objects&lt;/li&gt;
&lt;li&gt;Delete content objects&lt;/li&gt;
&lt;li&gt;Get draft content objects&lt;/li&gt;
&lt;li&gt;Move content objects&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The REST API is useful for pushing external content to Optimizely without having to deploy custom integration code to the Optimizely Content Management System (CMS).&lt;/p&gt;
&lt;p&gt;In this quick guide, I&#39;ll walk through the steps to set up a solution for content creation using the API.&lt;/p&gt;
&lt;h2&gt;Prerequisites&lt;/h2&gt;
&lt;p&gt;Before diving into the process, ensure you have the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;An active Optimizely CMS instance (For Alloy site setup, refer to my guide &lt;a href=&quot;/link/9eaccb8259bf485296a54407dc071768.aspx&quot;&gt;here&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;Familiarity with an API request tool like Postman.&lt;/li&gt;
&lt;li&gt;A good understanding of your CMS&#39;s content structure and types.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Step 1: Install and Configure the Content Management API&lt;/h2&gt;
&lt;p&gt;&lt;span&gt;First, let&#39;s set up the Content Management API. If you&#39;re using Visual Studio, you can install it via the Package Manager or via the CLI using this command:&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;dotnet add package EPiServer.ContentManagementApi&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span&gt;Once installed, modify the Startup Class to include the basic configuration. In the &lt;/span&gt;&lt;code&gt;ConfigureServices&lt;/code&gt;&lt;span&gt; method, add the following line:&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;services.AddContentManagementApi(o =&amp;gt; o.DisableScopeValidation = true);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then, build and run your solution with:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;dotnet run&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Step 2: Test the Endpoint&lt;/h2&gt;
&lt;p&gt;&lt;span&gt;After installing the Content Management API, you can test the endpoint using Postman. Send a simple POST request to &lt;/span&gt;&lt;code&gt;/api/episerver/v3.0/contentmanagement&lt;/code&gt;&lt;span&gt;. As we haven&#39;t added any parameters, expect a 400 Bad Request status code with a specific JSON response.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;{
    &quot;errors&quot;: {
        &quot;filename&quot;: [
            &quot;The filename field is required.&quot;
        ],
        &quot;content-Type&quot;: [
            &quot;The contentType field is required.&quot;
        ]
    },
    &quot;type&quot;: &quot;https://tools.ietf.org/html/rfc7231#section-6.5.1&quot;,
    &quot;title&quot;: &quot;One or more validation errors occurred.&quot;,
    &quot;status&quot;: 400,
    &quot;traceId&quot;: &quot;00-b2f4877cb70b04fc20ea6fc0dfe1d2fe-d37b0cf2e4dc9458-00&quot;,
    &quot;code&quot;: &quot;InvalidModel&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span&gt;This response tells us the endpoint is operational. Alternatively, it would have resulted in a 404 status code. At this stage, we can try adding content to Optimizely CMS via Postman. Use the provided JSON body, but you&#39;ll likely encounter a 401 status due to permissions.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;{
  &quot;name&quot;: &quot;Text Block (Content Management API)&quot;,
  &quot;language&quot;: {
      &quot;name&quot;: &quot;en&quot;
  },
  &quot;contentType&quot;: [
      &quot;Block&quot;,
      &quot;EditorialBlock&quot;
  ],
  &quot;parentLink&quot;: {
      &quot;id&quot;: &quot;33&quot;
  },
  &quot;mainBody&quot;: {
      &quot;value&quot;: &quot;&amp;lt;p&amp;gt;Hello World&amp;lt;/p&amp;gt;&quot;
  },
  &quot;status&quot;: &quot;Published&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Response Example&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;{
    &quot;type&quot;: &quot;https://tools.ietf.org/html/rfc7235#section-3.1&quot;,
    &quot;title&quot;: &quot;Unauthorized&quot;,
    &quot;status&quot;: 401,
    &quot;detail&quot;: &quot;Access was denied to content 33. The required access level was \&quot;Create, Publish\&quot;.&quot;,
    &quot;instance&quot;: &quot;/api/episerver/v3.0/contentmanagement&quot;,
    &quot;traceId&quot;: &quot;00-c91883ef32f39b8dda0571df62c44b7b-d48fe0471f6254da-00&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span&gt;For testing purposes, you can temporarily grant access rights to &quot;Everyone&quot; for creating and publishing pages. After making this change, re-run the request. You should now receive a 201 (Success) status code with the content successfully created in CMS.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;img src=&quot;/link/17191f74030b45d2890b9a429189d49c.aspx&quot; /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;img src=&quot;/link/6bec4a590ea3429eab25c352fb109049.aspx&quot; /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h2&gt;Step 3: API Authentication via OpenID Connect&lt;/h2&gt;
&lt;p&gt;In the real world, giving &quot;Everyone&quot; access to publish content is not acceptable. To ensure authorized individuals/services create and publish content, we need to authenticate Content Management API requests using OpenID Connect and Bearer Tokens (JWT). Here&#39;s how:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Install the OpenIDConnect NuGet package with this command:
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;dotnet add package EPiServer.OpenIDConnect.UI&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;span&gt;Amend the Startup class to configure this package and make Content Management API support it. Update the code as follows:&lt;/span&gt;&lt;/span&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;services.AddContentManagementApi(OpenIDConnectOptionsDefaults.AuthenticationScheme, options =&amp;gt;
{
    options.DisableScopeValidation = true;
});

services.AddOpenIDConnect&amp;lt;ApplicationUser&amp;gt;(
    useDevelopmentCertificate: true,
    signingCertificate: null,
    encryptionCertificate: null,
    createSchema: true
);

services.AddOpenIDConnectUI();
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span&gt;After saving the file, build and run the solution and access Optimizely Admin mode. Look for &quot;OpenID Connect&quot; in Settings, create a new application (set Scopes as &quot;epi_content_management&quot;).&lt;/span&gt;&lt;span&gt;&lt;img src=&quot;/link/4a8cd7fe899141e49f8d4b6e62c806ec.aspx&quot; /&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;Test if it all works by making a POST request in Postman to &lt;code&gt;/api/episerver/connect/token&lt;/code&gt; with the following parameters:&lt;/span&gt;
&lt;ul&gt;
&lt;li&gt;client_id: api-client&lt;/li&gt;
&lt;li&gt;client_secret: SuperSecret&lt;/li&gt;
&lt;li&gt;grant_type: client_credentials&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span&gt;You&#39;ll receive a time-limited access token.&lt;/span&gt;&lt;span&gt;&lt;img src=&quot;/link/f2faa2d4556842c28c6ce3ae081acd34.aspx&quot; /&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;span&gt;Having got this far and sucessfully being able to generate a bearer token, we have to grant our newly created client permissions to create and publish content, we do this by managing access rights in Optimizely. Remove additional rights given to the &quot;Everyone&quot; group and add them to the &lt;code&gt;api-client&lt;/code&gt; with the same rights instead.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;img src=&quot;/link/57750409614146909e5d1a7d5fb85875.aspx&quot; /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;Finally, copy the access token from Postman and make another POST request to &lt;code&gt;/api/episerver/v3.0/contentmanagement&lt;/code&gt;, this time also setting the Authorization Token.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/f5558b9a21b44378aaf97fa3d41bc492.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/26638829b9a54d08b457425e8a539d74.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;That&#39;s it! We&#39;re now securely creating content via the Content Management API using a JWT Bearer Token.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/58c2c1319e4e45d99bfaf0c15bf50040.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;** For additional Security measures you can add some additional CORS policies to your Optimizely Solution so only certain services can call the Content Management API requests or even lock the API down via IP Restrictions.&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;The Content Management API alongside Content Definitions API are quite powerful tools, they are not difficult to setup and have many use cases for example with the Content Management API we can quite easily utilise for migration processes and import a large amount of content into the CMS in bulk all programmatically. With the recent announcement of the SaaS CMS offering the Content Definitions API is quite useful in creating Content Types and something that can quite easily be source controlled, so definitely something to look out for and try-out.&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;References&amp;nbsp;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.developers.optimizely.com/content-management-system/v1.6.0-content-management-api/docs&quot;&gt;Get started with Content Management API (optimizely.com)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.developers.optimizely.com/content-management-system/v1.5.0-content-delivery-api/docs/api-authentication&quot;&gt;API authentication (optimizely.com)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.developers.optimizely.com/content-management-system/v1.6.0-content-management-api/docs/api-fundamentals&quot;&gt;Examples of Content Management API (optimizely.com)&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</description>            <guid>https://world.optimizely.com/blogs/Minesh-Shah/Dates/2023/11/a-quick-guide-to-using-the-content-management-api-and-openid-connect-authentication/</guid>            <pubDate>Wed, 01 Nov 2023 22:44:53 GMT</pubDate>           <category>Blog post</category></item><item> <title>Insights from Setting up and Getting Started with SaaS Core [Beta]</title>            <link>https://world.optimizely.com/blogs/Minesh-Shah/Dates/2023/10/insights-from-setting-up-and-getting-started-with-saas-core/</link>            <description>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Shortly after Opticon San Diego OMVPs were given a chance to test drive and evaluate the SaaS Core CMS [Beta]. Having seen David Knipe&amp;rsquo;s demo in one of the breakout sessions I initially felt this should be plain sailing how wrong I was, fully expected considering it is in early Beta.&lt;/p&gt;
&lt;p&gt;Below I write about some of the pain points I found which will hopefully help the wider community avoid the issues I faced, and hopefully get to a fully working solution a lot quicker.&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;Initial Setup&lt;/h2&gt;
&lt;p&gt;So like David I wanted to utilise Remko&amp;rsquo;s Next.JS Presentation Layer and CMS Content Types, this is based on Mosey Bank Demo Templates. The Git Repository for this can be found &lt;a href=&quot;https://github.com/episerver/cms-saas-vercel-demo/&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Firstly, I started by Importing the Content Definitions and Content Instances into Optimizely, the import file is called InitialData.episerverdata.&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;Optimizely Graph Synch Issues&lt;/h2&gt;
&lt;p&gt;The content imported just fine although indexing this to Optimizely Graph was not working, later it was found that one of the properties utilised &amp;ldquo;&lt;strong&gt;LinkItem&lt;/strong&gt;&amp;rdquo; property was not being serialized properly which was causing the whole indexing Job to fail. This is a known Bug and Optimizely are currently working on rectifying this.&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;There&#39;s an issue during contents are being indexed: An error while sync content. Status: 0, message: Error when building index operation for contentIds: 27_30., An error while sync content. Status: 0, message: Error when building index operation for contentIds: 84_89.. (see log for more information)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I felt the best way to resolve the issue would be to delete the properties from the Content Types:&amp;nbsp;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Button&lt;/li&gt;
&lt;li&gt;Hero Banner&lt;/li&gt;
&lt;li&gt;Menu Item&lt;/li&gt;
&lt;li&gt;Website Footer&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Upon doing so my Content Graph Indexing Job ran thru fine. &lt;em&gt;(This would cause knock on affects with my presentation layer as the property is expected to be present in templates and graph more on this later).&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;img src=&quot;/link/f151a4e58dc6423891f62759db6b46b3.aspx&quot; /&gt;&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Vercel Deployment&lt;/h2&gt;
&lt;p&gt;Ok so now we have Content in CMS and the Graph Indexing Job looks to be working it was time to move onto deploying the presentation layer. For this we need to have a Git and Vercel account (Hobby account as a bare minimum).&lt;/p&gt;
&lt;p&gt;Navigating to Remok&amp;rsquo;s Git repo you will see the following Deploy button, simply click this to start the process.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/b4a92534275b41d99744e14d035bb189.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;When configuring the project you will need to populate some of the Optimizely Graph Security Tokens and DXP URL, these can be found in the Dashboard tab within Optimizely and the DXP URL will be the domain address you see in CMS e.g. &lt;a href=&quot;https://app-ocxcxxxx122w0uprod.cms.optimizely.com/&quot;&gt;https://app-ocxcxxxx122w0uprod.cms.optimizely.com/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/d59206291aec4c2f841e904618e33371.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;So, my first run of this did not work the build process failed due to missing properties of course I should have known this having deleted the LinkItem properties earlier.&lt;/p&gt;
&lt;p&gt;Rather than amend the frontend templates I added the LinkItem properties back into the Content Types ensuring they were not populated, this meant the serialization process for this property would not break.&lt;/p&gt;
&lt;p&gt;On my next run the build process did get further although I was getting errors on all of my landing pages which stated the following:&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;Error occurred prerendering page &quot;/en/services&quot;. Read more: https://nextjs.org/docs/messages/prerender-error
Error: Expected to load exactly one content item, received 0 from Optimizely Graph.
    at cms_content_CmsContent (/vercel/path0/apps/frontend/.next/server/chunks/998.js:197:2804)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async /vercel/path0/apps/frontend/.next/server/chunks/998.js:197:4372
    at async Promise.all (index 0)
    at async CmsContentArea (/vercel/path0/apps/frontend/.next/server/chunks/998.js:197:3977)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Having tried to resolve this issue via Content update on the specific Page Instances, I gave up as nothing I was changing in the Content was making an affect with the Build Process. Not getting far I thought it would be best to clear out all the content from Optimizely and start again with just a single Homepage and no block instances.&lt;/p&gt;
&lt;p&gt;e.g.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/1311d3d7d2a242c6b179c7bc7193d12a.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;I then deleted my Graph Index and re-ran the Indexing Job. After a successful Index I re-ran the deployment via Vercel and this time it was sucessful and a skeleton homepage was presented&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/db01853f1eab4ada9439e1c1e060c3df.aspx&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;On Page Editing&lt;/h2&gt;
&lt;p&gt;With my homepage loading I decided to add some content to the content area like a simple Rich Text Block into the Main Content Area, initially I created this as an &lt;strong&gt;Inline Block&lt;/strong&gt; which did not work, the template or graph currently cant handle this and it is something Optimizely are working on rectifying. Having changed the Inline Block to a Shared Block the content was rendering just fine on my Homepage.&lt;/p&gt;
&lt;p&gt;My next steps were to try and get the On Page Editing aspect of the CMS working for this I had to set the Hostnames up correctly within Manage Websites, my Edit Host is the SaaS CMS and Primary Host is the Vercel Front end i.e.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/5b75d4f6f3c9476c89c561826db86185.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Navigating to the CMS &amp;ldquo;On Page Edit&amp;rdquo; mode I assumed it would just work like shown in the Demo, to my dissmay I was presented with a 404 status code. After lots of digging and discussing with Optimizely it turned out they had turned off ContentReference ID in Optimizely Graph, thus the presentation layer could not route to the correct page to display the On Page edit view. Luckily after a day they re-enabled the numerical identifiers I cleared and re-indexed Opti Graph went back to On Page Edit Mode, and everything was now working.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/27ff0d99a26c4907bd6c3bf09f18e85e.aspx&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;Despite several challenges, I was happy to get the solution working including the On Page Editing aspect. If trying yourself after importing the Initial Data I would delete all the Content in Page Tree and Assets Folder and start building the content up once you have a successful deployment. &lt;span&gt;Some known bugs, such as Link Item Serialization and issues with Inline Blocks, require further investigation. Overall, I&#39;m optimistic about the potential of the SaaS CMS Core and look forward to its evolution.&lt;/span&gt;&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/Minesh-Shah/Dates/2023/10/insights-from-setting-up-and-getting-started-with-saas-core/</guid>            <pubDate>Mon, 23 Oct 2023 15:36:59 GMT</pubDate>           <category>Blog post</category></item><item> <title>Responsive Image Rendering at the Edge</title>            <link>https://world.optimizely.com/blogs/Minesh-Shah/Dates/2023/10/responsive-image-rendering-at-the-edge/</link>            <description>&lt;h2&gt;&lt;span&gt;Dynamic Image Resizing&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;span&gt;At Netcel, we recognize the significance of delivering high-performance solutions that enhance user experience and contribute to better Google rankings.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;To achieve this, we prioritize serving optimized images tailored to a user&#39;s viewport size, commonly referred to as responsive images. Our approach involves advising our clients to upload the highest quality image available, while we take care of dynamically resizing and serving the appropriate version to end users.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;Here&#39;s an example of what the image tag would resemble in the HTML source:&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;img src=&quot;/link/e783f492cf3e4d138cc59ef74b9a8060.aspx&quot; width=&quot;561&quot; height=&quot;374&quot; /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;In the given example, the image file &quot;about-intro-people-collage-4.jpg&quot; represents the original high-quality image. The srcset attribute complements the sizes attribute by specifying various image options with their corresponding widths. Together, these attributes determine the appropriate sizes of the image for different viewport widths, display pixel density and layout.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Seeing this in Action:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Small viewport (320 pixels width)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;img src=&quot;/link/7462a4ead0c84994aaf892603408822f.aspx&quot; /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Medium viewport (768 pixels width)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;img src=&quot;/link/f4b559c48b434092909d9bef0ecbb4e5.aspx&quot; /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Large viewport (1200 pixels width)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;img src=&quot;/link/a74be68970c2444a881e878a3610c19a.aspx&quot; /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;As you can see with each request at different viewports the images returned were different in both storage size and width this in return meant the time taken to load was also different. &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;To achieve dynamic resizing and optimization of images, it is common to employ specialized tools and libraries that can handle the processing on the server side. Some popular examples of such tools are Image Resizer, Image Processor, and Six Labors ImageSharp.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;Here&#39;s how the process typically works:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Image Resizing and Optimization:&lt;/strong&gt;&lt;br /&gt;
&lt;p&gt;These tools provide APIs or server-side libraries that can be integrated into your web application. When a request is made to an image, the server-side code utilizes these tools to resize and optimize the image on-the-fly based on the requested dimensions and optimization settings. This ensures that the image is served in the most appropriate format, size, and quality for the specific user&#39;s device and viewport.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Content Delivery Network (CDN) Caching:&lt;/strong&gt;
&lt;p&gt;Once an image is processed and served by the server, it is often beneficial to cache the optimized image in a Content Delivery Network (CDN). CDNs are distributed networks of servers located worldwide, designed to deliver content with low latency. By caching the optimized image in the CDN, subsequent requests for the same image can be served directly from the CDN servers, reducing the load on the origin server and improving response times for users across different regions.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The advantage of this approach is that the image processing and optimization tasks are performed dynamically on the server side, enabling flexibility and adaptability to different image requests. Once the image is processed and cached in the CDN, subsequent requests for the same image can be efficiently served directly from the CDN, reducing the need for repeated processing on the server.&lt;/p&gt;
&lt;h2&gt;Edge Resizing and .NET 6 Tag Helpers in Action&lt;/h2&gt;
&lt;p&gt;With the evolution to CMS12, we encountered challenges with compatibility and licensing agreements for the image optimization tools we previously used, especially in the context of .NET 6+ compatibility. We sought a solution that would alleviate the need for server-side processing and overcome these hurdles. Fortunately, Optimizely with Cloudflare are now enabling, &quot;Image resizing at the edge for CMS and Commerce.&quot; (Beta)&lt;/p&gt;
&lt;p&gt;This innovative feature from Optimizely allows us to leverage Cloudflare&#39;s powerful edge platform to transform images directly at the edge. It eliminates the necessity for additional tooling within our solution, removing the optimization burden from our servers entirely. Now, we can seamlessly resize, adjust quality, and even convert images to next-generation formats on demand.&lt;/p&gt;
&lt;p&gt;The HTML tags on the front end remain unchanged. However, instead of specifying the image widths as query string parameters, we construct the image URL in a specific format to leverage Cloudflare&#39;s functionality for resizing and serving the appropriate image. For example, we might use a URL structure like &quot;&lt;strong&gt;/cdn-cgi/image/width=80/siteassets/test-folder/demo.jpg&lt;/strong&gt;&quot; to indicate that we want Cloudflare to resize and deliver the image with a width of 80 pixels.&lt;/p&gt;
&lt;h3&gt;Model Attribution + Tag Helper&lt;/h3&gt;
&lt;p&gt;How we manage this all via code and attribute our Image Content References with the Width and Sizes attributes is quite very simple, here is an example of an Image on a Promo Block&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;    [Display(
            Name = &quot;Image&quot;,
            Description = &quot;Image&quot;,
            GroupName = TabsGroups.Content,
            Order = 100)]
    [CultureSpecific]
    [UIHint(UIHint.Image)]
    [ImageSize(Width = 210)]
    [ImageSize(Width = 280)]
    [ImageSize(Width = 335)]
    [ImageSize(Width = 420)]
    [ImageSize(Width = 452)]
    [ImageSize(Width = 526)]
    [ImageSize(Width = 550, IsDefault = true)]
    [ImageSize(Width = 560)]
    [ImageSize(Width = 630)]
    [ImageSize(Width = 670)]
    [ImageSize(Width = 840)]
    [ImageSize(Width = 904)]
    [ImageSize(Width = 1005)]
    [ImageSize(Width = 1052)]
    [ImageSize(Width = 1100)]
    [ImageSize(Width = 1356)]
    [ImageSize(Width = 1578)]
    [ImageSize(Width = 1650)]
    [ImageSizes(Sizes = &quot;(min-width: 1280px) 550px, (min-width: 1024px) calc(100vw - 120px) / 2, (min-width: 768px) calc(100vw - 80px) / 2, calc(100vw - 40px)&quot;)]
    [Required]
    public virtual ContentReference PromoImage { get; set; }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We have then created a simple Tag Helper to read these attributes and generate the Image Tag for us, some caveats to remember, resizing at the edge only works when routing via the CDN so not locally, also not when in the Optimizely Edit Interface. We have fallen back to not resizing in these scenarios and can be seen below.&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;[HtmlTargetElement(&quot;img&quot;, Attributes = &quot;image-for&quot;)]
public class EdgeImageTagHelper : TagHelper
{
    private readonly IUrlResolver _urlResolver;
    private readonly IContentLoaderService _contentLoader;
    private readonly IWebHostEnvironment _webHostEnvironment;
    public ModelExpression ImageFor { get; set; }
    public string CssClass { get; set; }
    public string Fit {get; set; }

    public EdgeImageTagHelper(IUrlResolver urlResolver, IContentLoaderService contentLoader, IWebHostEnvironment webHostEnvironment)
    {
        _urlResolver = urlResolver;
        _contentLoader = contentLoader;
        _webHostEnvironment = webHostEnvironment;
    }

    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        if (ImageFor.Model is not ContentReference edgeImage) return;
        if(ContentReference.IsNullOrEmpty(edgeImage)) return;
        if(!_contentLoader.TryGet(edgeImage, out ImageFile imageFile)) return;
            
        var actualUrl = _urlResolver.GetUrl(edgeImage);
        var property = ImageFor
            .Metadata
            .ContainerType
            .GetProperties()
            .FirstOrDefault(x =&amp;gt; x.Name == ImageFor.Metadata.PropertyName);

        if (property == null)
        {
            RenderNonResponsiveImage(output, actualUrl);
            return;
        }

        var shouldSkipEdgeResizing = _webHostEnvironment.IsDevelopment() || _contentLoader.IsContentInEditMode;
        var imageSize = property.GetCustomAttributes&amp;lt;ImageSizeAttribute&amp;gt;().ToList();
        var imageSizes = property.GetCustomAttribute&amp;lt;ImageSizesAttribute&amp;gt;();

        var defaultImageSize = imageSize.FirstOrDefault(x =&amp;gt; x.IsDefault);
        if (defaultImageSize == null)
        {
            RenderNonResponsiveImage(output, actualUrl);
            return;
        }

        var srcSetUrls = imageSize
            .OrderBy(x =&amp;gt; x.Width)
            .Select(s =&amp;gt; GetEdgeUrl(actualUrl, s.Width, shouldSkipEdgeResizing)).ToList();

        SetAttribute(output, &quot;src&quot;, GetEdgeUrl(actualUrl, defaultImageSize.Width, shouldSkipEdgeResizing, true));
        SetAttribute(output, &quot;srcset&quot;, string.Join(&quot;,&quot;, srcSetUrls));
        if (imageSizes != null) SetAttribute(output, &quot;sizes&quot;, imageSizes.Sizes);
        SetAttribute(output, &quot;alt&quot;, imageFile.AlternativeText);
        SetAttribute(output, &quot;loading&quot;, &quot;lazy&quot;);
        SetAttribute(output, &quot;decoding&quot;, &quot;auto&quot;);
        SetAttribute(output, &quot;fetchpriority&quot;, &quot;auto&quot;);
        SetAttribute(output, &quot;class&quot;, CssClass);

        if (imageFile is not IHasPixelSize pixelSize) return;

        if (pixelSize.Width &amp;gt; 0)
        {
            SetAttribute(output,&quot;width&quot;, pixelSize.Width.ToString());
        }

        if (pixelSize.Height &amp;gt; 0)
        {
            SetAttribute(output, &quot;height&quot;, pixelSize.Height.ToString());
        }
    }

    private void RenderNonResponsiveImage(TagHelperOutput output, string url)
    {
        SetAttribute(output, &quot;src&quot;, url);
        SetAttribute(output, &quot;class&quot;, CssClass);
    }

    private string GetEdgeUrl(string actualUrl, int width, bool isLocal, bool isDefault = false)
    {
        if (!isLocal)
        {
            return isDefault 
                ? $&quot;/cdn-cgi/image/{GetEdgeImageOptions(width)}{actualUrl}&quot; 
                : $&quot;/cdn-cgi/image/{GetEdgeImageOptions(width)}{actualUrl} {width}w&quot;;
        }
        else
        {
            return isDefault
                ? $&quot;{actualUrl}?{GetLocalImageOptions(width)}&quot;
                : $&quot;{actualUrl}?{GetLocalImageOptions(width)} {width}w&quot;;
        }
    }

    private string GetLocalImageOptions(int width)
    {
        var options = $&quot;width={width}&quot;;
        if (!string.IsNullOrWhiteSpace(Fit))
        {
            options += $&quot;&amp;amp;fit={Fit}&quot;;
        }

        return options;
    }

    private string GetEdgeImageOptions(int width)
    {
        var options = string.Empty;
        if (!string.IsNullOrWhiteSpace(Fit))
        {
            options = $&quot;fit={Fit},&quot;;
        }

        options += $&quot;width={width}&quot;;
        return options;
    }

    private void SetAttribute(TagHelperOutput output, string key, string value)
    {
        if (!string.IsNullOrWhiteSpace(value))
        {
            output.Attributes.SetAttribute(key, value);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;Then finally to utilise the Tag Helper in the Razor View we simple do the following :&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;        &amp;lt;div class=&quot;image&quot; @Html.EditAttributes(x=&amp;gt;x.PromoImage)&amp;gt;
            &amp;lt;img image-for=&quot;@Model.PromoImage&quot; css-class=&quot;promoBlockImage&quot; fit=&quot;cover&quot; FlexibleAttribute=&quot;Goes Here&quot;/&amp;gt;
        &amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Tag helpers for me are easier to comprehend and manage I also have the flexibility to add any additional attributes to my HTML tag without any additional coding.&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;The benefits of this new approach are numerous. First and foremost, the image transformation takes place at the edge, leveraging Cloudflare&#39;s global network of servers. This ensures lightning-fast delivery of optimized images with minimal latency, enhancing overall performance and user experience.&lt;/p&gt;
&lt;p&gt;Furthermore, as the solution is provided by Optimizely and integrated into CMS12 and the Optimizely DXP, we can confidently rely on their support and compatibility with the latest versions of .NET. Say goodbye to the worries of compatibility issues and the hassle of ever-changing licensing agreements.&lt;/p&gt;
&lt;p&gt;By leveraging &quot;Image resizing at the edge for CMS and Commerce,&quot; we have the potential to simplify our image optimization workflow. There&#39;s no longer a need for server-side processing or the installation of additional tools. The optimization process seamlessly occurs within the Cloudflare edge platform, making our solution more scalable and efficient.&lt;/p&gt;
&lt;p&gt;In conclusion, Optimizely&#39;s solution will empower us to overcome the challenges we faced with previous tools and server-side optimization. With image processing now taking place at the edge, we can deliver optimized images effortlessly, enhancing user experience and performance without adding complexity to our server infrastructure.&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/Minesh-Shah/Dates/2023/10/responsive-image-rendering-at-the-edge/</guid>            <pubDate>Thu, 12 Oct 2023 19:29:24 GMT</pubDate>           <category>Blog post</category></item><item> <title>How to Write an xUnit Test to Verify Unique Content Type Guids in Content Management</title>            <link>https://world.optimizely.com/blogs/Minesh-Shah/Dates/2023/3/how-to-write-an-xunit-test-to-verify-unique-content-type-guids-in-content-management/</link>            <description>&lt;p&gt;When developing an Optimizely CMS solution, it is important to ensure that each content type has a unique GUID. If two or more content types share the same GUID, the CMS can have unexpected behavior and issues. In this blog post, we will explore how to write an xUnit test to verify that all Content Type Guids are unique in an Content Management solution.&lt;/p&gt;
&lt;h3&gt;The Problem&lt;/h3&gt;
&lt;p&gt;&lt;span&gt;When developing an Optimizely CMS solution, it is common to define content types using C# classes that implement the &lt;/span&gt;&lt;code&gt;IContentData&lt;/code&gt;&lt;span&gt; interface. Each content type is typically decorated with the &lt;/span&gt;&lt;code&gt;ContentType&lt;/code&gt;&lt;span&gt; attribute, which defines properties such as the name and GUID of the content type.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;When multiple content types share the same GUID, it can cause unexpected behavior and issues in the CMS. For example, if two content types have the same GUID, it can cause an exception to be thrown when attempting to create a new instance of one of the content types.&lt;/span&gt;&lt;/p&gt;
&lt;h3&gt;The Solution&lt;/h3&gt;
&lt;p&gt;&lt;span&gt;To verify that all Content Type Guids are unique in an Optimizely Content Management solution, we can write an xUnit test that iterates through all the content types in the solution and checks that each content type has a unique GUID. Here is the code for the test:&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using EPiServer.Core;
using EPiServer.DataAbstraction;
using Xunit;

namespace YourProject.Tests
{
    public class ContentTypesGuidTest
    {
        [Fact]
        public void VerifyContentTypesGuidsAreUnique()
        {
             // Get the assembly that contains the Project.Cms.Domain project
             var domainAssembly = Assembly.Load(&quot;Project.Cms.Domain&quot;);

            // Get all content types in the solution, this covers Pages, Blocks and Assets
            var contentTypes = typeof(PageData).Assembly.GetTypes()
                .Where(x =&amp;gt; typeof(IContentData).IsAssignableFrom(x) &amp;amp;&amp;amp; !x.IsAbstract);

            // Create a dictionary to hold the Guids and their counts
            var guidDictionary = new Dictionary&amp;lt;Guid, int&amp;gt;();

            // Iterate through all the content types
            foreach (var contentType in contentTypes)
            {
                // Get the SiteContentType attribute of the content type
                var siteContentTypeAttribute = contentType.GetCustomAttribute&amp;lt;SiteContentTypeAttribute&amp;gt;();

                // Get the value of the Guid property in the SiteContentType attribute
                var guid = siteContentTypeAttribute.GUID;

                // Check if the Guid already exists in the dictionary
                if (guidDictionary.ContainsKey(guid))
                {
                    // Increment the count for the Guid if it already exists
                    guidDictionary[guid]++;
                }
                else
                {
                    // Add the Guid to the dictionary if it doesn&#39;t exist
                    guidDictionary.Add(guid, 1);
                }
            }

            // Iterate through the dictionary to check if there are any Guids with a count greater than 1
            foreach (var guid in guidDictionary)
            {
                Assert.Equal(1, guid.Value); // Ensure the count for each Guid is equal to 1
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span&gt;This test code uses reflection to get all the content types in the solution that implement the &lt;/span&gt;&lt;code&gt;IContentData&lt;/code&gt;&lt;span&gt; interface and are not abstract. It then iterates through all the content types and checks that each content type has a unique GUID. The test fails if any content type has a GUID that is not unique.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;By writing an xUnit test to verify that all Content Type Guids are unique in an Optimizely solution, we can ensure that the CMS has expected behavior and avoid potential issues caused by duplicate GUIDs.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/Minesh-Shah/Dates/2023/3/how-to-write-an-xunit-test-to-verify-unique-content-type-guids-in-content-management/</guid>            <pubDate>Mon, 27 Mar 2023 12:55:15 GMT</pubDate>           <category>Blog post</category></item><item> <title>URL Rewrites in CMS12 (.Net 6) </title>            <link>https://world.optimizely.com/blogs/Minesh-Shah/Dates/2023/2/url-rewrites-in-cms12--net-6-/</link>            <description>&lt;p&gt;URL rewriting is a common technique used in web applications to create user-friendly URLs that are easier to understand, provide consistency and have SEO benefits. In the past, URL rewriting was commonly accomplished using IIS URL Rewrite module. However, with the release of .NET Core, the process of rewriting URLs has undergone some changes.&lt;/p&gt;
&lt;p&gt;Microsoft has introduced a new URL rewriting middleware called &lt;strong&gt;Microsoft.AspNetCore.Rewrite&lt;/strong&gt;. This middleware is part of the ASP.NET Core framework and is designed to rewrite URLs in a much more flexible and efficient way.&lt;/p&gt;
&lt;p&gt;One of the key benefits of using the &lt;strong&gt;Microsoft.AspNetCore.Rewrite&lt;/strong&gt; middleware is that it allows URL rewriting without requiring IIS or any other web server. This means that we can create Optimizely Solutions that are completely self-contained and can be run on any platform.&lt;/p&gt;
&lt;p&gt;Here is an example of how to use the &lt;strong&gt;Microsoft.AspNetCore.Rewrite&lt;/strong&gt; middleware in .NET 6 to handle some common conventions like redirecting to https, enforcing lowercase urls and adding trailing slash to the end of all URLs:&lt;/p&gt;
&lt;h3&gt;Lower Case URL &amp;ndash; Implement IRule Base Rule&lt;/h3&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;    public class LowercaseUrlsRule : IRule
    {
        public int StatusCode { get; } = (int)HttpStatusCode.MovedPermanently;

        public void ApplyRule(RewriteContext context)
        {
            HttpRequest request = context.HttpContext.Request;
            PathString path = context.HttpContext.Request.Path;
            HostString host = context.HttpContext.Request.Host;

            if (path.HasValue &amp;amp;&amp;amp; path.Value.Any(char.IsUpper) || host.HasValue &amp;amp;&amp;amp; host.Value.Any(char.IsUpper))
            {
                HttpResponse response = context.HttpContext.Response;
                response.StatusCode = StatusCode;
                response.Headers[HeaderNames.Location] = (request.Scheme + &quot;://&quot; + host.Value + request.PathBase.Value + request.Path.Value).ToLower() + request.QueryString;
                context.Result = RuleResult.EndResponse;
            }
            else
            {
                context.Result = RuleResult.ContinueRules;
            }
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Use Rules in Middleware Startup.cs&lt;/h3&gt;
&lt;p&gt;In the example below we are using the &lt;strong&gt;RewriteOptions &lt;/strong&gt;class to define paths that should be negated, and adding the rules for forcing HTTPS, Lowercase and Trailing Slashes. I have explicitly added below &lt;strong&gt;app.UseStaticFiles()&lt;/strong&gt; so the rules do not get added to files like css, javascript or images.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;        app.UseStaticFiles();

        var options = new RewriteOptions()
            .Add(context =&amp;gt;
                {
                    if (context.HttpContext.Request.Path.StartsWithSegments(&quot;/util&quot;) || 
                        context.HttpContext.Request.Path.StartsWithSegments(&quot;/episerver&quot;) || 
                        context.HttpContext.Request.Path.StartsWithSegments(&quot;/modules&quot;))
                    {
                        context.Result = RuleResult.SkipRemainingRules;
                    }
                })
            // Redirect to HTTPS
            .AddRedirectToHttpsPermanent()
            // Enforce lower case. 
            .Add(new LowercaseUrlsRule())
            // Enforce trailing slash.
            .AddRedirect(&quot;(.*[^/])$&quot;, &quot;$1/&quot;);



        app.UseRewriter(options);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These techniques are not Optimizely specific and can be applied to any .Net Solution, more info on IRule based rewrites can be found &lt;a href=&quot;https://learn.microsoft.com/en-us/aspnet/core/fundamentals/url-rewriting?view=aspnetcore-7.0&quot;&gt;here&lt;/a&gt;&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/Minesh-Shah/Dates/2023/2/url-rewrites-in-cms12--net-6-/</guid>            <pubDate>Tue, 28 Feb 2023 23:22:56 GMT</pubDate>           <category>Blog post</category></item></channel>
</rss>