<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom"><title type="text">Blogs</title><link href="http://world.optimizely.com" /><updated>2026-08-23T17:38:18.0000000Z</updated><id>https://world.optimizely.com/blogs/</id> <generator uri="http://world.optimizely.com" version="2.0">Optimizely World</generator> <entry><title>Copies vs. Connections: Modeling Content Areas for Multilingual Success</title><link href="https://world.optimizely.com/blogs/vipin-banka--learnings--insights/dates/2026/8/copies-vs.-connections-modeling-content-areas-for-multilingual-success" /><id>&lt;h2&gt;Starting With the Simple Question&lt;/h2&gt;
&lt;p&gt;Most multilingual content modeling conversations start with a deceptively simple question from a client:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;If we set this image once in English, will it show up automatically in French?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It sounds like a yes-or-no question. In Optimizely SaaS CMS, the honest answer is: &lt;strong&gt;it depends entirely on how that image was modeled.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Before diving into anything complex, here are three quick sanity checks worth running on your own project. If you can answer all three confidently, you are probably in good shape:&lt;/p&gt;
&lt;ol class=&quot;tight&quot;&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Open a translated page in your CMS.&lt;/strong&gt; Change something in the master language. Does the translated version follow, or stay as it was?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Look at a Content Area in the editor.&lt;/strong&gt; Can you tell, at a glance, what each item inside it actually is?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Ask a translator to open a page.&lt;/strong&gt; Do they immediately know which field maps to which part of the design?&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If any of those made you pause, this post is written for you. Nothing here is exotic or advanced. It is simply about understanding what the platform stores where, and choosing shapes that match what your editors and translators actually need.&lt;/p&gt;
&lt;p&gt;This is an area where reasonable teams land on different answers, so treat what follows as a starting point for your own discussion rather than a fixed rule set.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Designing for Both the Code and the Editor&lt;/h2&gt;
&lt;p&gt;One of the genuine strengths of Optimizely SaaS CMS is its code-first, headless approach. Developers define content types directly in TypeScript and push them straight to the CMS, keeping the model in version control alongside the front-end application.&lt;/p&gt;
&lt;p&gt;But a content model is not only a database schema or a GraphQL query shape. It is also the daily interface for your editorial team.&lt;/p&gt;
&lt;p&gt;When we model content, we are designing the workspace that authors, content managers, and translators will live in. Model purely for developer convenience and it is easy to hand editors something that is awkward to navigate, fragile across languages, and difficult to translate cleanly.&lt;/p&gt;
&lt;p&gt;This post focuses on one specific area of that decision space: &lt;strong&gt;Content Areas&lt;/strong&gt;, and how the items inside them behave once you introduce a second language.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;The Core Building Blocks&lt;/h2&gt;
&lt;p&gt;There are three primary ways to structure content within a page or experience in Optimizely SaaS CMS.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Flat properties&lt;/strong&gt; are scalar fields defined directly on the content type, such as a &lt;strong&gt;string&lt;/strong&gt;, a &lt;strong&gt;contentReference&lt;/strong&gt;, or a &lt;strong&gt;boolean&lt;/strong&gt;. They are simple, predictable, and the easiest to reason about across languages.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Embedded components&lt;/strong&gt; are nested data structures stored inside the parent page&#39;s data as a cohesive group. Their lifecycle is bound to the parent, so publishing the page publishes the component with it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Content arrays&lt;/strong&gt; are the headless equivalent of a &lt;strong&gt;Content Area&lt;/strong&gt;. They hold a collection of content items, and those items can be one of two kinds:&lt;/p&gt;
&lt;ul class=&quot;tight&quot;&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Shared blocks&lt;/strong&gt; are reusable items that live in the Assets panel with their own publishing workflow and their own locale branches.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Inline blocks&lt;/strong&gt; are created on the fly inside the Content Area. They have no independent identity and their lifecycle is tied entirely to the parent page.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each of these is a good fit for something. The interesting question is which one fits the requirement in front of you.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;The SHARED Label: A Signal Worth Teaching&lt;/h2&gt;
&lt;p&gt;One of the most useful affordances in the CMS editing interface is a small label: &lt;strong&gt;SHARED&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;When an author opens a Content Area, each item inside it is visually marked:&lt;/p&gt;
&lt;ul class=&quot;tight&quot;&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Shared blocks&lt;/strong&gt; show a &lt;strong&gt;SHARED&lt;/strong&gt; badge next to their name.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Inline blocks&lt;/strong&gt; show no badge at all.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is not decoration. It is a live signal about editorial reach:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If you see SHARED, this block is a connected item. Editing it affects every page that references it, and for non-localizable properties it updates all language versions at once.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;An inline block, by contrast, is a private copy owned by that one page. On screen the two look almost identical. The consequences of editing them are quite different.&lt;/p&gt;
&lt;p&gt;This is worth five minutes in editor onboarding. It is one of the simplest ways to prevent a well-meaning edit from rippling further than anyone intended.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Naming Habits: Small Effort, Large Payoff&lt;/h2&gt;
&lt;p&gt;When an author creates an inline block inside a Content Area, the CMS offers a Name field. It is optional. Leave it blank and the CMS fills it with the content type name.&lt;/p&gt;
&lt;p&gt;The code compiles and the API returns everything correctly. But let&#39;s look at what actually happens in the editor&#39;s workspace on a multilingual site when we don&#39;t establish a naming standard.&lt;/p&gt;
&lt;p&gt;Imagine an editor builds a Modular Landing Page using a Content Area, where they can add a flexible sequence of blocks: a hero banner, three promotional cards, and a call-to-action button. Because a landing page is highly dynamic, a flexible Content Area is exactly the right tool.&lt;/p&gt;
&lt;p&gt;However, if they leave the optional Name fields blank during block creation, the CMS defaults each name to the block&#39;s content type (such as &lt;strong&gt;PromoCard&lt;/strong&gt; or &lt;strong&gt;HeroBanner&lt;/strong&gt;).&lt;/p&gt;
&lt;p&gt;Here is what the English and French editors see side-by-side in their page editor:&lt;/p&gt;
&lt;pre class=&quot;code-block&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;THE UNNAMED PAIN POINT (What the translator sees)

English Page                          French Page (Copied)
Landing Page                          Landing Page
 |- HeroBanner                         |- HeroBanner
 |- PromoCard                          |- PromoCard
 |- PromoCard                          |- PromoCard
 |- PromoCard                          |- PromoCard
 |- CTABlock                           |- CTABlock&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;The Problem:&lt;/strong&gt; The list is completely anonymous. If the French translator wants to translate the text for the &quot;Free Shipping&quot; card vs. the &quot;20% Discount&quot; card, they have no idea which &lt;strong&gt;PromoCard&lt;/strong&gt; is which. They have to click and open every single inline block on both sides of the screen to match them up. This turns a straightforward translation task into a tedious guessing game.&lt;/p&gt;
&lt;h3&gt;A Convention That Solves It&lt;/h3&gt;
&lt;p&gt;Now look at the difference when we introduce a simple, structured naming convention. If we instruct authors to prefix their inline blocks with a simple numeric order and a functional marketing label, the translation workspace transforms:&lt;/p&gt;
&lt;pre class=&quot;code-block&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;THE NAMED BEST PRACTICE (Effortless translation)

English Page                          French Page (Copied)
Landing Page                          Landing Page
 |- 1. Hero - Summer Promo             |- 1. Hero - Summer Promo
 |- 2. Card - Free Shipping            |- 2. Card - Free Shipping
 |- 3. Card - 20% Discount             |- 3. Card - 20% Discount
 |- 4. Card - New Arrivals             |- 4. Card - New Arrivals
 |- 5. CTA - Sign Up                   |- 5. CTA - Sign Up&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;The Benefit:&lt;/strong&gt; Because the names are cloned during the copy-to-language flow, the French translator immediately knows the context of each block. They click exactly the card they want, translate the English text to French, and can optionally update the block names if they choose. The page layout remains perfectly clear, organized, and predictable.&lt;/p&gt;
&lt;p&gt;For shared blocks, a meaningful name is even more critical because that name appears in the global Assets panel where every editor on the project finds, filters, and manages them:&lt;/p&gt;
&lt;pre class=&quot;code-block&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Assets panel
 |- Global Blocks
     |- Homepage Hero Banner (SHARED)
     |- Q3 Promo CTA (SHARED)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Agreeing this convention before the first content entry session is a small investment that keeps paying off as the site grows.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;The Multilingual Flow: Copies and Connections&lt;/h2&gt;
&lt;p&gt;Optimizely SaaS CMS makes creating a language variant straightforward. When you add a French version of an English page, the CMS offers to &lt;strong&gt;copy content from English&lt;/strong&gt;, duplicating the structure and cloning the inline block data so translators start from a complete page rather than a blank one.&lt;/p&gt;
&lt;p&gt;What happens after that copy depends on whether the items are inline or shared.&lt;/p&gt;
&lt;h3&gt;Inline Blocks: A Snapshot in Time&lt;/h3&gt;
&lt;p&gt;Inline blocks have no independent key, so they belong entirely to the language version of the page that owns them. Once the copy is made, the two sets of blocks are separate. Update the image in the English block and the French page keeps the one it already had. Fix a typo in English and the French text stays as it was. Add or reorder blocks in English and the French arrangement is unaffected.&lt;/p&gt;
&lt;p&gt;This is reasonable behaviour, and for some teams it is exactly what they want. It gives regional editors complete freedom to shape their own market&#39;s content without a central team overwriting their work. The trade-off is that any master-language change needs to be applied again in each translated version.&lt;/p&gt;
&lt;p&gt;Where this catches teams out is when the expectation was the opposite. If a client believes they are setting a value once for all markets, this is the moment to talk it through.&lt;/p&gt;
&lt;h3&gt;Shared Blocks: A Genuine Connection&lt;/h3&gt;
&lt;p&gt;Shared blocks are content items in their own right, with their own key and their own locale branches. When both the English and French pages reference the same shared block, they are pointing at the &lt;strong&gt;same item&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;For properties marked as non-localizable, the value is held once and served to every language version of that block. Change it in the master language and all languages reflect it. In the editor, those fields appear as read-only when working in a non-master language, with an indicator showing where the value comes from.&lt;/p&gt;
&lt;p&gt;For properties marked as localizable, each locale branch keeps its own value, exactly as you would expect for translatable text.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;The Localizable Setting&lt;/h2&gt;
&lt;p&gt;The SDK exposes a &lt;strong&gt;localizable&lt;/strong&gt; flag on property definitions. Setting it to false means one shared value across languages; setting it to true means a unique value per language version.&lt;/p&gt;
&lt;pre class=&quot;code-block&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;export const ImageWithAltCT = contentType({
  key: &#39;ImageWithAlt&#39;,
  baseType: &#39;_component&#39;,
  properties: {
    image: {
      type: &#39;contentReference&#39;,
      allowedTypes: [&#39;_image&#39;],
      displayName: &#39;Image&#39;,
      localizable: false,
    },
    altText: {
      type: &#39;string&#39;,
      displayName: &#39;Alt text&#39;,
      localizable: true,
    },
  },
});&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The CLI maps this to the &lt;strong&gt;Unique value per language&lt;/strong&gt; setting on the property when types are pushed to the CMS.&lt;/p&gt;
&lt;h3&gt;Where the Setting Creates a Connection&lt;/h3&gt;
&lt;p&gt;This is the part most worth internalising, because the same setting behaves differently depending on where the property lives.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; border: 1px solid rgb(0, 0, 0);&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left; vertical-align: top; background-color: rgb(236, 240, 241); border: 1px solid rgb(0, 0, 0); padding: 10px; width: 20.8323%;&quot;&gt;
&lt;p&gt;&lt;strong&gt;Where the property lives&lt;/strong&gt;&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;text-align: left; vertical-align: top; background-color: rgb(236, 240, 241); border: 1px solid rgb(0, 0, 0); padding: 10px; width: 79.1677%;&quot;&gt;
&lt;p&gt;&lt;strong&gt;Effect of setting localizable to false&lt;/strong&gt;&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left; vertical-align: top; border: 1px solid rgb(0, 0, 0); padding: 10px; width: 20.8323%;&quot;&gt;
&lt;p&gt;Flat property on a page&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;text-align: left; vertical-align: top; border: 1px solid rgb(0, 0, 0); padding: 10px; width: 79.1677%;&quot;&gt;
&lt;p&gt;One value shared by every language version of that page. Change it in the master language and all locales reflect it.&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left; vertical-align: top; border: 1px solid rgb(0, 0, 0); padding: 10px; width: 20.8323%;&quot;&gt;
&lt;p&gt;Property on a shared block&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;text-align: left; vertical-align: top; border: 1px solid rgb(0, 0, 0); padding: 10px; width: 79.1677%;&quot;&gt;
&lt;p&gt;One value shared across all locale branches of that block. Both the English and French pages referencing it see the update.&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left; vertical-align: top; border: 1px solid rgb(0, 0, 0); padding: 10px; width: 20.8323%;&quot;&gt;
&lt;p&gt;Property inside an inline block&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;text-align: left; vertical-align: top; border: 1px solid rgb(0, 0, 0); padding: 10px; width: 79.1677%;&quot;&gt;
&lt;p&gt;The setting still governs whether the field is translatable, but there are two separate anonymous copies. A change in the English copy does not reach the French copy.&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The underlying principle is straightforward once you see it: &lt;strong&gt;a shared value needs something stable to attach to.&lt;/strong&gt; Flat properties and shared blocks both have a stable identity. Inline blocks do not, so there is nothing for the two language versions to share.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Pairing Shared Blocks With Localizable Settings&lt;/h2&gt;
&lt;p&gt;When you need grouped content, such as an image with its alt text, and that group needs to behave sensibly across languages, combining a shared block with selective localizable settings is a clean approach worth considering.&lt;/p&gt;
&lt;p&gt;Using the definition above as a shared block, referenced from both the English and French pages, gives you this behaviour:&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; border: 1px solid rgb(0, 0, 0);&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: rgb(236, 240, 241); text-align: left; vertical-align: top; border: 1px solid rgb(0, 0, 0); padding: 10px;&quot;&gt;
&lt;p&gt;&lt;strong&gt;Action&lt;/strong&gt;&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;background-color: rgb(236, 240, 241); text-align: left; vertical-align: top; border: 1px solid rgb(0, 0, 0); padding: 10px;&quot;&gt;
&lt;p&gt;&lt;strong&gt;Result&lt;/strong&gt;&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; border: 1px solid rgb(0, 0, 0);&quot;&gt;
&lt;p&gt;English editor selects a different image&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;padding: 10px; border: 1px solid rgb(0, 0, 0);&quot;&gt;
&lt;p&gt;French page shows the new image, since the property is non-localizable on a shared item&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; border: 1px solid rgb(0, 0, 0);&quot;&gt;
&lt;p&gt;French translator updates the French alt text&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;padding: 10px; border: 1px solid rgb(0, 0, 0);&quot;&gt;
&lt;p&gt;English alt text is unaffected&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; border: 1px solid rgb(0, 0, 0);&quot;&gt;
&lt;p&gt;English editor updates the English alt text&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;padding: 10px; border: 1px solid rgb(0, 0, 0);&quot;&gt;
&lt;p&gt;French alt text is unaffected&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The result is the split many multilingual projects are reaching for: shared structural assets, independently translated text, enforced by the platform rather than by editorial vigilance.&lt;/p&gt;
&lt;p&gt;Worth noting honestly: this pattern also means the shared block is genuinely shared. Editing it affects every page referencing it. That is the intent, but it should be a deliberate choice rather than a side effect, and the SHARED badge is what makes it visible to authors.&lt;/p&gt;
&lt;h3&gt;Choosing Between Flat Properties and Shared Blocks&lt;/h3&gt;
&lt;p&gt;Both approaches keep languages aligned. The question that usually separates them is whether the content is genuinely reused.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; border: 1px solid rgb(0, 0, 0);&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: rgb(236, 240, 241); text-align: left; vertical-align: top; border: 1px solid rgb(0, 0, 0); padding: 10px;&quot;&gt;
&lt;p&gt;&lt;strong&gt;Scenario&lt;/strong&gt;&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;background-color: rgb(236, 240, 241); text-align: left; vertical-align: top; border: 1px solid rgb(0, 0, 0); padding: 10px;&quot;&gt;
&lt;p&gt;&lt;strong&gt;Approach worth considering&lt;/strong&gt;&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; border: 1px solid rgb(0, 0, 0);&quot;&gt;
&lt;p&gt;Image and alt text in one fixed slot on one page&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;padding: 10px; border: 1px solid rgb(0, 0, 0);&quot;&gt;
&lt;p&gt;Flat &lt;strong&gt;contentReference&lt;/strong&gt; set to non-localizable, plus a flat &lt;strong&gt;string&lt;/strong&gt; set to localizable&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; border: 1px solid rgb(0, 0, 0);&quot;&gt;
&lt;p&gt;The same grouped structure appearing on many pages&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;padding: 10px; border: 1px solid rgb(0, 0, 0);&quot;&gt;
&lt;p&gt;A shared block with the image non-localizable and the alt text localizable&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; border: 1px solid rgb(0, 0, 0);&quot;&gt;
&lt;p&gt;Analytics identifiers or configuration values&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;padding: 10px; border: 1px solid rgb(0, 0, 0);&quot;&gt;
&lt;p&gt;A flat &lt;strong&gt;string&lt;/strong&gt; set to non-localizable on the parent type&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; border: 1px solid rgb(0, 0, 0);&quot;&gt;
&lt;p&gt;A centrally managed promo banner or CTA&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;padding: 10px; border: 1px solid rgb(0, 0, 0);&quot;&gt;
&lt;p&gt;A shared block, structural fields non-localizable, headline localizable&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;A reasonable rule of thumb: if nobody can name two pages today where the same instance genuinely needs to update together, starting with flat properties keeps things simple. Moving to a shared block later is a one-time migration. Unwinding a shared block that turned out not to be shared is usually more work.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;A Practical Example: Analytics Container IDs&lt;/h2&gt;
&lt;p&gt;Consider a tracking container ID that needs to be present across the site.&lt;/p&gt;
&lt;p&gt;Placed inside an inline block within a Content Area, it gets copied into each language version when the variant is created. If that ID ever changes, someone needs to open every language version of every page holding it and update the field individually. On a site with ten languages and fifty pages, that is a lot of repetitive edits for a single value.&lt;/p&gt;
&lt;p&gt;Modeled as a flat property on the parent type with localizable set to false, the same change is made once in the master language and every language version reflects it.&lt;/p&gt;
&lt;p&gt;The useful distinction here is that a tracking ID is not translatable content at all. It is configuration. Recognising which of your fields are configuration rather than content often makes the modeling choice obvious.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;What Happens If an Author Deletes a Block&lt;/h2&gt;
&lt;p&gt;This question comes up on most projects, and the answer is more reassuring than teams often expect.&lt;/p&gt;
&lt;p&gt;Inline blocks do not appear in the global Trash, because they are not standalone items. Deleting one is closer to clearing a property value on the parent page. However, the parent page&#39;s version history does cover them.&lt;/p&gt;
&lt;p&gt;Every save and publish creates a revision in the Versions gadget, and rolling back to an earlier revision restores the Content Area as it was, including the inline blocks it contained. For everyday accidental deletions, this works well.&lt;/p&gt;
&lt;p&gt;There are limits worth knowing. The Versions gadget holds a &lt;strong&gt;20-version history&lt;/strong&gt;, so if a deletion goes unnoticed across many subsequent publishes, the revision containing the block can age out. The documentation is also explicit that content versions are not covered by trash management, meaning a version deleted directly from the gadget is permanently gone.&lt;/p&gt;
&lt;p&gt;Shared blocks behave differently here in a way that favours them: because they are standalone items, they do go to the Trash and can be restored independently of the pages referencing them.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;A Decision Matrix to Discuss With Your Team&lt;/h2&gt;
&lt;p&gt;This is offered as a conversation starter rather than a prescription. Your team&#39;s editorial workflow may well justify different choices.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; border: 1px solid rgb(0, 0, 0);&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: rgb(236, 240, 241); text-align: left; vertical-align: top; border: 1px solid rgb(0, 0, 0); padding: 10px;&quot;&gt;
&lt;p&gt;&lt;strong&gt;Requirement&lt;/strong&gt;&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;background-color: rgb(236, 240, 241); text-align: left; vertical-align: top; border: 1px solid rgb(0, 0, 0); padding: 10px;&quot;&gt;
&lt;p&gt;&lt;strong&gt;Approach worth considering&lt;/strong&gt;&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;background-color: rgb(236, 240, 241); text-align: left; vertical-align: top; border: 1px solid rgb(0, 0, 0); padding: 10px;&quot;&gt;
&lt;p&gt;&lt;strong&gt;Reasoning&lt;/strong&gt;&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; border: 1px solid rgb(0, 0, 0);&quot;&gt;
&lt;p&gt;A fixed slot unique to one page&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;padding: 10px; border: 1px solid rgb(0, 0, 0);&quot;&gt;
&lt;p&gt;Flat &lt;strong&gt;contentReference&lt;/strong&gt; and &lt;strong&gt;string&lt;/strong&gt; properties&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;padding: 10px; border: 1px solid rgb(0, 0, 0);&quot;&gt;
&lt;p&gt;Structure stays locked, image can be shared across languages, alt text stays per language&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; border: 1px solid rgb(0, 0, 0);&quot;&gt;
&lt;p&gt;The same grouped structure on many pages&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;padding: 10px; border: 1px solid rgb(0, 0, 0);&quot;&gt;
&lt;p&gt;A shared block with mixed localizable settings&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;padding: 10px; border: 1px solid rgb(0, 0, 0);&quot;&gt;
&lt;p&gt;One instance referenced everywhere, with the SHARED badge making the reach visible&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; border: 1px solid rgb(0, 0, 0);&quot;&gt;
&lt;p&gt;Configuration values identical everywhere&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;padding: 10px; border: 1px solid rgb(0, 0, 0);&quot;&gt;
&lt;p&gt;A flat &lt;strong&gt;string&lt;/strong&gt; set to non-localizable&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;padding: 10px; border: 1px solid rgb(0, 0, 0);&quot;&gt;
&lt;p&gt;One edit updates all languages&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; border: 1px solid rgb(0, 0, 0);&quot;&gt;
&lt;p&gt;Centrally managed reusable content&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;padding: 10px; border: 1px solid rgb(0, 0, 0);&quot;&gt;
&lt;p&gt;A Content Area holding shared blocks&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;padding: 10px; border: 1px solid rgb(0, 0, 0);&quot;&gt;
&lt;p&gt;Managed from the Assets panel, changes propagate to all referencing pages&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px; border: 1px solid rgb(0, 0, 0);&quot;&gt;
&lt;p&gt;Genuinely flexible author-composed layouts&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;padding: 10px; border: 1px solid rgb(0, 0, 0);&quot;&gt;
&lt;p&gt;A Content Area holding inline blocks&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;padding: 10px; border: 1px solid rgb(0, 0, 0);&quot;&gt;
&lt;p&gt;Full composition freedom, with the naming convention and language copy flow documented for the team&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;Questions Worth Asking Before You Model&lt;/h2&gt;
&lt;p&gt;Running through these with your technical and editorial teams tends to surface the right answer quickly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Is the item count fixed by the design?&lt;/strong&gt; If the layout always calls for exactly three slots, flat properties express that more directly than an array.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Should any field update everywhere when the master language changes?&lt;/strong&gt; If so, a flat property or a shared block will give you that. An inline block will not.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Is this content genuinely reused across pages today?&lt;/strong&gt; If no one can name two specific pages, starting simple is usually the safer bet.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Are we planning to use an external translation service?&lt;/strong&gt; Stable named properties are generally easier for translation tooling to map reliably than nested arrays of anonymous items. This is worth validating with your specific vendor before committing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Have editors been shown what the SHARED badge means?&lt;/strong&gt; A short walkthrough prevents a lot of surprises.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Have we agreed a naming convention?&lt;/strong&gt; Documenting it before the first content session is far easier than retrofitting it later.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Wrapping Up&lt;/h2&gt;
&lt;p&gt;Optimizely SaaS CMS gives teams a genuinely flexible toolkit here. Flat properties, embedded components, inline blocks, and shared blocks each serve a real purpose, and the localizable setting gives you fine-grained control over which values travel with translations and which stay anchored to one source of truth.&lt;/p&gt;
&lt;p&gt;If there is one idea worth carrying into your next modeling conversation, it is this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A shared value needs a stable identity to attach to. Flat properties and shared blocks have one. Inline blocks, by design, do not.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Everything else tends to follow from that. Decide deliberately, write the decisions down, walk your editors through the SHARED badge, and test the language copy flow on a real page before go-live rather than after.&lt;/p&gt;
&lt;p&gt;I would genuinely like to hear how other teams are approaching this. Multilingual modeling is one of those areas where the best answer often depends on how a specific editorial team works day to day, and there is a lot to learn from comparing notes.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;How is your team handling Content Areas across languages?&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em class=&quot;_1gm2eq5 _4n13zc&quot;&gt;If you enjoyed this technical breakdown, you&amp;rsquo;ll like my monthly newsletter,&amp;nbsp;&lt;strong class=&quot;_1gm2eq5 lab8bo _4n13zc&quot;&gt;&lt;a class=&quot;_4n13zc _1l4j4nae _1q9ygi7 _4ivg92 _12zsz04 _4n13zc&quot; href=&quot;https://www.linkedin.com/newsletters/pragmatic-judgement-7487084233719549953/&quot;&gt;Pragmatic Judgement&lt;/a&gt;&lt;/strong&gt;. I write about where technical decisions meet real operational constraints for tech and delivery leaders.&lt;/em&gt;&lt;/p&gt;</id><updated>2026-08-23T17:38:18.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Content Transfer coming to Optimizely SaaS CMS</title><link href="https://world.optimizely.com/blogs/matt-pallatt-is-not-a-developer/dates/2026/8/content-transfer-coming-to-saas/" /><id>&lt;p class=&quot;MsoNormal&quot;&gt;&lt;span style=&quot;mso-ansi-language: EN-US;&quot;&gt;Back in June I released &lt;span style=&quot;color: rgb(224, 62, 45);&quot;&gt;&lt;a style=&quot;color: rgb(224, 62, 45);&quot; href=&quot;/link/9ad65182172c47689f5e21a8f01f781b.aspx&quot;&gt;Content Transfer&lt;/a&gt;&lt;/span&gt; on the Optimizely nuget feed &amp;ndash; today I&amp;rsquo;m &lt;span style=&quot;color: rgb(224, 62, 45);&quot;&gt;&lt;a style=&quot;color: rgb(224, 62, 45);&quot; href=&quot;https://nuget.optimizely.com/packages/mp.dxpcontenttransfer/&quot;&gt;releasing version 1.0&lt;/a&gt;&lt;/span&gt;, because we&amp;rsquo;ve had it running out in the wild for a while now and nobody has shouted that it&amp;rsquo;s done anything other than work as expected and hasn&amp;rsquo;t destroyed any content. So, I&amp;rsquo;m a happy boy.&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;span style=&quot;mso-ansi-language: EN-US;&quot;&gt;Related to this news, I&amp;rsquo;ve spent some of the last month porting a lot of that work over to Optimizely SaaS, working on the new OCP UI extension framework that&amp;rsquo;s being released later this year.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;span style=&quot;mso-ansi-language: EN-US;&quot;&gt;Content Transfer SaaS&amp;nbsp;allows you to move content that you&#39;ve authored in one enviroment easily to one of your other environments, without the need of Admin&#39;s Export and Import tools. It works in almost exactly the same way as the PaaS tool &amp;ndash; albeit with a few extra bells and whistles for the SaaS experience &amp;ndash; in that you set your environments up (in the Connect Platform, as opposed to CMS settings), then wherever you are viewing content (experiences, pages, blocks, images...) in the CMS you have the option to send it to one of your other configured environments. Easy peasy.&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;span style=&quot;mso-ansi-language: EN-US;&quot;&gt;You&amp;rsquo;ll hopefully have roughly the same tree across environments and so a like for like transfer will be possible, but if you don&amp;rsquo;t then Content Transfer will do its best to locate where the content is supposed to land, and if it gets it wrong (or right), it offers you the option to re-position your content into a completely different place if needed.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;span style=&quot;mso-ansi-language: EN-US; mso-no-proof: yes;&quot;&gt;&lt;!-- [if gte vml 1]&gt;&lt;v:shapetype id=&quot;_x0000_t75&quot; coordsize=&quot;21600,21600&quot;
 o:spt=&quot;75&quot; o:preferrelative=&quot;t&quot; path=&quot;m@4@5l@4@11@9@11@9@5xe&quot; filled=&quot;f&quot;
 stroked=&quot;f&quot;&gt;
 &lt;v:stroke joinstyle=&quot;miter&quot;/&gt;
 &lt;v:formulas&gt;
  &lt;v:f eqn=&quot;if lineDrawn pixelLineWidth 0&quot;/&gt;
  &lt;v:f eqn=&quot;sum @0 1 0&quot;/&gt;
  &lt;v:f eqn=&quot;sum 0 0 @1&quot;/&gt;
  &lt;v:f eqn=&quot;prod @2 1 2&quot;/&gt;
  &lt;v:f eqn=&quot;prod @3 21600 pixelWidth&quot;/&gt;
  &lt;v:f eqn=&quot;prod @3 21600 pixelHeight&quot;/&gt;
  &lt;v:f eqn=&quot;sum @0 0 1&quot;/&gt;
  &lt;v:f eqn=&quot;prod @6 1 2&quot;/&gt;
  &lt;v:f eqn=&quot;prod @7 21600 pixelWidth&quot;/&gt;
  &lt;v:f eqn=&quot;sum @8 21600 0&quot;/&gt;
  &lt;v:f eqn=&quot;prod @7 21600 pixelHeight&quot;/&gt;
  &lt;v:f eqn=&quot;sum @10 21600 0&quot;/&gt;
 &lt;/v:formulas&gt;
 &lt;v:path o:extrusionok=&quot;f&quot; gradientshapeok=&quot;t&quot; o:connecttype=&quot;rect&quot;/&gt;
 &lt;o:lock v:ext=&quot;edit&quot; aspectratio=&quot;t&quot;/&gt;
&lt;/v:shapetype&gt;&lt;v:shape id=&quot;Picture_x0020_1&quot; o:spid=&quot;_x0000_i1025&quot; type=&quot;#_x0000_t75&quot;
 style=&#39;width:145.8pt;height:237pt;visibility:visible;mso-wrap-style:square&#39;&gt;
 &lt;v:imagedata src=&quot;file:///C:/Users/MATTPA~1/AppData/Local/Temp/msohtmlclip1/01/clip_image001.png&quot;
  o:title=&quot;&quot;/&gt;
&lt;/v:shape&gt;&lt;![endif]--&gt;&lt;!-- [if !vml]--&gt;&lt;img src=&quot;/link/ef28d5111496493c8e319ca932aaceaa.aspx&quot; width=&quot;194&quot; height=&quot;316&quot; border=&quot;0&quot; /&gt;&lt;!--[endif]--&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;span style=&quot;mso-ansi-language: EN-US;&quot;&gt;As for bells and whistles, you can now transfer locales, content types, property groups and display templates as well as content!&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;span style=&quot;mso-ansi-language: EN-US;&quot;&gt;All in all, I&#39;ve been quite impressed with the SaaS OCP UI extensions, and it has been remarkable easy to get my PaaS extension over into SaaS, and looking forwards to it being available on the OCP UI ecosystem - so n&lt;/span&gt;&lt;span style=&quot;mso-ansi-language: EN-US;&quot;&gt;ow we must wait patiently for Optimizely to release SaaS OCP UI extensions to the public (presumably around Opticon?) and hope that their pricing model for custom extensions isn&#39;t too eye watering.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;</id><updated>2026-08-23T15:04:53.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Improving Optimizely Product Information in LLM Tools</title><link href="https://nicholas.sideras.net/blog/2026/08/21/improving-optimizely-information-in-llm-tools/" /><id>Tools like Claude Code can be great for working with Optimizely CMS, but when they index outdated or incomplete information about developing a website, it can lead to wasted time and money. Building a custom documentation skill can help LLM tools return correct answers about Optimizely products without constant course correction from the developer.</id><updated>2026-08-21T18:33:22.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>How to Get a Bacpac Out of DXP (And Never Do It By Hand Again)</title><link href="https://world.optimizely.com/blogs/kennyg/dates/2026/8/how-to-get-a-bacpac-out-of-dxp-and-never-do-it-by-hand-again/" /><id>&lt;p&gt;Every Optimizely DXP project eventually runs into the same task: getting production data onto a local machine so you can actually troubleshoot something. Real content. Real catalog data. Real edge cases that no seed script ever thinks to include.&lt;/p&gt;
&lt;p&gt;At first, most teams handle it the same way. Someone logs into the portal, downloads a couple of multi-gigabyte files, runs &lt;strong&gt;SqlPackage &lt;/strong&gt;from memory, and hopes nothing goes sideways.&lt;/p&gt;
&lt;p&gt;After doing that enough times myself, I automated the process by teaching an AI coding agent the entire workflow and packaging it as a Claude Code skill. This post walks through where bacpacs actually come from in DXP, how we retrieve them, and what the skill does once it has them.&lt;/p&gt;
&lt;h2&gt;Why bother with real data locally at all?&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;The case for it&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Seed data lies to you.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;It&#39;s clean, it&#39;s small, and it never contains the malformed catalog entry or orphaned content reference that&#39;s sitting in production right now waiting to expose a problem you haven&#39;t seen yet. Restoring &lt;strong&gt;epicms &lt;/strong&gt;and &lt;strong&gt;epicommerce &lt;/strong&gt;from an actual DXP export gives you the real shape of the data: actual row counts, weird nulls, years-old content nobody has touched, and all the other surprises that come with a live system.&lt;/p&gt;
&lt;p&gt;The only downside is that the process is tedious enough that most teams don&#39;t do it nearly as often as they should. That&#39;s the part worth automating.&lt;/p&gt;
&lt;p&gt;This also turned out to be a great use case for an AI coding agent because the process is mostly orchestration. None of the individual steps are difficult, but there are enough of them that it&#39;s easy to miss one when you&#39;re doing it manually.&lt;/p&gt;
&lt;h2&gt;Where do bacpacs actually come from on DXP?&lt;/h2&gt;
&lt;h3&gt;1. The portal, by hand&lt;/h3&gt;
&lt;p&gt;DXP&#39;s PaaS portal has a Troubleshoot page for each project where you can request and download a database export directly. This is where most teams start: log in, select an environment, wait for the export, and download it.&lt;/p&gt;
&lt;p&gt;It works. It&#39;s also the slowest option because every step requires manual interaction.&lt;/p&gt;
&lt;h3&gt;2. The morning export, automated&lt;/h3&gt;
&lt;p&gt;On our project, a separate Azure DevOps release pipeline already generates fresh &lt;strong&gt;epicms&lt;/strong&gt; and &lt;strong&gt;epicommerce&lt;/strong&gt; bacpacs every morning and writes them into a DXP blob storage container.&lt;/p&gt;
&lt;p&gt;That container has a &lt;strong&gt;container-scoped SAS token&lt;/strong&gt; stored in the release definition variables with read and list permissions. As long as the token is refreshed periodically, grabbing the latest backups is as simple as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;./tools/get-dxp-backups.ps1
# -OutDir &amp;lt;dir&amp;gt; optional; defaults to Downloads&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No Entra ID login. No portal navigation. No API credentials.&lt;/p&gt;
&lt;p&gt;The script lists the container, finds the newest &lt;strong&gt;epicms*&lt;/strong&gt;&amp;nbsp;and &lt;strong&gt;epicommerce*&lt;/strong&gt; blobs, and downloads both.&lt;/p&gt;
&lt;p&gt;If your project already has a similar nightly export pipeline, it&#39;s worth looking through the release variables. You may already have everything you need.&lt;/p&gt;
&lt;h3&gt;3. A fresh export, on demand&lt;/h3&gt;
&lt;p&gt;Sometimes the overnight export isn&#39;t fresh enough. Maybe you&#39;re trying to verify whether today&#39;s deployment already ran, or you need data that changed recently.&lt;/p&gt;
&lt;p&gt;In those cases, the EpiCloud PowerShell module can trigger a brand-new export directly from a DXP environment:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;Import-Module EpiCloud

$ctx = @{
    ClientKey = $key
    ClientSecret = $secret
    ProjectId = $projectId
}

$export = Start-EpiDatabaseExport `
    @ctx `
    -Environment &#39;Production&#39; `
    -DatabaseName &#39;epicms&#39; `
    -RetentionHours 24

# Poll Get-EpiDatabaseExport until status is &#39;Succeeded&#39;,
# then download $status.downloadLink&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This approach requires DXP API credentials created under &lt;strong&gt;Project &amp;rarr; Settings &amp;rarr; API&lt;/strong&gt; in the DXP portal.&lt;/p&gt;
&lt;p&gt;It also takes longer. A production-sized export can take 10 to 30 minutes before a file is even available to download.&lt;/p&gt;
&lt;p&gt;I generally only use this option when the nightly export isn&#39;t fresh enough.&lt;/p&gt;
&lt;h2&gt;So what does the skill actually do?&lt;/h2&gt;
&lt;p&gt;Once it has a &lt;strong&gt;.bacpac&lt;/strong&gt; for each database, the skill follows the same workflow every time:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Download&lt;/strong&gt; the newest pair of bacpacs and display their filenames, sizes, and timestamps.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Display the plan&lt;/strong&gt; before making any changes, including which databases will be affected, which mode will be used, and what will be dropped.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Request confirmation&lt;/strong&gt; when the planned action is destructive.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Drop the target databases&lt;/strong&gt; when running in replacement mode.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Import both databases in parallel&lt;/strong&gt; using &lt;strong&gt;SqlPackage /Action:Import&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Verify the results&lt;/strong&gt; using index health checks and sanity row counts against known tables, including &lt;strong&gt;tblContent&lt;/strong&gt; for CMS and &lt;strong&gt;CatalogEntry&lt;/strong&gt; for Commerce.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Report freshness information&lt;/strong&gt; using the source bacpac timestamps so you know exactly how current the restored data is.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clean up&lt;/strong&gt; the multi-gigabyte bacpac files after the imports have been confirmed successful.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Running the imports in parallel usually means the total time is roughly equal to whichever database takes longer instead of the combined time of both imports.&lt;/p&gt;
&lt;h2&gt;What can I tell it to do differently?&lt;/h2&gt;
&lt;p&gt;The skill accepts plain-language instructions and maps them to four independent decisions.&lt;/p&gt;
&lt;h3&gt;Scope&lt;/h3&gt;
&lt;p&gt;Restore both databases by default, or restore only:&lt;/p&gt;
&lt;ul&gt;
&lt;li style=&quot;font-weight: bold;&quot;&gt;&lt;strong&gt;cms-only&lt;/strong&gt;&lt;/li&gt;
&lt;li style=&quot;font-weight: bold;&quot;&gt;&lt;strong&gt;commerce-only&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Mode&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Replace (default)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This drops the existing &lt;strong&gt;epicms &lt;/strong&gt;and &lt;strong&gt;epicommerce &lt;/strong&gt;databases and restores into those same database names. Nothing else in your local configuration has to change.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Additional&lt;/strong&gt;, also called &lt;em&gt;keep&lt;/em&gt; or &lt;em&gt;side-by-side&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This imports the bacpacs into new timestamped database names and leaves your existing databases untouched. It&#39;s useful when you&#39;re in the middle of something and don&#39;t want to disrupt whatever you&#39;re currently working on.&lt;/p&gt;
&lt;h3&gt;Source&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;blob (default)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This uses the automated morning export from blob storage.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;fresh-export&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This generates a new export directly from DXP, optionally from Production, Preproduction, Integration, or another environment.&lt;/p&gt;
&lt;h3&gt;Confirmation&lt;/h3&gt;
&lt;p&gt;Destructive operations always request confirmation unless you explicitly say something like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;q&gt;just do it&lt;/q&gt;&lt;/li&gt;
&lt;li&gt;&lt;q&gt;force&lt;/q&gt;&lt;/li&gt;
&lt;li&gt;&lt;q&gt;don&#39;t wait for confirmation&lt;/q&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So a request like:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Refresh my local databases.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;runs the full default workflow.&lt;/p&gt;
&lt;p&gt;While:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Restore commerce only from Preproduction and don&#39;t ask.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;does exactly that.&lt;/p&gt;
&lt;p&gt;Same skill. Same guardrails. Different scope.&lt;/p&gt;
&lt;h2&gt;How fast is this, really?&lt;/h2&gt;
&lt;p&gt;A production-sized import is still a heavy operation. Every index gets disabled before the bulk load and rebuilt afterward.&lt;/p&gt;
&lt;p&gt;The biggest performance improvements come from a few places.&lt;/p&gt;
&lt;h3&gt;Run both restores in parallel&lt;/h3&gt;
&lt;p&gt;This is already the default behavior. The CMS and Commerce imports run at the same time, so the total duration is based mostly on whichever import takes longer.&lt;/p&gt;
&lt;h3&gt;Tune SQL Server once&lt;/h3&gt;
&lt;p&gt;A few local SQL Server configuration changes can help every subsequent restore:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Configure the &lt;strong&gt;model &lt;/strong&gt;database with simple recovery and appropriately sized files so new databases don&#39;t immediately run into autogrow stalls.&lt;/li&gt;
&lt;li&gt;Pre-size &lt;strong&gt;tempdb &lt;/strong&gt;to avoid unnecessary growth during the index rebuild phase.&lt;/li&gt;
&lt;li&gt;Increase max degree of parallelism so index rebuild operations can use the available CPU resources.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Monitor progress&lt;/h3&gt;
&lt;p&gt;Instead of staring at a blank terminal window, the bundled progress watcher follows the import logs and displays progress by phase:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Download&lt;/li&gt;
&lt;li&gt;Data import&lt;/li&gt;
&lt;li&gt;Index rebuild&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That doesn&#39;t turn a multi-gigabyte restore into a five-minute operation.&lt;/p&gt;
&lt;p&gt;It does mean the twenty-something minutes it takes no longer require constant supervision.&lt;/p&gt;
&lt;h2&gt;Worth stealing for your own project?&lt;/h2&gt;
&lt;p&gt;None of this is particularly Optimizely-specific.&lt;/p&gt;
&lt;p&gt;Under the covers, it&#39;s a blob storage container, an Azure DevOps pipeline variable, and a PowerShell wrapper around &lt;strong&gt;SqlPackage&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;What makes it worthwhile on a DXP project is that almost every team already has this process. The problem is that it&#39;s often tribal knowledge. One person knows where the exports live. Another remembers the right &lt;strong&gt;SqlPackage &lt;/strong&gt;switches. Everybody else ends up asking the same questions every few months.&lt;/p&gt;
&lt;p&gt;Turning &lt;q&gt;How do I get fresh production data locally?&lt;/q&gt; into a reusable skill means that knowledge lives in the repository instead of in somebody&#39;s head. Anyone on the team can use it, and the process stays consistent every time.&lt;/p&gt;
&lt;p&gt;If your project already has a nightly export pipeline, go check the release variables before building anything from scratch. There&#39;s a decent chance you&#39;ve already got most of what you need.&lt;/p&gt;</id><updated>2026-08-20T20:59:49.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Error FK_tblContentSoftlink_tblPropertyDefinition in CMS</title><link href="https://world.optimizely.com/blogs/vinit-gavankar/dates/2026/8/error-fk_tblcontentsoftlink_tblpropertydefinition-in-cms/" /><id>&lt;p&gt;&lt;strong&gt;Example Error :&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&amp;nbsp;Microsoft.Data.SqlClient.SqlException (0x80131904): The DELETE statement conflicted with the REFERENCE constraint &quot;FK_tblContentSoftlink_tblPropertyDefinition&quot;. The conflict occurred in database &quot;XYZ-epicms&quot;, table &quot;dbo.tblContentSoftlink&quot;, column &#39;fkOwnerPropertyDefinitionID&#39;.&lt;/div&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Description&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;This is happening because you&amp;rsquo;re attempting to remove a property definition, and during model sync on initialization, it tries to delete that row from &lt;strong&gt;tblPropertyDefinition&lt;/strong&gt;. However, there are still rows in&amp;nbsp;&lt;strong&gt;tblContentSoftlink&lt;/strong&gt;&amp;nbsp;that reference this property definition.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;First, I will ask you to try &quot;Restart one instance at a time&quot; from the &quot;Troubleshoot&quot; tab of the&lt;strong&gt; PAAS Portal&lt;/strong&gt;&amp;nbsp;and check again.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;If the issue continues, this query will give you all property definitions that have rows in &lt;strong&gt;tblContentSoftlink&lt;/strong&gt;, along with their property definition type:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT *&lt;br /&gt;FROM tblPropertyDefinition pd&lt;br /&gt;INNER JOIN tblPropertyDefinitionType pdtype&lt;br /&gt;    ON pdtype.pkid = pd.fkPropertyDefinitionTypeID&lt;br /&gt;WHERE EXISTS (&lt;br /&gt;    SELECT 1&lt;br /&gt;    FROM tblContentSoftlink cs&lt;br /&gt;    WHERE cs.fkOwnerPropertyDefinitionID = pd.pkid&lt;br /&gt;);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once you know the property definition to remove, you can delete the dependent softlink rows first (replace&amp;nbsp;&lt;code&gt;X&lt;/code&gt;&amp;nbsp;with the&amp;nbsp;&lt;code&gt;pkid&lt;/code&gt;&amp;nbsp;of the property definition):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DELETE FROM tblContentSoftlink&lt;br /&gt;WHERE fkOwnerPropertyDefinitionID = X;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;br /&gt;After that, the modelsync should be able to remove the property definition.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;I&amp;rsquo;d recommend testing this&amp;nbsp;&lt;strong&gt;locally first, using a copy of the preproduction database,&lt;/strong&gt;&amp;nbsp;to ensure it behaves as expected.&lt;/p&gt;</id><updated>2026-08-20T11:58:26.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Get count of pages, blocks , Assets from the CMS.</title><link href="https://world.optimizely.com/blogs/vinit-gavankar/dates/2026/8/get-count-of-pages-blocks--assets-from-the-cms/" /><id>&lt;p&gt;The following queries retrieve details from the CMS database. Make sure to try these queries locally first.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 1 &amp;mdash; Discover your content type bases first:&lt;br /&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;```&lt;br /&gt;SELECT Base, COUNT(*) AS TypeCount&lt;br /&gt;FROM tblContentType&lt;br /&gt;GROUP BY Base&lt;br /&gt;ORDER BY TypeCount DESC;&lt;br /&gt;```&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 2 &amp;mdash; Count all content grouped by base type (single query):&lt;br /&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;```&lt;br /&gt;SELECT &lt;br /&gt;    ct.Base         AS ContentBase,&lt;br /&gt;    ct.Name         AS ContentTypeName,&lt;br /&gt;    COUNT(c.pkID)   AS ContentCount&lt;br /&gt;FROM tblContent c&lt;br /&gt;INNER JOIN tblContentType ct ON c.fkContentTypeID = ct.pkID&lt;br /&gt;GROUP BY ct.Base, ct.Name&lt;br /&gt;ORDER BY ct.Base, ContentCount DESC;&lt;br /&gt;```&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 3 &amp;mdash; Individual counts per category:&lt;br /&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;```&lt;br /&gt;-- Pages&lt;br /&gt;SELECT COUNT(*) AS PageCount&lt;br /&gt;FROM tblContent c&lt;br /&gt;INNER JOIN tblContentType ct ON c.fkContentTypeID = ct.pkID&lt;br /&gt;WHERE ct.Base = &#39;Page&#39;;&lt;br /&gt;&lt;br /&gt;-- Blocks&lt;br /&gt;SELECT COUNT(*) AS BlockCount&lt;br /&gt;FROM tblContent c&lt;br /&gt;INNER JOIN tblContentType ct ON c.fkContentTypeID = ct.pkID&lt;br /&gt;WHERE ct.Base = &#39;Block&#39;;&lt;br /&gt;&lt;br /&gt;-- Images (adjust base value based on Step 1 results)&lt;br /&gt;SELECT COUNT(*) AS ImageCount&lt;br /&gt;FROM tblContent c&lt;br /&gt;INNER JOIN tblContentType ct ON c.fkContentTypeID = ct.pkID&lt;br /&gt;WHERE ct.Base = &#39;Image&#39;;&lt;br /&gt;&lt;br /&gt;-- Documents / Files&lt;br /&gt;SELECT COUNT(*) AS DocumentCount&lt;br /&gt;FROM tblContent c&lt;br /&gt;INNER JOIN tblContentType ct ON c.fkContentTypeID = ct.pkID&lt;br /&gt;WHERE ct.Base IN (&#39;File&#39;, &#39;Video&#39;, &#39;Media&#39;);&lt;br /&gt;```&lt;/code&gt;&lt;/pre&gt;</id><updated>2026-08-20T11:52:59.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Optimizely CMP Login Failure After Enabling SSO – The Certificate Mismatch That Locked Everyone Out</title><link href="https://madhuanbalagan.com/?p=5050" /><id>&lt;p&gt;Recently, our team faced a production issue where users suddenly lost access to Optimizely CMP, Opti ID Admin Center, and the Optimizely Support Portal. The&amp;#46;&amp;#46;&amp;#46;&lt;/p&gt;
&lt;p&gt;The post &lt;a href=&quot;https://madhuanbalagan.com/optimizely-cmp-login-failure-after-enabling-sso-the-certificate-mismatch-that-locked-everyone-out&quot;&gt;Optimizely CMP Login Failure After Enabling SSO – The Certificate Mismatch That Locked Everyone Out&lt;/a&gt; appeared first on &lt;a href=&quot;https://madhuanbalagan.com&quot;&gt;Madhu Anbalagan&amp;#039;s Blog&lt;/a&gt;.&lt;/p&gt;
</id><updated>2026-08-19T01:45:00.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Upgrade Optimizely CMS 12 to .NET 10</title><link href="https://www.gulla.net/en/blog/upgrade-optimizely-cms-12-to-net10/" /><id>If you are still running CMS 12 on .NET 8, it&#39;s time to upgrade!</id><updated>2026-08-17T11:57:17.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>What Your Optimizely DXP Is Serving to AI Crawlers - and the Three Levers You Actually Have</title><link href="https://pino-labs.com/blog/optimizely-dxp-ai-crawlers-three-levers/" /><id>Unblocked bot traffic on Optimizely DXP is a billable page view, and the Cloudflare zone in front of your site is not yours to configure. Three levers remain: robots.txt, Optimizely Support, and bringing your own Cloudflare zone (Orange-to-Orange) for AI Crawl Control. Plus the trap in Cloudflare&#39;s July 2026 change, where blocking Training also blocks Googlebot, Applebot and BingBot.</id><updated>2026-08-17T00:00:00.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Migrating from AEM to Optimizely SaaS CMS: Solving the Architectural Hurdles</title><link href="https://world.optimizely.com/blogs/vipin-banka--learnings--insights/dates/2026/8/migrating-from-aem-to-optimizely-saas-cms-solving-the-architectural-hurdles/" /><id>&lt;p&gt;Migrating a digital footprint from Adobe Experience Manager (AEM) to Optimizely SaaS CMS is a major milestone for any enterprise modernizing its digital experience platform. When you are dealing with years of accumulated legacy content spanning a large number of pages, it is tempting to treat this purely as a volume problem.&lt;/p&gt;
&lt;p&gt;In reality, bulk loading data is the easy part. The real challenge lies in the architectural differences between how AEM and Optimizely SaaS CMS represent content structure, localization, and identity.&lt;/p&gt;
&lt;p&gt;In this article, we will bypass the generic advice and focus on the high-stakes architectural hurdles you will encounter during an AEM-to-Optimizely migration &amp;mdash; and how to solve them programmatically using the Optimizely SaaS CMS REST API.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;The Sanity Checks: Start Simple&lt;/h2&gt;
&lt;p&gt;Before writing migration scripts or converting templates, always begin with a few basic sanity checks on your source JCR (Java Content Repository) data. Running these early prevents major pipeline failures later:&lt;/p&gt;
&lt;ol class=&quot;tight&quot;&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Clean up the ROT&lt;/strong&gt;: Run an analysis in AEM to find Redundant, Outdated, and Trivial content. Look for pages with zero traffic over the last 12 months, expired pages (where &lt;strong&gt;offTime&lt;/strong&gt; is in the past), or duplicate assets. Retiring this content before migrating saves significant engineering, QA, and content-review effort.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Verify target locales&lt;/strong&gt;: Make sure all languages used in your AEM tree are explicitly registered in your Optimizely SaaS CMS instance before pushing content. Attempting to load localized content into an unregistered locale will cause API errors.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Once your basic environment is ready, you can tackle the primary architectural shifts.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Architectural Hurdle 1: Parallel Trees vs. The Single-Node Localization Model&lt;/h2&gt;
&lt;p&gt;One of the deepest conceptual mismatches between AEM and Optimizely is how they handle multi-language websites.&lt;/p&gt;
&lt;h3&gt;The Problem&lt;/h3&gt;
&lt;p&gt;AEM organizes languages using parallel folder trees. Under &lt;strong&gt;/content/mysite&lt;/strong&gt;, you might have &lt;strong&gt;/en&lt;/strong&gt; for English and &lt;strong&gt;/fr&lt;/strong&gt; for French. These are completely separate nodes in the JCR, linked together externally via Multi-Site Manager (MSM) or Language Copy. If a French page exists under &lt;strong&gt;/content/mysite/fr/about&lt;/strong&gt; but has no English equivalent under &lt;strong&gt;/content/mysite/en/about&lt;/strong&gt;, AEM is perfectly happy.&lt;/p&gt;
&lt;p&gt;Optimizely SaaS CMS uses a single-node localization model. A content item is represented by a single unique key. That single node has a &lt;strong&gt;primaryLocale&lt;/strong&gt; &amp;mdash; the master language &amp;mdash; established permanently at creation. Any additional languages (such as French) exist as locale branches hanging off that same single node.&lt;/p&gt;
&lt;p&gt;If your client&#39;s stated intent is that English must be the master language, but your discovery analysis reveals that a meaningful portion of regional pages or folders have no English equivalent, you have a structural conflict. You cannot create a French translation branch in Optimizely if the master English node does not exist.&lt;/p&gt;
&lt;h3&gt;The Solution: The Unpublished English Draft Shell&lt;/h3&gt;
&lt;p&gt;To keep a consistent English-master model without publishing placeholder or machine-translated English content that regional teams never requested, use the &lt;strong&gt;Unpublished English Draft Shell&lt;/strong&gt; pattern.&lt;/p&gt;
&lt;p&gt;When your migration pipeline encounters a French page with no English sibling, the loader should:&lt;/p&gt;
&lt;ol class=&quot;tight&quot;&gt;
&lt;li&gt;
&lt;p&gt;Create the content item with English (&lt;strong&gt;en&lt;/strong&gt;) set as the master locale, carrying only non-localized properties and a fallback display name. Keep this version as an unpublished draft.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Create the French version (&lt;strong&gt;fr&lt;/strong&gt;) as a translation branch on that same key and publish it.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Here is the two-step API flow:&lt;/p&gt;
&lt;h4&gt;Step 1: Create the master item in English (Unpublished Draft)&lt;/h4&gt;
&lt;pre class=&quot;code-block&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;POST https://api.cms.optimizely.com/v1/content
Content-Type: application/json

{
  &quot;key&quot;: &quot;5f2b9c1e-4a7d-4b8e-9c0d-1a2b3c4d5e6f&quot;,
  &quot;contentType&quot;: &quot;standardPage&quot;,
  &quot;container&quot;: &quot;parent-container-guid&quot;,
  &quot;initialVersion&quot;: {
    &quot;displayName&quot;: &quot;About Us (FR Source &amp;mdash; Master Shell)&quot;,
    &quot;locale&quot;: &quot;en&quot;,
    &quot;properties&quot;: {
      &quot;heading&quot;: { &quot;value&quot;: &quot;Placeholder English Title&quot; }
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Step 2: Post the real French version to the same key&lt;/h4&gt;
&lt;pre class=&quot;code-block&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;POST https://api.cms.optimizely.com/v1/content/5f2b9c1e-4a7d-4b8e-9c0d-1a2b3c4d5e6f/versions
Content-Type: application/json

{
  &quot;displayName&quot;: &quot;&amp;Agrave; propos&quot;,
  &quot;locale&quot;: &quot;fr&quot;,
  &quot;properties&quot;: {
    &quot;heading&quot;: { &quot;value&quot;: &quot;&amp;Agrave; propos de nous&quot; }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After verifying the version was created, publish it:&lt;/p&gt;
&lt;pre class=&quot;code-block&quot;&gt;&lt;code class=&quot;language-http&quot;&gt;POST https://api.cms.optimizely.com/v1/content/5f2b9c1e-4a7d-4b8e-9c0d-1a2b3c4d5e6f/versions/1:publish&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This ensures your public-facing site remains clean, while maintaining a consistent and governable data model behind the scenes. Editors will see an empty English draft in the CMS &amp;mdash; make sure your editorial team is briefed on this pattern so it does not cause confusion post-migration.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Architectural Hurdle 2: Making the Migration Re-runnable with Deterministic Keys&lt;/h2&gt;
&lt;p&gt;Migrations are rarely a one-and-done big bang. You will run dry runs, test waves, and delta syncs.&lt;/p&gt;
&lt;h3&gt;The Problem&lt;/h3&gt;
&lt;p&gt;If your migration loader relies on the target CMS to auto-generate random IDs upon creation, you lose idempotency immediately. If a script fails halfway through, running it again will result in duplicate pages, orphaned assets, and broken references. Maintaining a large lookup table mapping old AEM paths to new random GUIDs is fragile and prone to synchronization errors across environments.&lt;/p&gt;
&lt;h3&gt;The Solution: Deterministic Key Generation (UUID v5)&lt;/h3&gt;
&lt;p&gt;Optimizely SaaS CMS allows you to supply your own &lt;strong&gt;key&lt;/strong&gt; (a valid UUID/GUID format) inside the request body when creating content.&lt;/p&gt;
&lt;p&gt;You can exploit this by generating a deterministic UUID v5. UUID v5 uses a SHA-1 hash combining a fixed namespace UUID with a unique input string. In your migration pipeline, use AEM&#39;s highly stable &lt;strong&gt;jcr:uuid&lt;/strong&gt; as the input name:&lt;/p&gt;
&lt;pre class=&quot;code-block&quot;&gt;&lt;code&gt;optimizely_key = uuidv5(MIGRATION_NAMESPACE_UUID, aem_jcr_uuid)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Because &lt;strong&gt;jcr:uuid&lt;/strong&gt; is unique and survives page moves or renames inside AEM, your migration pipeline will generate the exact same Optimizely key every single time it processes a given item &amp;mdash; on any machine, in any environment, with no coordination required.&lt;/p&gt;
&lt;p&gt;This gives you native idempotency:&lt;/p&gt;
&lt;ul class=&quot;tight&quot;&gt;
&lt;li&gt;
&lt;p&gt;If the item already exists in Optimizely, your script performs a &lt;strong&gt;PATCH&lt;/strong&gt; update.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;If it does not exist, it performs a &lt;strong&gt;POST&lt;/strong&gt; to create it.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;If you need to roll back a wave, your script knows exactly which keys to target for hard deletion.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Architectural Hurdle 3: Content Type Immutability and the &quot;Reparent &amp;amp; Swap&quot; Pattern&lt;/h2&gt;
&lt;p&gt;During discovery, you classify AEM pages into Optimizely content types &amp;mdash; some remain Pages, others (such as Experience Fragments or non-routed pages) become Shared Blocks.&lt;/p&gt;
&lt;h3&gt;The Problem&lt;/h3&gt;
&lt;p&gt;What happens if you load an item as a Page, and the editorial team identifies during UAT that it should have been a Shared Block?&lt;/p&gt;
&lt;p&gt;In Optimizely, an item&#39;s &lt;strong&gt;contentType&lt;/strong&gt; is immutable once created. You cannot change a Page to a Block because they inherit from different base classes and are stored differently by the platform &amp;mdash; Pages have URL routing and hierarchy placement, Blocks do not. There is no API endpoint that converts one to the other.&lt;/p&gt;
&lt;p&gt;Furthermore, if that incorrectly typed item is a parent container with a large number of correct children sitting beneath it, a hard delete of the parent will cascade and destroy all those child nodes.&lt;/p&gt;
&lt;h3&gt;The Solution: The &quot;Reparent &amp;amp; Swap&quot; Pattern&lt;/h3&gt;
&lt;p&gt;Rather than destroying and rebuilding the entire child hierarchy, execute an architectural swap via the REST API:&lt;/p&gt;
&lt;ol class=&quot;tight&quot;&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Create a temporary holding folder&lt;/strong&gt; elsewhere in your content tree.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Move all children&lt;/strong&gt; by updating each child&#39;s &lt;strong&gt;container&lt;/strong&gt; property via a &lt;strong&gt;PATCH&lt;/strong&gt; request to point to the temporary holder.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Hard-delete the now-empty parent&lt;/strong&gt;, passing the special &lt;strong&gt;cms-permanent-delete&lt;/strong&gt; header to release the key immediately rather than moving it to the Trash:&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;code-block&quot;&gt;&lt;code class=&quot;language-http&quot;&gt;DELETE https://api.cms.optimizely.com/v1/content/bad-parent-guid
cms-permanent-delete: true&lt;/code&gt;&lt;/pre&gt;
&lt;ol class=&quot;tight&quot;&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Recreate the parent&lt;/strong&gt; using the exact same GUID key, this time supplying the correct &lt;strong&gt;contentType&lt;/strong&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Move the children back&lt;/strong&gt; by patching each child&#39;s &lt;strong&gt;container&lt;/strong&gt; to point to the original GUID.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Hard-delete the temporary folder&lt;/strong&gt; to leave no trace.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This pattern preserves every child&#39;s GUID, version history, regional locale branches, and any inbound content references from other pages &amp;mdash; because none of that data was touched.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Architectural Hurdle 4: Applying the Swap Pattern to Language Versions&lt;/h2&gt;
&lt;p&gt;The Reparent &amp;amp; Swap concept extends naturally to localization issues, and this is where it becomes especially powerful during large migrations. There are two distinct language-version scenarios you are likely to encounter.&lt;/p&gt;
&lt;h3&gt;Scenario A: The &quot;Locale Swap&quot; &amp;mdash; Changing the Master Language of a GUID&lt;/h3&gt;
&lt;p&gt;The &lt;strong&gt;primaryLocale&lt;/strong&gt; of a content item is structurally immutable in Optimizely. If you created a page with English as the master, you cannot call an API to change it to French. However, if the business requires a specific GUID to have French as its master language, you can execute a Locale Swap using a temporary item &amp;mdash; preserving the original GUID so all inbound links remain intact.&lt;/p&gt;
&lt;h4&gt;The Process&lt;/h4&gt;
&lt;pre class=&quot;code-block&quot;&gt;&lt;code&gt;[Original: GUID_A]              [Temp: GUID_T]             [Restored: GUID_A]
  Master: EN                      Master: FR (copy)          Master: FR
  Branch: FR                      Branch: EN (copy)          Branch: EN
        │                               ▲                          ▲
        └── Read all properties ────────┘                          │
        └── Hard-delete GUID_A                                     │
        └── Recreate GUID_A with FR as master ─────────────────────┘
        └── Hard-delete GUID_T&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;The API Steps&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Step 1&lt;/strong&gt; &amp;mdash; Read all version properties for both &lt;strong&gt;en&lt;/strong&gt; and &lt;strong&gt;fr&lt;/strong&gt; from the original item and hold them in your script.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2&lt;/strong&gt; &amp;mdash; Create a temporary item with French as the &lt;strong&gt;primaryLocale&lt;/strong&gt;:&lt;/p&gt;
&lt;pre class=&quot;code-block&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;POST https://api.cms.optimizely.com/v1/content
{
  &quot;key&quot;: &quot;temp-guid&quot;,
  &quot;contentType&quot;: &quot;standardPage&quot;,
  &quot;container&quot;: &quot;temp-holding-folder-guid&quot;,
  &quot;initialVersion&quot;: {
    &quot;displayName&quot;: &quot;Temp Shell&quot;,
    &quot;locale&quot;: &quot;fr&quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 3&lt;/strong&gt; &amp;mdash; Hard-delete the original item to release GUID_A:&lt;/p&gt;
&lt;pre class=&quot;code-block&quot;&gt;&lt;code class=&quot;language-http&quot;&gt;DELETE https://api.cms.optimizely.com/v1/content/GUID_A
cms-permanent-delete: true&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 4&lt;/strong&gt; &amp;mdash; Recreate GUID_A with French as the master locale and all original French content restored.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 5&lt;/strong&gt; &amp;mdash; Create the English translation branch on GUID_A with the original English content.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 6&lt;/strong&gt; &amp;mdash; Publish according to the original publication state.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 7&lt;/strong&gt; &amp;mdash; Hard-delete the temporary item.&lt;/p&gt;
&lt;p&gt;The result: GUID_A now has French as its &lt;strong&gt;primaryLocale&lt;/strong&gt;, English as its translation branch, and every page in the system that referenced GUID_A is completely unaware that anything changed.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Scenario B: The &quot;Branch Swap&quot; &amp;mdash; Moving a Translation to the Correct Parent&lt;/h3&gt;
&lt;p&gt;In large migrations, AEM&#39;s parallel language trees can be misaligned during the bulk load &amp;mdash; the French version of Page X ends up attached to Page Y as a locale branch instead. Since locale branches are bound to a specific GUID, you cannot directly reparent them. You must use a Read-Write-Delete approach.&lt;/p&gt;
&lt;h4&gt;The API Steps&lt;/h4&gt;
&lt;p&gt;Assume French (&lt;strong&gt;fr&lt;/strong&gt;) is incorrectly sitting on &lt;strong&gt;Page_A&lt;/strong&gt; but belongs on &lt;strong&gt;Page_B&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 1&lt;/strong&gt; &amp;mdash; Read the French version&#39;s full property data from Page_A:&lt;/p&gt;
&lt;pre class=&quot;code-block&quot;&gt;&lt;code class=&quot;language-http&quot;&gt;GET https://api.cms.optimizely.com/v1/content/Page_A/versions?locale=fr&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 2&lt;/strong&gt; &amp;mdash; Create a new French version branch on Page_B using the properties retrieved in Step 1:&lt;/p&gt;
&lt;pre class=&quot;code-block&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;POST https://api.cms.optimizely.com/v1/content/Page_B/versions
Content-Type: application/json

{
  &quot;displayName&quot;: &quot;French Title&quot;,
  &quot;locale&quot;: &quot;fr&quot;,
  &quot;properties&quot;: { ... }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 3&lt;/strong&gt; &amp;mdash; Publish the new French branch on Page_B if the original was published:&lt;/p&gt;
&lt;pre class=&quot;code-block&quot;&gt;&lt;code class=&quot;language-http&quot;&gt;POST https://api.cms.optimizely.com/v1/content/Page_B/versions/{newVersionId}:publish&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 4&lt;/strong&gt; &amp;mdash; Delete the incorrect French locale branch from Page_A. Locale branch deletion is immediate and irreversible &amp;mdash; it does not go to Trash &amp;mdash; which is exactly the behavior you need here:&lt;/p&gt;
&lt;pre class=&quot;code-block&quot;&gt;&lt;code class=&quot;language-http&quot;&gt;DELETE https://api.cms.optimizely.com/v1/content/Page_A/locales/fr&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Important note on locale deletion:&lt;/strong&gt; Because deleting a locale branch is permanent with no recovery path, always verify that Step 2 and Step 3 have succeeded and the content is correctly visible in the target before executing Step 4. Build a mandatory verification check into your migration script before it fires the delete.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;Building These Patterns into Your Migration Toolbox&lt;/h2&gt;
&lt;p&gt;All four patterns described above share a common foundation:&lt;/p&gt;
&lt;ul class=&quot;tight&quot;&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;A Migration State Database&lt;/strong&gt; that tracks every AEM node&#39;s original &lt;strong&gt;jcr:uuid&lt;/strong&gt;, its deterministic Optimizely key, its content archetype, and its load status.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;A purge/rollback mode&lt;/strong&gt; in your loader that can fire &lt;strong&gt;cms-permanent-delete&lt;/strong&gt; against a wave&#39;s key set &amp;mdash; gated behind an explicit flag and environment check so it can never be accidentally pointed at production.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Structured logging with a run ID&lt;/strong&gt; on every API call, so any partial failure can be diagnosed and resumed rather than restarted.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When these utility scripts exist from day one of your migration build, structural corrections that would otherwise cost days of manual recovery become a 20-minute scripted operation.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;The architectural gap between AEM and Optimizely SaaS CMS is real, but it is entirely solvable with deliberate engineering. By designing your localization strategy around a consistent master language model, generating deterministic UUIDs to make your pipeline re-runnable, treating content type immutability as a design constraint rather than a blocker, and having the Locale Swap and Branch Swap patterns ready as operational utilities, you give your migration the resilience it needs to handle edge cases without unplanned rework.&lt;/p&gt;
&lt;p&gt;Have you encountered similar structural challenges during platform migrations? Drop your experience in the comments below &amp;mdash; the community would love to hear how you approached them.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em class=&quot;_1gm2eq5 _4n13zc&quot;&gt;If you enjoyed this technical breakdown, you&amp;rsquo;ll like my monthly newsletter, &lt;strong class=&quot;_1gm2eq5 lab8bo _4n13zc&quot;&gt;&lt;a class=&quot;_4n13zc _1l4j4nae _1q9ygi7 _4ivg92 _12zsz04 _4n13zc&quot; href=&quot;https://www.linkedin.com/newsletters/pragmatic-judgement-7487084233719549953/&quot;&gt;Pragmatic Judgement&lt;/a&gt;&lt;/strong&gt;. I write about where technical decisions meet real operational constraints for tech and delivery leaders.&lt;/em&gt;&lt;/p&gt;</id><updated>2026-08-16T04:25:04.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>You’re not headless. You’re hybrid — and that’s the CMS 12 default.</title><link href="https://world.optimizely.com/blogs/hanskumar-tripathi/dates/2026/8/youre-not-headless.-youre-hybrid--and-thats-the-cms-12-default" /><id>&lt;p&gt;&lt;em&gt;For CMS 12 / Commerce teams with a React (or similar) storefront who keep hearing &amp;ldquo;just go headless&amp;rdquo; or &amp;ldquo;just move to Graph.&amp;rdquo; This is &lt;strong&gt;Part 1&lt;/strong&gt; of &lt;strong&gt;Find &amp;rarr; Graph: the hybrid storefront&lt;/strong&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Optimizely CMS 12 sites that look &amp;ldquo;headless&amp;rdquo; in the browser are usually hybrid.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Editors still work in the CMS. Catalog still lives in Commerce. Preview and on-page edit still need a server-rendered page. React (or similar) is a storefront shell, not the system of record.&lt;/p&gt;
&lt;p&gt;If you skip that, CMS 13 + Optimizely Graph feels like a frontend rewrite. It isn&amp;rsquo;t. Graph replaces Search &amp;amp; Navigation (Find) as the query/index layer.&lt;/p&gt;
&lt;p&gt;It does &lt;strong&gt;not&lt;/strong&gt; replace:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Content types and &lt;code&gt;IContentRepository&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Commerce catalog, pricing, and cart&lt;/li&gt;
&lt;li&gt;Edit mode and preview&lt;/li&gt;
&lt;li&gt;Your React / SSR host&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Three layers people mix up&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;CMS 12 today&lt;/th&gt;
&lt;th&gt;After Graph / CMS 13&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Authoring&lt;/td&gt;
&lt;td&gt;CMS UI, content types&lt;/td&gt;
&lt;td&gt;Still CMS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storefront&lt;/td&gt;
&lt;td&gt;Razor + React / Content Delivery API&lt;/td&gt;
&lt;td&gt;Same hybrid, or more CDA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Search and listings&lt;/td&gt;
&lt;td&gt;Find (&lt;code&gt;IClient&lt;/code&gt;, facets, conventions)&lt;/td&gt;
&lt;td&gt;Optimizely Graph&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Content Delivery API is &lt;strong&gt;content over HTTP&lt;/strong&gt;. Find/Graph is &lt;strong&gt;query, facet, rank, and index&lt;/strong&gt;. Using CDA as your product-listing search is how teams paint themselves into a corner.&lt;/p&gt;
&lt;h2&gt;A hybrid shape that survives Graph&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;CMS/Commerce owns content and catalog.&lt;/li&gt;
&lt;li&gt;A thin .NET host does auth, cart, preview, and first paint (SSR).&lt;/li&gt;
&lt;li&gt;React hydrates listing / PDP / checkout islands.&lt;/li&gt;
&lt;li&gt;Listings talk to a query module (&lt;code&gt;IProductSearch&lt;/code&gt;, &lt;code&gt;IPageSearch&lt;/code&gt;) &amp;mdash; not &lt;code&gt;IClient&lt;/code&gt; in 40 controllers.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That last point &lt;em&gt;is&lt;/em&gt; the migration. Find behind a port &amp;rarr; Graph is an adapter. Find in every controller &amp;rarr; Graph is a rewrite.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface IProductSearch
{
    Task&amp;lt;ProductSearchResult&amp;gt; SearchAsync(
        ProductSearchRequest request,
        CancellationToken cancellationToken);
}

// CMS 12: Find adapter implements this.
// CMS 13: Graph adapter implements this.
// Controllers and React never take a dependency on IClient.
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Do this before&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Draw the line: CDA vs Find vs &lt;code&gt;IContentLoader&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Put Find behind interfaces (search, facets, paging).&lt;/li&gt;
&lt;li&gt;Don&amp;rsquo;t let React own search URLs the server doesn&amp;rsquo;t know.&lt;/li&gt;
&lt;li&gt;Measure three numbers: time from publish until the listing shows the new item in Find; listing p95; percent of searches with zero hits.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is &lt;strong&gt;Part 1&lt;/strong&gt; of &lt;strong&gt;Find &amp;rarr; Graph: the hybrid storefront&lt;/strong&gt;. Next: typed Find query builders, facets, and &lt;code&gt;GetContentResultAsync&lt;/code&gt; &amp;mdash; so a Graph adapter can sit beside them later.&lt;/p&gt;</id><updated>2026-08-13T14:38:03.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Stott Security for Optimizely SaaS CMS</title><link href="https://world.optimizely.com/blogs/mark-stott/dates/2026/8/stott-security-for-optimizely-saas-cms/" /><id>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.stott.pro/article/getting-started-with-stott-security&quot;&gt;Stott Security&lt;/a&gt;&amp;nbsp;has been helping developers and editors manage Content Security Policies and security headers on&amp;nbsp;&lt;strong&gt;Optimizely PaaS CMS&lt;/strong&gt;&amp;nbsp;for several years. Today I am pleased to share an early access release of&amp;nbsp;&lt;strong&gt;Stott Security for Optimizely SaaS CMS&lt;/strong&gt;: a complete rebuild designed for Optimizely&amp;rsquo;s composable platform, built on the&amp;nbsp;&lt;strong&gt;Optimizely Connect Platform (OCP)&lt;/strong&gt;&amp;nbsp;and rendered inside the CMS using&amp;nbsp;&lt;strong&gt;CMS UI Extensions&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;A Complete Rebuild&lt;/h2&gt;
&lt;p&gt;Optimizely SaaS CMS does not give you a web server to install a NuGet package into, so the add-on model that has served the PaaS version so well simply does not exist there. Instead, extensions are built as apps on the Optimizely Connect Platform and surface their interfaces inside the CMS using CMS UI Extensions. Stott Security for SaaS is exactly that: an OCP app that adds a full-page security console to your CMS and serves your compiled headers to your site&amp;rsquo;s front end from a public endpoint.&lt;/p&gt;
&lt;p&gt;Everything about this implementation lives in OCP, and the data belongs to your installation.&amp;nbsp;&lt;strong&gt;Entity Framework and SQL Server are gone&lt;/strong&gt;; configuration is now held in OCP&amp;rsquo;s key value storage. This is a much simpler data store, and it does mean some functional changes &amp;mdash; relational features of the PaaS version such as the audit history and per-page policies have not carried across. I have set out an honest comparison in the feature parity table below.&lt;/p&gt;
&lt;p&gt;What has carried across is the engine. The header compilation engine is a faithful TypeScript port of the C# engine that powers the PaaS add-on, cross-validated against the original so that both products produce the same output. That includes the automatic optimisation and splitting of oversized Content Security Policies across multiple headers so that your policy stays within browser and webserver limits.&lt;/p&gt;
&lt;p&gt;The rebuild was also an opportunity to add something the PaaS version has never had: a&amp;nbsp;&lt;strong&gt;draft and publish workflow&lt;/strong&gt;. Changes are saved as a draft, and nothing reaches your site until you press&amp;nbsp;&lt;strong&gt;Publish&lt;/strong&gt;. Publishing compiles your configuration once and stores the result, so the endpoint your front end calls is a single fast read with no compilation on the hot path. The console always shows you when the live configuration went live and whether unpublished changes exist.&lt;/p&gt;
&lt;h2&gt;What You Get Today&lt;/h2&gt;
&lt;p&gt;The console is split into four tabs:&amp;nbsp;&lt;strong&gt;Response headers&lt;/strong&gt;,&amp;nbsp;&lt;strong&gt;Content Security Policy&lt;/strong&gt;,&amp;nbsp;&lt;strong&gt;Preview&lt;/strong&gt;&amp;nbsp;and&amp;nbsp;&lt;strong&gt;Tools&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The&amp;nbsp;&lt;strong&gt;Response headers&lt;/strong&gt;&amp;nbsp;tab covers eight standard security headers &amp;mdash; including X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Strict-Transport-Security and the Cross-Origin family &amp;mdash; each with a plain-English description, a behaviour of&amp;nbsp;&lt;strong&gt;Add&lt;/strong&gt;,&amp;nbsp;&lt;strong&gt;Remove&lt;/strong&gt;&amp;nbsp;or&amp;nbsp;&lt;strong&gt;Disabled&lt;/strong&gt;, and a preview of exactly what will be emitted. You can also add your own custom headers.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.stott.pro/assets/stott-security-saas-response-headers-screen.png&quot; alt=&quot;The Response headers tab of Stott Security for Optimizely SaaS CMS, showing standard security headers with behaviour, value and preview options.&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The&amp;nbsp;&lt;strong&gt;Content Security Policy&lt;/strong&gt; tab uses the same source-first model as the PaaS version: you manage a list of sources &amp;mdash; domains, schemes, keywords, hashes and the &lt;strong&gt;&#39;nonce-random&#39;&lt;/strong&gt;&amp;nbsp;placeholder &amp;mdash; and grant each source the directives it needs. The Settings section lets you run the policy in report-only mode, upgrade insecure requests and send violation reports to an external collector, while the Sandbox section provides switches for the fifteen sandbox permissions.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.stott.pro/assets/stott-security-saas-csp-sources-screen.png&quot; alt=&quot;The Content Security Policy Sources tab of Stott Security for Optimizely SaaS CMS, showing sources and the directives granted to each.&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The&amp;nbsp;&lt;strong&gt;Preview&lt;/strong&gt;&amp;nbsp;tab shows your headers in three views:&amp;nbsp;&lt;strong&gt;Pending&lt;/strong&gt;&amp;nbsp;shows what publishing your current draft would produce,&amp;nbsp;&lt;strong&gt;Live&lt;/strong&gt;&amp;nbsp;shows what your site&amp;rsquo;s head is being served right now, and&amp;nbsp;&lt;strong&gt;Integration&lt;/strong&gt;&amp;nbsp;provides the endpoint for your installation together with a worked example of applying the headers. A diagnostics panel also sits above the console, warning you when a policy is approaching size limits, has been split, or has been dropped entirely.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.stott.pro/assets/stott-security-saas-preview-screen.png&quot; alt=&quot;The Preview tab of Stott Security for Optimizely SaaS CMS, showing the live headers currently being served to the site head.&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Finally, the&amp;nbsp;&lt;strong&gt;Tools&lt;/strong&gt;&amp;nbsp;tab provides import and export of the complete configuration as JSON. This matters more on SaaS than it ever did on PaaS: the configuration is held by the app installation, so uninstalling the app deletes it. Keep an exported copy somewhere outside of Optimizely.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.stott.pro/assets/stott-security-saas-tools-screen.png&quot; alt=&quot;The Tools tab of Stott Security for Optimizely SaaS CMS, showing the export of the complete configuration as JSON.&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Feature Parity&lt;/h2&gt;
&lt;p&gt;A complete rebuild on a much simpler data store means this is not a feature-for-feature copy of the PaaS add-on. Some features have not carried across, some are on the roadmap, and a couple of things are new to the SaaS product.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;PaaS&lt;/th&gt;
&lt;th&gt;SaaS&lt;/th&gt;
&lt;th&gt;SaaS Roadmap&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CSP sources &amp;amp; directives&lt;/td&gt;
&lt;td&gt;✔&lt;/td&gt;
&lt;td&gt;✔&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CSP Keyword, scheme, hash &amp;amp; nonce sources&lt;/td&gt;
&lt;td&gt;✔&lt;/td&gt;
&lt;td&gt;✔&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CSP Report-only mode &amp;amp; upgrade insecure requests&lt;/td&gt;
&lt;td&gt;✔&lt;/td&gt;
&lt;td&gt;✔&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CSP sandbox&lt;/td&gt;
&lt;td&gt;✔&lt;/td&gt;
&lt;td&gt;✔&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CSP Automatic policy splitting &amp;amp; optimisation&lt;/td&gt;
&lt;td&gt;✔&lt;/td&gt;
&lt;td&gt;✔&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;External violation reporting&lt;/td&gt;
&lt;td&gt;✔&lt;/td&gt;
&lt;td&gt;✔&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Internal violation reporting (Violations tab)&lt;/td&gt;
&lt;td&gt;✔&lt;/td&gt;
&lt;td&gt;✘&lt;/td&gt;
&lt;td&gt;No current plans&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remote CSP Allow List&lt;/td&gt;
&lt;td&gt;✔&lt;/td&gt;
&lt;td&gt;✘&lt;/td&gt;
&lt;td&gt;No current plans&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-page CSP sources&lt;/td&gt;
&lt;td&gt;✔&lt;/td&gt;
&lt;td&gt;✘&lt;/td&gt;
&lt;td&gt;No current plans&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Standard &amp;amp; custom response headers&lt;/td&gt;
&lt;td&gt;✔&lt;/td&gt;
&lt;td&gt;✔&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CORS settings&lt;/td&gt;
&lt;td&gt;✔&lt;/td&gt;
&lt;td&gt;✘&lt;/td&gt;
&lt;td&gt;No current plans&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Permissions Policy&lt;/td&gt;
&lt;td&gt;✔&lt;/td&gt;
&lt;td&gt;✘&lt;/td&gt;
&lt;td&gt;Planned&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security.txt files&lt;/td&gt;
&lt;td&gt;✔&lt;/td&gt;
&lt;td&gt;✘&lt;/td&gt;
&lt;td&gt;No current plans&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Global / Application / Host contexts&lt;/td&gt;
&lt;td&gt;✔&lt;/td&gt;
&lt;td&gt;Global only&lt;/td&gt;
&lt;td&gt;Planned&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Header preview&lt;/td&gt;
&lt;td&gt;✔&lt;/td&gt;
&lt;td&gt;✔&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Draft &amp;amp; publish workflow&lt;/td&gt;
&lt;td&gt;✘&lt;/td&gt;
&lt;td&gt;✔&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audit history&lt;/td&gt;
&lt;td&gt;✔&lt;/td&gt;
&lt;td&gt;✘&lt;/td&gt;
&lt;td&gt;No current plans&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Import &amp;amp; export&lt;/td&gt;
&lt;td&gt;✔&lt;/td&gt;
&lt;td&gt;✔&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Headless headers API&lt;/td&gt;
&lt;td&gt;✔&lt;/td&gt;
&lt;td&gt;✔&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Headless middleware for your head&lt;/td&gt;
&lt;td&gt;Build your own&lt;/td&gt;
&lt;td&gt;Build your own&lt;/td&gt;
&lt;td&gt;Planned package&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;The biggest omissions follow directly from the storage model: without relational data there is no audit trail, and violation reports are sent to an external collector rather than being stored and summarised inside the CMS. The draft and publish workflow, together with the export tooling, are the counterweights &amp;mdash; you can always see what is live and when it went live, and you can keep copies of your configuration outside the platform.&lt;/p&gt;
&lt;p&gt;On the roadmap, Application and Host level contexts are a firm commitment &amp;mdash; the storage model was designed for them from day one &amp;mdash; alongside Permissions Policy management and a distributable middleware package for your front end.&lt;/p&gt;
&lt;h2&gt;Plugging It Into Your Headless Solution&lt;/h2&gt;
&lt;p&gt;Every Optimizely SaaS CMS solution is a headless solution, so applying the headers is always the responsibility of your front end. Your head requests the compiled headers from the endpoint shown on the&amp;nbsp;&lt;strong&gt;Integration&lt;/strong&gt; view of the Preview tab and applies them to its responses.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;An example response looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-JSON&quot;&gt;{
    &quot;headers&quot;: [
        {
            &quot;key&quot;: &quot;Content-Security-Policy&quot;,
            &quot;value&quot;: &quot;default-src &#39;self&#39;;...&quot;,
            &quot;isRemoval&quot;: false,
            &quot;isReplacement&quot;: false
        },
        {
            &quot;key&quot;: &quot;X-Content-Type-Options&quot;,
            &quot;value&quot;: &quot;nosniff&quot;,
            &quot;isRemoval&quot;: false,
            &quot;isReplacement&quot;: true
        },
        {
            &quot;key&quot;: &quot;server&quot;,
            &quot;value&quot;: &quot;&quot;,
            &quot;isRemoval&quot;: true,
            &quot;isReplacement&quot;: false
        }
    ],
    &quot;publishedAt&quot;: &quot;2026-08-12T00:06:28.000Z&quot;,
    &quot;cacheSeconds&quot;: 300
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each entry in the headers collection uses the same structure as the PaaS Header Listing API, so a consumer only needs to implement three behaviours:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;When&amp;nbsp;&lt;strong&gt;isRemoval&lt;/strong&gt; is &lt;strong&gt;true&lt;/strong&gt;:&amp;nbsp;remove the header from your response.&lt;/li&gt;
&lt;li&gt;When&amp;nbsp;&lt;strong&gt;isReplacement&lt;/strong&gt; is &lt;strong&gt;true&lt;/strong&gt;:&amp;nbsp;set the header, replacing any existing value.&lt;/li&gt;
&lt;li&gt;Otherwise: append the header to your response. The Content Security Policy is served this way because a large policy may legitimately be split across multiple headers.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Your front end has two further responsibilities: replace the &lt;strong&gt;&#39;nonce-random&#39;&lt;/strong&gt;&amp;nbsp;placeholder with a fresh value on every request, and add the &lt;strong&gt;nonce=&quot;random&quot;&lt;/strong&gt; attribute to script and style elements.&lt;/p&gt;
&lt;p&gt;If you want to see what this looks like in practice,&amp;nbsp;&lt;a href=&quot;https://www.linkedin.com/in/minesh-shah-dev/&quot;&gt;Minesh&lt;/a&gt;&amp;nbsp;has written an excellent walkthrough of building exactly this middleware in Next.js for the PaaS version of Stott Security:&amp;nbsp;&lt;a href=&quot;/link/27bc4af218b148caac6aab1e14871b00.aspx&quot;&gt;Dynamic CSP Management for Headless and Hybrid Optimizely CMS with Next.js&lt;/a&gt;. Because the header structure is identical, the same middleware approach carries straight over to the SaaS product. The only difference is the envelope: the PaaS API returns a bare array while the SaaS endpoint wraps the array in an object, so a one-line check such as &lt;strong&gt;Array.isArray(body) ? body : body.headers&lt;/strong&gt; allows a single middleware implementation to serve both products.&lt;/p&gt;
&lt;h2&gt;Get Involved&lt;/h2&gt;
&lt;p&gt;Stott Security for Optimizely SaaS CMS is in early access, and this is exactly the right time to influence its direction. I would love to hear how you would plug this into your own SaaS headless solution: which framework does your head run on, would the planned middleware package be useful to you, and which of the missing features matter most to your builds?&lt;/p&gt;
&lt;p&gt;Like the PaaS add-on, Stott Security for SaaS is free and open source under the MIT licence. If you would like to try it against your own Optimizely SaaS CMS instance then&amp;nbsp;&lt;a href=&quot;https://github.com/GeekInTheNorth/Stott.Security.Optimizely.SaaS/discussions/1&quot;&gt;join the Discussion&lt;/a&gt;. If you want to help shape what comes next, please&amp;nbsp;&lt;a href=&quot;https://github.com/GeekInTheNorth/Stott.Security.Optimizely.SaaS&quot;&gt;raise an issue on the Stott Security for SaaS GitHub repository&lt;/a&gt; &amp;mdash; feature requests, integration questions and early access requests are all welcome there.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;I&#39;m a Platinum Optimizely MVP and the maintainer of two open source add-ons: Stott Security for &lt;a href=&quot;https://github.com/GeekInTheNorth/Stott.Security.Optimizely.SaaS&quot;&gt;SaaS CMS&lt;/a&gt; and &lt;a href=&quot;https://github.com/GeekInTheNorth/Stott.Security.Optimizely&quot;&gt;PaaS CMS 12 &amp;amp; 13&lt;/a&gt;, and &lt;a href=&quot;https://github.com/GeekInTheNorth/Stott.Optimizely.RobotsHandler&quot;&gt;Stott Robots Handler&lt;/a&gt; for PaaS CMS 12 &amp;amp; 13. All of my writing is collated at &lt;a href=&quot;https://www.stott.pro/&quot;&gt;https://www.stott.pro&lt;/a&gt;.&lt;/p&gt;</id><updated>2026-08-13T13:29:29.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Optimizely XML Resource Builder: A Visual Studio Extension for Language Resource Files</title><link href="https://www.adnanzameer.com/2026/08/optimizely-xml-resource-builder.html" /><id>&lt;!--excerpt: Optimizely XML Resource Builder is a Visual Studio extension that generates Optimizely language resource XML from content type definitions, helping teams keep editor labels, descriptions, and shared properties in sync with less manual work.--&gt;

Anyone who has worked on a larger Optimizely solution will know the feeling. You add or update a page type, block type, or shared base class,</id><updated>2026-08-13T09:34:42.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Optimizely SaaS CMS Management API: The Write Side of Headless</title><link href="https://kpbasics.com/?p=8413" /><id>Everything I&amp;#8217;ve written in this series so far has been about reading content — Optimizely Graph, webhooks, ISR, caching layers. But at some point you need to actually write content programmatically. Maybe you&amp;#8217;re migrating 500 blog posts from WordPress. Maybe you want a seed script that spins up demo content on every fresh CMS instance. [&amp;#8230;]</id><updated>2026-08-12T21:22:01.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Assistants, Instructions and Statistics</title><link href="https://optimizely.blog/2026/08/assistants-instructions-and-statistics/" /><id>We are introducing three new features for AI Assistant in Optimizely CMS. They make it easier to reuse AI expertise, work faster and understand how AI is being used across your website.
</id><updated>2026-08-11T15:46:49.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Optimizely Commerce 14 Now Supports .NET 10</title><link href="https://world.optimizely.com/blogs/bien-nguyen/dates/2026/8/optimizely-commerce-14-now-supports-.net-10" /><id>&lt;div&gt;
&lt;p&gt;In my previous post, &lt;a href=&quot;/link/d15848f947264cc59c3d86b9c7d55c95.aspx&quot;&gt;&lt;em&gt;Optimizely CMS 12 Now Fully Supports .NET 10&lt;/em&gt;&lt;/a&gt;, I noted one remaining limitation: &lt;strong&gt;Commerce 14 (Commerce Connect)&lt;/strong&gt; did not yet support .NET 10, and Commerce customers were advised to stay on .NET 8.&lt;/p&gt;
&lt;p&gt;I&#39;m happy to report that this gap has now been closed. &lt;strong&gt;Commerce 14 officially supports .NET 10 starting with version 14.46.0. &lt;/strong&gt;It&#39;s also mentioned in the &lt;a href=&quot;https://support.optimizely.com/hc/en-us/articles/23973422587405-2026-Commerce-Connect-release-notes#h_01KECT5AZ1XKGXHK59KB1XF3WT&quot;&gt;release notes&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;What&#39;s New&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;EPiServer.Commerce 14.46.0&lt;/strong&gt; &amp;ndash; adds official .NET 10 support.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;EPiServer.Find.Commerce 12.3.0&lt;/strong&gt; &amp;ndash; supports Commerce 14 running on .NET 10.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you&#39;re using &lt;strong&gt;Optimizely Search &amp;amp; Navigation (Find)&lt;/strong&gt; with Commerce, upgrade both packages together to ensure full compatibility.&lt;/p&gt;
&lt;div class=&quot;___1dmoc29 f10pi13n ftgm304 f1enuhaj fdclmfp f1nbblvp fat0sn4 f1ov4xf1 fekwl8i f1lmfglv f1oz7aqm f1abmfm4 f1w619qj f16h0jq8&quot;&gt;
&lt;table class=&quot;___1vyiefv f1ddd56o f16vktn6 f1ahpp82 f11qra4b f1uinfot fibjyge fvueend f9yszdx f1fu4s3n f3l3pb3 f10ghnd0 f8fmt76 fjvbh62 f1qrqxae f1vw5qpk fc02sbz fxawf59 fymf513 f1aoyrul f1el8yx3 f1pymoxg f1ofu761 fe6itr f7coize f1794535 f1o0pw0q fbjjl9v fk1v6el f16pyhcb f1ixlhx9 f12zef0i flu5r5u f19haqzy f1owmcxx f1oddm8q f1004tna fcoaxci fh0ee9u f15v23i2 f1dmj53 f1r1gcv9 f14z1veh ffufd3x f1ypplot f1660cg&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;th&gt;Package&lt;/th&gt;
&lt;th&gt;Minimum Version&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EPiServer.Commerce&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://nuget.optimizely.com/packages/episerver.commerce/14.46.0&quot;&gt;14.46.0&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EPiServer.Find.Commerce&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://nuget.optimizely.com/packages/episerver.find.commerce/12.3.0&quot;&gt;12.3.0&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p&gt;This means the full Optimizely stack, including both &lt;strong&gt;CMS 12 and Commerce 14&lt;/strong&gt;, can now run on &lt;strong&gt;.NET 10&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;Why Upgrade?&lt;/h2&gt;
&lt;p&gt;With &lt;strong&gt;.NET 8 and .NET 9 reaching end of support on November 10, 2026&lt;/strong&gt;, .NET 10 is now the recommended target. As the current LTS release, it provides support through &lt;strong&gt;November 2028&lt;/strong&gt;, along with performance improvements, security updates, and a longer support horizon.&lt;/p&gt;
&lt;h2&gt;Recommendations&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Upgrade &lt;strong&gt;EPiServer.Commerce&lt;/strong&gt; to &lt;strong&gt;14.46.0&lt;/strong&gt; or later before moving to .NET 10.&lt;/li&gt;
&lt;li&gt;Upgrade &lt;strong&gt;EPiServer.Find.Commerce&lt;/strong&gt; to &lt;strong&gt;12.3.0&lt;/strong&gt; or later if you use Find with Commerce.&lt;/li&gt;
&lt;li&gt;Verify that any additional add-ons or integrations are compatible with .NET 10.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For upgrade guidance and system requirements, refer to the &lt;a href=&quot;https://docs.developers.optimizely.com/content-management-system/docs/system-requirements-for-optimizely&quot;&gt;Optimizely Developer Documentation&lt;/a&gt; or contact &lt;a href=&quot;https://support.optimizely.com/hc/en-us&quot;&gt;Optimizely Support&lt;/a&gt; for assistance.&lt;/p&gt;
&lt;/div&gt;</id><updated>2026-08-07T02:53:49.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>The Top 10 Things I&#39;m Actually Using AI For</title><link href="https://world.optimizely.com/blogs/kennyg/dates/2026/8/the-top-10-things-im-actually-using-ai-for/" /><id>&lt;div&gt;
&lt;p&gt;A while back I wrote about ReviewPR, an Azure Function that uses AI to review Azure DevOps pull requests. That was one specific use case, but over the last year AI has become part of my day-to-day workflow in a lot of other ways.&lt;/p&gt;
&lt;p&gt;For context, we run Optimizely CMS 12 and Commerce Connect 14 on DXP for a national homebuilder. It&#39;s a large platform with a lot of moving parts, years of history, and a relatively small team responsible for keeping everything running.&lt;/p&gt;
&lt;p&gt;The biggest thing that changed for me is that I stopped thinking of AI as a coding tool. The real value has been giving it access to the systems I already use every day, then letting it help connect information across them. Logs, telemetry, deployments, databases, source code, configuration, and documentation all tell part of the story. Having something that can look across all of them at once has saved me a surprising amount of time.&lt;/p&gt;
&lt;p&gt;Here are the areas where I&#39;ve gotten the most value from it.&lt;/p&gt;
&lt;h2&gt;1. Troubleshooting Production Issues&lt;/h2&gt;
&lt;p&gt;This is the one that changed how I work.&lt;/p&gt;
&lt;p&gt;Most production incidents aren&#39;t caused by a single obvious failure. They show up as symptoms scattered across several systems, and a lot of the effort goes into figuring out which signals actually matter.&lt;/p&gt;
&lt;p&gt;One recurring pattern has been discovering that the original assumption was wrong. Problems that looked like database issues turned out to be networking issues. Stability problems ended up being scaling rules watching the wrong metric. Intermittent exceptions came back to application design problems that had been hiding for months. Looking at telemetry, deployments, infrastructure metrics, and source code together tends to surface connections that are easy to miss when you&#39;re investigating one system at a time.&lt;/p&gt;
&lt;p&gt;Just as importantly, it&#39;s been useful for ruling things out. Finding out a theory is wrong in an hour is often more valuable than finding the right answer after three days.&lt;/p&gt;
&lt;h2&gt;2. Answering the Questions Nobody Has Time For&lt;/h2&gt;
&lt;p&gt;Every long-running Optimizely implementation develops a backlog of questions that never quite make it onto a sprint.&lt;/p&gt;
&lt;p&gt;Which scheduled jobs are still needed? Which old integrations can be removed? Are there migrations that never completed correctly? How much data is sitting around because nobody ever cleaned it up?&lt;/p&gt;
&lt;p&gt;These are all answerable questions, but they&#39;re usually more tedious than difficult. AI has been particularly useful for this kind of investigative work because it can sift through the details much faster than I can manually.&lt;/p&gt;
&lt;h2&gt;3. Building Small Projects That Cross Too Many Boundaries&lt;/h2&gt;
&lt;p&gt;Some of my favorite uses have been side projects that touch several different technologies at once.&lt;/p&gt;
&lt;p&gt;Usually these aren&#39;t difficult projects. They just require learning a new API, figuring out an authentication model, understanding an unfamiliar library, and wiring everything together. That&#39;s often enough friction to keep a good idea from ever getting finished.&lt;/p&gt;
&lt;p&gt;Having AI help bridge those gaps has made it much easier to take an idea from concept to something working without spending days context-switching between documentation sites.&lt;/p&gt;
&lt;h2&gt;4. Arguing With My Own Code&lt;/h2&gt;
&lt;p&gt;I&#39;ve gotten into the habit of asking for a deliberately skeptical review before opening a pull request.&lt;/p&gt;
&lt;p&gt;Not &quot;does this look okay?&quot; but &quot;explain why this is wrong.&quot;&lt;/p&gt;
&lt;p&gt;That tends to produce much better feedback. It&#39;s caught fixes that only suppressed warnings, tests that weren&#39;t actually validating behavior, and assumptions that looked safe until somebody challenged them. Even when I disagree with the feedback, forcing myself to defend the implementation usually improves the final result.&lt;/p&gt;
&lt;h2&gt;5. Writing Better Tests&lt;/h2&gt;
&lt;p&gt;This is one area where I&#39;ve learned to be careful.&lt;/p&gt;
&lt;p&gt;Left alone, AI tends to write tests around implementation details rather than behavior. Those tests often pass, but they don&#39;t necessarily prove anything valuable.&lt;/p&gt;
&lt;p&gt;The best results have come from treating it like a partner in test design rather than a test generator. I want help identifying scenarios, edge cases, and requirements. The actual test is much less important than proving the behavior we&#39;re trying to protect.&lt;/p&gt;
&lt;h2&gt;6. Making Sense of Telemetry&lt;/h2&gt;
&lt;p&gt;Modern systems generate more telemetry than most developers can realistically consume.&lt;/p&gt;
&lt;p&gt;One thing AI does well is help summarize what changed between two periods of time and identify where an investigation should start. Sometimes the answer is obvious after the fact, but getting to that point can require digging through thousands of events, metrics, and traces.&lt;/p&gt;
&lt;p&gt;The biggest value isn&#39;t necessarily the answer itself. It&#39;s reducing the amount of time spent looking in the wrong place.&lt;/p&gt;
&lt;h2&gt;7. Finding the Bugs Nobody Sees&lt;/h2&gt;
&lt;p&gt;Some problems are obvious once they&#39;re found and almost invisible before that.&lt;/p&gt;
&lt;p&gt;I&#39;ve seen issues caused by a single unexpected character, subtle data inconsistencies, and small assumptions that quietly affected behavior without ever generating an obvious failure.&lt;/p&gt;
&lt;p&gt;These are the kinds of things humans can find, but they&#39;re also the kinds of things we tend to overlook because we&#39;re focused on larger problems. AI is surprisingly good at noticing details that don&#39;t stand out during normal troubleshooting.&lt;/p&gt;
&lt;h2&gt;8. Capturing Institutional Knowledge&lt;/h2&gt;
&lt;p&gt;Every team has information that exists mostly in people&#39;s heads.&lt;/p&gt;
&lt;p&gt;Environment-specific quirks, deployment lessons, platform limitations, and the odd exceptions that nobody remembers until something breaks.&lt;/p&gt;
&lt;p&gt;I&#39;ve started being much more intentional about documenting those lessons. AI has been useful for organizing and retrieving that information, especially when it spans years of projects and multiple systems.&lt;/p&gt;
&lt;h2&gt;9. Hardware Troubleshooting&lt;/h2&gt;
&lt;p&gt;The same approach works surprisingly well outside software development.&lt;/p&gt;
&lt;p&gt;Most hardware troubleshooting comes down to collecting evidence. Event logs, diagnostics, performance data, firmware versions, and error messages all provide clues, but pulling them together takes time.&lt;/p&gt;
&lt;p&gt;Being able to feed in the evidence and get a structured analysis has made the process significantly faster.&lt;/p&gt;
&lt;h2&gt;10. Home Networking and Security&lt;/h2&gt;
&lt;p&gt;I&#39;ve also used AI extensively for home networking and security projects.&lt;/p&gt;
&lt;p&gt;The biggest value hasn&#39;t been creating configurations. It&#39;s been reviewing them. Understanding what a rule actually allows, identifying unnecessary exposure, and validating assumptions turns out to be just as useful at home as it is in production systems.&lt;/p&gt;
&lt;p&gt;One lesson that carries across both worlds is that asking &quot;what could this reach?&quot; is often more useful than asking &quot;is this secure?&quot;&lt;/p&gt;
&lt;h2&gt;A Few Things to Watch&lt;/h2&gt;
&lt;p&gt;AI gets things wrong. Regularly.&lt;/p&gt;
&lt;p&gt;The failure mode isn&#39;t that it doesn&#39;t know the answer. The failure mode is that it gives a clean, confident explanation that sounds reasonable and happens to be completely wrong.&lt;/p&gt;
&lt;p&gt;That&#39;s why I always want to see the underlying evidence. The logs, the query, the deployment, the code, or whatever data led to the conclusion.&lt;/p&gt;
&lt;p&gt;I&#39;ve also found that context matters far more than prompts. The more access it has to the relevant information, the more useful it becomes. A model that can read logs, telemetry, source code, and configuration is a very different tool from one that can only answer questions in a chat window.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;If I had to summarize where AI has helped me most, it comes down to three things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Investigating problems faster.&lt;/li&gt;
&lt;li&gt;Connecting information across systems.&lt;/li&gt;
&lt;li&gt;Challenging assumptions before I spend time acting on them.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It hasn&#39;t replaced experience, judgment, or critical thinking.&lt;/p&gt;
&lt;p&gt;Mostly, it&#39;s removed a lot of the manual digging that used to sit between noticing a problem and understanding it.&lt;/p&gt;
&lt;/div&gt;</id><updated>2026-08-05T19:24:01.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Drag-and-Drop Reordering for Commerce Media Collection in Optimizely Commerce Connect</title><link href="https://world.optimizely.com/blogs/linh-doan-cuu/dates/2026/8/drag-and-drop-reordering-for-commerce-media-collection-in-optimizely-commerce-connect/" /><id>&lt;p&gt;Optimizely Commerce Connect ships a polished asset editor for the &lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;CommerceMediaCollection&amp;nbsp;&lt;/span&gt;property on catalog entries. It lets editors add, remove, and reorder media assets directly in the edit view &amp;mdash; a solid baseline for most projects. Reordering is done via &quot;Move Up&quot; and &quot;Move Down&quot; buttons in the grid, which is perfectly fine when you have a handful of assets.&lt;/p&gt;
&lt;p&gt;For some projects, though, editors need to manage dozens of images per product &amp;mdash; product shots, lifestyle images, detail crops, downloads &amp;mdash; and clicking a button 30 times to move an asset to the top becomes a real workflow problem. Drag-and-drop row reordering is the natural solution. This post walks through how to add it by extending the built-in Commerce editor rather than replacing it.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;The Property&lt;/h2&gt;
&lt;p&gt;The&amp;nbsp;&lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;CommerceMediaCollection&amp;nbsp;&lt;/span&gt;property is declared on&amp;nbsp;&lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;EntryContentBase&amp;nbsp;&lt;/span&gt;and typed as&amp;nbsp;&lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;ItemCollection&amp;lt;CommerceMedia&amp;gt;&lt;/span&gt;. Each&amp;nbsp;&lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;CommerceMedia&amp;nbsp;&lt;/span&gt;item carries a&amp;nbsp;&lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;SortOrder&amp;nbsp;&lt;/span&gt;integer that controls display priority on the front end &amp;mdash; carousels, image galleries, download lists. Whatever order the editor sees in the CMS is what the front end is supposed to render.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;[UIHint(&quot;commercemediacollection&quot;)]
public virtual ItemCollection&amp;lt;CommerceMedia&amp;gt; CommerceMediaCollection { get; set; }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The built-in editor already renders this as a dgrid with drag-handle affordances &amp;mdash; the visual infrastructure for drag-and-drop is present. Wiring it up for internal row reordering and making sure&amp;nbsp;&lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;SortOrder&amp;nbsp;&lt;/span&gt;is written correctly afterward is the implementation work.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Approach: Extend, Don&#39;t Replace&lt;/h2&gt;
&lt;p&gt;Optimizely Commerce Connect Asset Collection&#39;s editor descriptor sets up column definitions, thumbnail formatters, item converters, and the dialog flow for adding assets. Rather than reimplementing all of that, the approach is to register a custom descriptor that runs last, inherits everything the Commerce descriptor configured, and only swaps out the client-side widget class.&lt;/p&gt;
&lt;p&gt;The widget itself extends&amp;nbsp;&lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;CommerceMediaCollectionEditor&amp;nbsp;&lt;/span&gt;and overrides the minimum needed: how the grid&#39;s DnD layer is wired, how sort order is written after a drag, and whether columns are user-sortable.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Step 1 &amp;mdash; Register a Custom Editor Descriptor&lt;/h2&gt;
&lt;p&gt;Optimizely CMS resolves editor descriptors by&amp;nbsp;&lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;(TargetType, UIHint)&lt;/span&gt;&amp;nbsp;pair. The&amp;nbsp;&lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;CommerceMediaCollection&lt;/span&gt;&amp;nbsp;property carries&amp;nbsp;&lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;[UIHint(&quot;commercemediacollection&quot;)]&lt;/span&gt;, so the custom descriptor must declare the same UIHint.&amp;nbsp;&lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;EditorDescriptorBehavior.PlaceLast&lt;/span&gt;&amp;nbsp;ensures it runs after Commerce&#39;s built-in descriptor, so all Commerce-specific metadata is already applied before we override just the widget class name.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;using EPiServer.Commerce.SpecializedProperties;
using EPiServer.Shell.ObjectEditing;
using EPiServer.Shell.ObjectEditing.EditorDescriptors;

[EditorDescriptorRegistration(
    TargetType = typeof(ItemCollection&amp;lt;CommerceMedia&amp;gt;),
    UIHint = &quot;commercemediacollection&quot;,
    EditorDescriptorBehavior = EditorDescriptorBehavior.PlaceLast)]
public class CommerceMediaDndEditorDescriptor : EditorDescriptor
{
    public override void ModifyMetadata(
        ExtendedMetadata metadata,
        IEnumerable&amp;lt;Attribute&amp;gt; attributes)
    {
        base.ModifyMetadata(metadata, attributes);
        metadata.ClientEditingClass = &quot;myproject/editors/CommerceMediaDndEditor&quot;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Step 2 &amp;mdash; Create a Protected Shell Module&lt;/h2&gt;
&lt;p&gt;The Dojo AMD path&amp;nbsp;&lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;myproject/editors/CommerceMediaDndEditor&lt;/span&gt;&amp;nbsp;must resolve to a real file. That means registering a Dojo package called&amp;nbsp;&lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;myproject&amp;nbsp;&lt;/span&gt;via a protected shell module.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;modules/_protected/MyProject.Commerce.UI/module.config:&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&amp;gt;
&amp;lt;module name=&quot;MyProject.Commerce.UI&quot; clientResourceRelativePath=&quot;&quot;&amp;gt;
    &amp;lt;dojo&amp;gt;
        &amp;lt;packages&amp;gt;
            &amp;lt;add name=&quot;myproject&quot; location=&quot;ClientResources&quot; /&amp;gt;
        &amp;lt;/packages&amp;gt;
    &amp;lt;/dojo&amp;gt;
    &amp;lt;clientModule&amp;gt;
        &amp;lt;moduleDependencies&amp;gt;
            &amp;lt;add dependency=&quot;CMS&quot; /&amp;gt;
            &amp;lt;add dependency=&quot;Commerce&quot; /&amp;gt;
        &amp;lt;/moduleDependencies&amp;gt;
    &amp;lt;/clientModule&amp;gt;
&amp;lt;/module&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Source file tracking:&lt;/strong&gt;&amp;nbsp;&lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;modules/_protected/&amp;nbsp;&lt;/span&gt;is typically gitignored because NuGet restores add-on packages there. Keep your source in a separate tracked directory (e.g.&amp;nbsp;&lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;ShellModules/_protected/&lt;/span&gt;) and copy it at build time:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;ItemGroup&amp;gt;
    &amp;lt;ShellModuleSource Include=&quot;ShellModules\_protected\**\*&quot; /&amp;gt;
    &amp;lt;Content Remove=&quot;ShellModules\_protected\**\*&quot; /&amp;gt;
    &amp;lt;Content Include=&quot;@(ShellModuleSource)&quot;&amp;gt;
        &amp;lt;Link&amp;gt;modules\_protected\%(RecursiveDir)%(FileName)%(Extension)&amp;lt;/Link&amp;gt;
        &amp;lt;CopyToOutputDirectory&amp;gt;PreserveNewest&amp;lt;/CopyToOutputDirectory&amp;gt;
        &amp;lt;CopyToPublishDirectory&amp;gt;PreserveNewest&amp;lt;/CopyToPublishDirectory&amp;gt;
    &amp;lt;/Content&amp;gt;
&amp;lt;/ItemGroup&amp;gt;

&amp;lt;Target Name=&quot;CopyCustomShellModules&quot; BeforeTargets=&quot;Build&quot;&amp;gt;
    &amp;lt;Copy
        SourceFiles=&quot;@(ShellModuleSource)&quot;
        DestinationFolder=&quot;$(MSBuildProjectDirectory)\modules\_protected\%(RecursiveDir)&quot;
        SkipUnchangedFiles=&quot;true&quot; /&amp;gt;
&amp;lt;/Target&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Explicit module registration:&lt;/strong&gt;&amp;nbsp;EPiServer Shell 12.x auto-discovery matches module directories to assemblies by name. A module named&amp;nbsp;&lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;MyProject.Commerce.UI&lt;/span&gt;&amp;nbsp;with no correspondingly-named assembly is skipped. Register it explicitly via&amp;nbsp;&lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;IConfigurableModule&lt;/span&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;using EPiServer.Framework;
using EPiServer.Framework.Initialization;
using EPiServer.ServiceLocation;
using EPiServer.Shell.Modules;
using Microsoft.Extensions.DependencyInjection;

[InitializableModule]
[ModuleDependency(typeof(EPiServer.Shell.UI.InitializationModule))]
public class CommerceUiModuleRegistration : IConfigurableModule
{
    public void ConfigureContainer(ServiceConfigurationContext context)
    {
        context.Services.Configure&amp;lt;ProtectedModuleOptions&amp;gt;(options =&amp;gt;
        {
            if (options.Items.Any(x =&amp;gt; x.Name == &quot;MyProject.Commerce.UI&quot;))
                return;

            options.Items.Add(new ModuleDetails
            {
                Name = &quot;MyProject.Commerce.UI&quot;
            });
        });
    }

    public void Initialize(InitializationEngine context) { }
    public void Uninitialize(InitializationEngine context) { }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt;&amp;nbsp;Do not set&amp;nbsp;&lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;Assemblies&amp;nbsp;&lt;/span&gt;in&amp;nbsp;&lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;ModuleDetails&amp;nbsp;&lt;/span&gt;to your main web assembly. This module is purely client-side (JavaScript +&amp;nbsp;&lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;module.config&lt;/span&gt;) &amp;mdash; no C# shell controllers. Pointing&amp;nbsp;&lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;Assemblies&lt;/span&gt;&amp;nbsp;at the main project assembly causes EPiServer Shell to re-process it under its own application-part rules, which conflicts with ASP.NET Core&#39;s existing registration of that assembly and breaks ViewComponent discovery. Omit&amp;nbsp;&lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;Assemblies&amp;nbsp;&lt;/span&gt;for client-only modules.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;Step 3 &amp;mdash; The Custom Dojo Widget&lt;/h2&gt;
&lt;p&gt;The widget has three jobs:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Enable internal row DnD&lt;/strong&gt;&amp;nbsp;&amp;mdash;&amp;nbsp;&lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;epi/shell/dnd/Source&lt;/span&gt;&amp;nbsp;(the DnD source class used by the grid) does not self-accept by default when&amp;nbsp;&lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;accept&lt;/span&gt;&amp;nbsp;type strings are configured. Commerce media items don&#39;t carry a recognized&amp;nbsp;&lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;typeIdentifier&lt;/span&gt;, so the type-matching check fails for same-source drops. Wrapping&amp;nbsp;&lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;checkAcceptance&amp;nbsp;&lt;/span&gt;with&amp;nbsp;&lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;dojo/aspect&lt;/span&gt;&#39;s&amp;nbsp;&lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;around&amp;nbsp;&lt;/span&gt;restores the standard behaviour for internal drags while leaving external drop handling (adding assets from the DAM) completely unchanged.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Write sequential&amp;nbsp;&lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;SortOrder&lt;/span&gt;&amp;nbsp;values&lt;/strong&gt;&amp;nbsp;&amp;mdash; After a drag, every item needs a&amp;nbsp;&lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;SortOrder&amp;nbsp;&lt;/span&gt;that matches its new visual position. The custom model override handles the move and renumbers all items 1, 2, 3, &amp;hellip; in one atomic update, firing&amp;nbsp;&lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;itemsChanged&amp;nbsp;&lt;/span&gt;exactly once with the final correct state.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Disable column header sorting&lt;/strong&gt;&amp;nbsp;&amp;mdash; Clicking a column header in the grid would re-sort rows by that column&#39;s data without saving, creating a mismatch between what the editor sees and what is stored. Marking all columns&amp;nbsp;&lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;sortable: false&lt;/span&gt;&amp;nbsp;prevents this.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;define([
  &quot;dojo/_base/declare&quot;,
  &quot;dojo/aspect&quot;,
  &quot;epi-ecf-ui/contentediting/editors/CommerceMediaCollectionEditor&quot;,
  &quot;epi-ecf-ui/contentediting/editors/model/CommerceMediaCollectionEditorModel&quot;,
], function (
  declare,
  aspect,
  CommerceMediaCollectionEditor,
  CommerceMediaCollectionEditorModel,
) {
  // Extended model: moves the item in the array and assigns sequential
  // SortOrder values across all items in a single atomic update.
  var ShiftReorderModel = declare([CommerceMediaCollectionEditorModel], {
    moveItem: function (item, target, before) {
      // Suppress itemsChanged from the first splice (remove)
      this._itemsUnchanged = true;
      var fromIdx = this._itemModels.indexOf(item);
      this._itemModels.splice(fromIdx, 1);

      // Suppress itemsChanged from the second splice (insert)
      this._itemsUnchanged = true;
      var toIdx = this._itemModels.indexOf(target);
      toIdx =
        toIdx === -1
          ? this._itemModels.length // dropped past last row &amp;rarr; append
          : before
            ? toIdx
            : toIdx + 1;
      this._itemModels.splice(toIdx, 0, item);

      // Renumber: SortOrder 1, 2, 3, &amp;hellip; in array order
      this._itemModels.forEach(function (m, i) {
        m.sortOrder = i + 1;
      });

      // Fire once with the final correct state
      this.emit(&quot;itemsChanged&quot;, this.get(&quot;items&quot;));
    },
  });

  return declare([CommerceMediaCollectionEditor], {
    modelType: ShiftReorderModel,

    // Disable column header sorting so grid order always reflects stored order
    _getGridDefinition: function () {
      var columns = this.inherited(arguments);
      for (var col in columns) {
        if (columns[col]) {
          columns[col].sortable = false;
        }
      }
      return columns;
    },

    // Wire internal DnD and restore self-acceptance for same-source drops
    _setupDnD: function () {
      this.inherited(arguments);

      var dndSrc = this.grid.dndSource;
      this.own(
        aspect.around(dndSrc, &quot;checkAcceptance&quot;, function (original) {
          return function (source, nodes) {
            // Allow reordering within the same grid
            if (source === this) {
              return true;
            }
            return original.apply(this, arguments);
          };
        }),
      );
    },
  });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Place this at&amp;nbsp;&lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;ShellModules/_protected/MyProject.Commerce.UI/ClientResources/editors/CommerceMediaDndEditor.js&lt;/span&gt;&amp;nbsp;(the MSBuild target copies it to&amp;nbsp;&lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;modules/_protected/&lt;/span&gt;&amp;nbsp;at build time).&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;End Result&lt;/h2&gt;
&lt;p&gt;Editors open a catalog entry, switch to the Assets tab, and drag rows to reorder media. The grid updates immediately. On save, each asset&#39;s&amp;nbsp;&lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;SortOrder&amp;nbsp;&lt;/span&gt;reflects its position in the grid &amp;mdash; 1 for the first row, 2 for the second, and so on. No separate admin page, no property changes, no base class modifications.&lt;/p&gt;
&lt;p&gt;The solution can be applied to any&amp;nbsp;&lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;ItemCollection&amp;lt;CommerceMedia&amp;gt;&lt;/span&gt;&amp;nbsp;property in the codebase with the correct UIHint.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;File Checklist&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;CommerceMediaDndEditorDescriptor.cs&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;Registers the custom widget for&amp;nbsp;&lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;ItemCollection&amp;lt;CommerceMedia&amp;gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;CommerceUiModuleRegistration.cs&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;Registers the shell module explicitly so auto-discovery doesn&#39;t skip it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;ShellModules/_protected/MyProject.Commerce.UI/module.config&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;Declares the&amp;nbsp;&lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;myproject&amp;nbsp;&lt;/span&gt;Dojo package&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;ShellModules/_protected/MyProject.Commerce.UI/ClientResources/editors/CommerceMediaDndEditor.js&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;The custom editor widget&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;MyProject.csproj&lt;/span&gt;&amp;nbsp;(MSBuild target)&lt;/td&gt;
&lt;td&gt;Copies shell module source to&amp;nbsp;&lt;span style=&quot;font-family: &#39;courier new&#39;, courier, monospace;&quot;&gt;modules/_protected/ &lt;/span&gt;at build time&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;</id><updated>2026-08-05T09:48:55.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Order tabs with drag and drop V2</title><link href="https://world.optimizely.com/blogs/Per-Nergard/Dates/2026/8/order-tabs-with-drag-and-drop-v2/" /><id>&lt;p&gt;I earlier did a very simple Blazor version to be able to sort tabs with drag and drop.&lt;/p&gt;
&lt;p&gt;I wanted to update it a bit and also display how the tabs are actually ordered on the content types to spot inconsistencys or make it easier to fine tune placement by being able to drag and drop in the tab layout directly instead of adjusting in a potential long vertical list.&lt;br /&gt;&lt;br /&gt;Nothing changes until you hit save. Have som export / import buttons but I havent tested that yet.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;You can find the code over at my &lt;a href=&quot;https://github.com/PNergard/Nergard.Opti.TabSorter&quot;&gt;GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/476714de6566474da9deacd2b66a120d.aspx&quot; alt=&quot;&quot; width=&quot;759&quot; height=&quot;627&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</id><updated>2026-08-04T09:56:57.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Optimizely : Missing Language Manager Gadget Fix in CMS 12 After Upgrading to .NET 10</title><link href="https://madhuanbalagan.com/?p=5042" /><id>&lt;p&gt;Recently, while upgrading an Optimizely CMS solution to .NET 10, we came across an issue where the Language Manager gadget completely disappeared from CMS Edit&amp;#46;&amp;#46;&amp;#46;&lt;/p&gt;
&lt;p&gt;The post &lt;a href=&quot;https://madhuanbalagan.com/optimizely-missing-language-manager-gadget-fix-in-cms-12-after-upgrading-to-net-10&quot;&gt;Optimizely : Missing Language Manager Gadget Fix in CMS 12 After Upgrading to .NET 10&lt;/a&gt; appeared first on &lt;a href=&quot;https://madhuanbalagan.com&quot;&gt;Madhu Anbalagan&amp;#039;s Blog&lt;/a&gt;.&lt;/p&gt;
</id><updated>2026-08-02T16:15:41.0000000Z</updated><summary type="html">Blog post</summary></entry></feed>