<?xml version="1.0" encoding="utf-8"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/"><channel><language>en</language><title>Blogs</title> <link>https://world.optimizely.com/blogs/</link><description></description><ttl>60</ttl><generator>Optimizely World</generator><item> <title>A note on using async computed fields with Optimizely Graph</title>            <link>https://krompaco.nu/2026/11/a-note-on-using-async-computed-fields-with-optimizely-graph/</link>            <description>Computed fields can be async, but do they work smoothly with the Graph C# SDK?</description>            <guid>https://krompaco.nu/2026/11/a-note-on-using-async-computed-fields-with-optimizely-graph/</guid>            <pubDate>Mon, 09 Nov 2026 06:44:00 GMT</pubDate>           <category>Blog post</category></item><item> <title>Visual Builder without Next.js, Part 2: The Routing Decision Travels, the Engine Contract Doubles</title>            <link>https://pino-labs.com/blog/visual-builder-without-nextjs-part-2-strangler-astro-nextjs-cms-13/</link>            <description>One content tree, two engines, one edge: a strangler for Astro and Next.js on CMS 13. The same routing decision in a Next.js proxy and in a Cloudflare Worker, measured with the checks from part 1.</description>            <guid>https://pino-labs.com/blog/visual-builder-without-nextjs-part-2-strangler-astro-nextjs-cms-13/</guid>            <pubDate>Sun, 08 Nov 2026 00:00:00 GMT</pubDate>           <category>Blog post</category></item><item> <title>Stride on SaaS CMS and Frontend Hosting, with Claude Code</title>            <link>https://world.optimizely.com/blogs/Minesh-Shah/Dates/2026/11/stride-on-saas-cms-and-frontend-hosting-with-claude-code/</link>            <description>&lt;p&gt;This post shows how I set up the Optimizely Stride sample site on a new Optimizely CMS (SaaS) instance. The front end is on Optimizely Frontend Hosting. It deploys from the CLI and from GitHub Actions. I used Claude Code to do most of the work.&lt;/p&gt;
&lt;p&gt;Stride is a Next.js sample site from Optimizely. Optimizely announced it in the &lt;a href=&quot;https://docs.optimizely.com/cms-saas/release-notes/2026-cms-saas-release-notes#new-stride-sample-site&quot;&gt;CMS (SaaS) release notes&lt;/a&gt; on 23/07/2026. It uses the Content JS SDK, Visual Builder compositions and Tailwind CSS.&lt;/p&gt;
&lt;p&gt;&lt;img style=&quot;max-width: 100%;&quot; src=&quot;/link/ceae6bdbb10c435e83cd9a4e3a52c2f5.aspx&quot; alt=&quot;The Stride home page with the imported content&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The Stride README gives the basic steps. On a new instance, I found seven problems that the README does not cover. This post gives each problem, its cause and the fix. At the end, there is a prompt that you can give to Claude Code to do the same setup.&lt;/p&gt;
&lt;p&gt;&lt;img style=&quot;max-width: 100%;&quot; src=&quot;/link/d3c2a47432ee46138e93eb6e3d8fd82c.aspx&quot; alt=&quot;How the parts connect: code ships through the DXP Deployment API, content through Graph&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Content goes from the CMS to Graph when you publish it. The site reads the content from Graph. Code goes from your computer or GitHub Actions to Frontend Hosting.&lt;/p&gt;
&lt;h2&gt;What you need&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;A CMS (SaaS) instance with Optimizely Graph. You need admin access to create API keys and import data.&lt;/li&gt;
&lt;li&gt;A Frontend Hosting project in the DXP management portal. The plan shows as &quot;SaaS Frontend Host&quot;. It has Test1, Test2 and Production environments.&lt;/li&gt;
&lt;li&gt;Node.js 22 or 24. Stride asks for &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;^22.23.4 || ^24.22.1&lt;/span&gt;. I used 22.22.3. The install gave a warning, and the build worked.&lt;/li&gt;
&lt;li&gt;npm or yarn. Do not use pnpm. Frontend Hosting accepts only &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;package-lock.json&lt;/span&gt; or &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;yarn.lock&lt;/span&gt;.&lt;/li&gt;
&lt;li&gt;A GitHub repository, if you want to deploy from GitHub Actions.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;1. Create the project with cms-create-app&lt;/h2&gt;
&lt;p&gt;Use the &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;@optimizely/cms-create-app&lt;/span&gt; package to create the project. Do not copy the &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;templates/stride&lt;/span&gt; folder from the &lt;a href=&quot;https://github.com/episerver/content-js-sdk/tree/main/templates/stride&quot;&gt;content-js-sdk repository&lt;/a&gt;. In that repository, the SDK and the CLI are &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;workspace:*&lt;/span&gt; dependencies. The Frontend Hosting deploy does not accept these. The package changes them to the latest published versions.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx @optimizely/cms-create-app@1.1.0 stride-saas --template nextjs-stride --pm npm --ci none&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This gave me &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;@optimizely/cms-sdk&lt;/span&gt; 3.1.0, &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;@optimizely/cms-cli&lt;/span&gt; 3.1.0 and Next.js 16.3.6. Version 1.1.0 of the package was published on 03/11/2026.&lt;/p&gt;
&lt;p&gt;The package does not include two files. Add them before your first commit:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;.gitignore&lt;/span&gt;. Copy it from the template. Add &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;/node_modules/&lt;/span&gt;. Add &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;!.env.example&lt;/span&gt;, because the &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;.env*&lt;/span&gt; rule also hides that file.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;.env.example&lt;/span&gt;. Copy it from the template. It lists all the variables that Stride and the CLI use.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I also added a &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;.gitattributes&lt;/span&gt; file with &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;* text=auto eol=lf&lt;/span&gt;. I work on Windows, and the platform builds on Linux.&lt;/p&gt;
&lt;h2&gt;2. Connect the project to the CMS&lt;/h2&gt;
&lt;p&gt;In the CMS, go to &lt;strong&gt;Settings &amp;rarr; API Keys&lt;/strong&gt;. Copy the Graph single key. Create an API key for the CLI. Put these values in &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;.env&lt;/span&gt;:&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin-bottom: 1rem;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding: 6px 10px; border: 1px solid #c8c8c8; vertical-align: top;&quot;&gt;Variable&lt;/th&gt;
&lt;th style=&quot;padding: 6px 10px; border: 1px solid #c8c8c8; vertical-align: top;&quot;&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 6px 10px; border: 1px solid #c8c8c8; vertical-align: top;&quot;&gt;&lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;OPTIMIZELY_CMS_URL&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 6px 10px; border: 1px solid #c8c8c8; vertical-align: top;&quot;&gt;The URL of your instance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 6px 10px; border: 1px solid #c8c8c8; vertical-align: top;&quot;&gt;&lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;OPTIMIZELY_GRAPH_SINGLE_KEY&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 6px 10px; border: 1px solid #c8c8c8; vertical-align: top;&quot;&gt;Settings &amp;rarr; API Keys&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 6px 10px; border: 1px solid #c8c8c8; vertical-align: top;&quot;&gt;&lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;OPTIMIZELY_CMS_CLIENT_ID&lt;/span&gt;, &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;OPTIMIZELY_CMS_CLIENT_SECRET&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 6px 10px; border: 1px solid #c8c8c8; vertical-align: top;&quot;&gt;The API key that you created&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 6px 10px; border: 1px solid #c8c8c8; vertical-align: top;&quot;&gt;&lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;APPLICATION_HOST&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 6px 10px; border: 1px solid #c8c8c8; vertical-align: top;&quot;&gt;The URL of the site (see step 6)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Give the API key access rights before you push the configuration:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;Settings &amp;rarr; Set Access Rights&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Select the root.&lt;/li&gt;
&lt;li&gt;Select &lt;strong&gt;Apply settings for all subitems&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Give the API key full permissions.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Note: I did not do this the first time. The push sent the content types and display templates. Then it stopped with a 403 error (&quot;Required access is &#39;create&#39;&quot;) when it tried to create the start page.&lt;/p&gt;
&lt;p&gt;Push the configuration:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm run config:push&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This command sends the content types and display templates to the CMS. It also creates the Stride application and an empty Home page. The empty Home page becomes the start page. Step 3 shows why this is a problem.&lt;/p&gt;
&lt;h2&gt;3. Import the content and fix the start page&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;Settings &amp;rarr; Import Data&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Upload &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;stride_content.episerverdata&lt;/span&gt;.&lt;/li&gt;
&lt;li&gt;Select the root as the destination.&lt;/li&gt;
&lt;li&gt;Select &lt;strong&gt;Update existing content items with matching ID&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Start the import.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Note: The README gives the file name &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;ExportedFile.episerverdata&lt;/span&gt;. The file in the template is &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;stride_content.episerverdata&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;My import showed &quot;Import failed&quot;. But the summary showed that all 34 content items, 26 content types and 6 style settings were imported. The three errors were all about the &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;Validators&lt;/span&gt; property of one form. Step 4 gives the cause.&lt;/p&gt;
&lt;p&gt;&lt;img style=&quot;max-width: 100%;&quot; src=&quot;/link/3370bf7378c14d85ac6642a8fdc3e8bc.aspx&quot; alt=&quot;The import summary: Import failed, but all 34 content items imported, with three Validators errors&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Problem:&lt;/strong&gt; The site shows a 404 error for &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;/en&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cause:&lt;/strong&gt; &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;config:push&lt;/span&gt; created an empty Home page and made it the start page. The import does not replace this page. The imported Home page gets the name &quot;Home(2)&quot;, and all the pages go under &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;/en/home2/&lt;/span&gt;. The empty Home page is not published, so Graph has no page at &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;/en&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;Settings &amp;rarr; Applications &amp;rarr; Stride&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Change the start page to Home(2).&lt;/li&gt;
&lt;li&gt;Delete the empty Home page.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img style=&quot;max-width: 100%;&quot; src=&quot;/link/72c01890681f495494f390fc32eeb0f6.aspx&quot; alt=&quot;The Stride application settings with Home(2) as the start page&quot; /&gt;&lt;/p&gt;
&lt;p&gt;When I checked Graph again, the pages had the correct URLs.&lt;/p&gt;
&lt;p&gt;Then change &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;optimizely.config.mjs&lt;/span&gt;. If you do not, the next &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;config:push&lt;/span&gt; sets the start page back to the empty Home page. Remove the &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;content&lt;/span&gt; array. Set &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;entryPoint&lt;/span&gt; to the imported Home page:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;applications: [
  {
    key: &#39;stride&#39;,
    // The imported Stride start page, not the empty &quot;home&quot; that the template creates
    entryPoint: &#39;cms://content/a8ff42c351e74f65a2f26223950c54ab&#39;,
    // ...
  },
],&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This GUID is the Home page in &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;stride_content.episerverdata&lt;/span&gt;, so it is the same on all instances.&lt;/p&gt;
&lt;h2&gt;4. Register only the Forms elements that your instance has&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Problem:&lt;/strong&gt; The home page works, but most other pages show a 500 error. Graph returns six errors, for example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Unknown type &quot;OptiFormsNumberElement&quot;. Did you mean &quot;OptiFormsSubmitElement&quot;, ...&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Cause:&lt;/strong&gt; Stride calls &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;initForms()&lt;/span&gt; in &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;lib/initialization.ts&lt;/span&gt;. This function registers all ten Optimizely Forms element types. The SDK then puts all ten types in the query for each page that can contain a form. My instance had only four of them: the form container, Textbox, Textarea and Submit. If one type is unknown, Graph rejects the full query.&lt;/p&gt;
&lt;p&gt;To see the types that your instance has, run &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;npx optimizely-cms-cli config pull -r -j&lt;/span&gt;. This difference in Forms versions is also the probable cause of the &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;Validators&lt;/span&gt; errors in step 3.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Do not call &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;initForms()&lt;/span&gt;. Register only the types that your instance has:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;initContentTypeRegistry([
  // ...Stride&#39;s own types
  OptiFormsContainerDataContentType,
  OptiFormsTextboxElementContentType,
  OptiFormsTextareaElementContentType,
  OptiFormsSubmitElementContentType,
  OptiFormsDependencyRuleContentType,
  OptiFormsConditionContentType,
]);

initReactComponentRegistry({
  resolver: {
    // ...Stride&#39;s own components
    OptiFormsContainerData: FormContainer,
    OptiFormsTextboxElement: FormInput,
    OptiFormsTextareaElement: FormTextarea,
    OptiFormsSubmitElement: FormSubmit,
  },
});&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Restart the dev server after this change. The registry is set one time, when the server starts. After the restart, all 16 Stride pages returned status 200, and the Contact Us form showed correctly. When your instance gets the other Forms elements, use &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;initForms()&lt;/span&gt; again.&lt;/p&gt;
&lt;h2&gt;5. Preview and drafts do not need the Graph app key&lt;/h2&gt;
&lt;p&gt;The setup asks only for the Graph single key. The single key gives only published content. Older headless builds used the Graph app key and secret to get drafts.&lt;/p&gt;
&lt;p&gt;The SDK uses preview tokens. The Stride application has &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;usePreviewTokens: true&lt;/span&gt;. When an editor opens a page in edit mode or in Visual Builder, the CMS opens the &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;/preview&lt;/span&gt; route of the site. The URL contains a &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;preview_token&lt;/span&gt; that is valid for a short time. &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;getPreviewContent()&lt;/span&gt; sends this token to Graph, and Graph returns the draft. When the editor saves, the CMS sends a new token, and the SDK uses it.&lt;/p&gt;
&lt;p&gt;&lt;img style=&quot;max-width: 100%;&quot; src=&quot;/link/fb9045fe0c28491ea497a19fb8829b74.aspx&quot; alt=&quot;Visual Builder in the CMS test environment, showing the Stride Home page from Frontend Hosting Test1&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Visual Builder in the CMS test environment. The preview on the right is the Stride site on Frontend Hosting Test1.&lt;/p&gt;
&lt;p&gt;You need the app key and secret only for server-side requests outside an editor session, with the SDK option &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;auth: { type: &#39;hmac&#39; }&lt;/span&gt;. Examples are restricted content, deleted or expired items, and search that includes drafts. Stride does not need these. The app key and secret can read all content in Graph. Do not put them in a public site unless a feature needs them.&lt;/p&gt;
&lt;h2&gt;6. Deploy to Frontend Hosting from the CLI&lt;/h2&gt;
&lt;p&gt;CLI 3.1.0 has a &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;deploy&lt;/span&gt; command. In the &lt;a href=&quot;/link/78cf3461994c4df084c5ae460b447ed0.aspx&quot;&gt;Frontend Hosting beta&lt;/a&gt;, you made a zip file with the name &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;&amp;lt;name&amp;gt;.head.app.&amp;lt;version&amp;gt;.zip&lt;/span&gt;. Then you uploaded it with the EpiCloud PowerShell module. Now you use one command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm run deploy -- --env Test1&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The command does these steps:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;It checks the project.&lt;/li&gt;
&lt;li&gt;It runs &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;next build&lt;/span&gt; on your computer. A build error stops the deploy before the upload.&lt;/li&gt;
&lt;li&gt;It makes the zip file. The zip file does not include build output, &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;node_modules&lt;/span&gt;, &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;.env&lt;/span&gt; files or the content export.&lt;/li&gt;
&lt;li&gt;It uploads the zip file with the DXP Deployment API.&lt;/li&gt;
&lt;li&gt;It starts the deployment, waits for it, and completes it.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Get the credentials from the &lt;strong&gt;API&lt;/strong&gt; tab in the DXP management portal. Put them in &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;.env&lt;/span&gt; as &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;OPTIMIZELY_DXP_PROJECT_ID&lt;/span&gt;, &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;OPTIMIZELY_DXP_CLIENT_KEY&lt;/span&gt; and &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;OPTIMIZELY_DXP_CLIENT_SECRET&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;To check the zip file before you upload it, run &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;npx optimizely-cms-cli deploy --output ./out&lt;/span&gt;. This command writes the zip file to a local folder. It does not upload it.&lt;/p&gt;
&lt;h3&gt;Use the front-end environment that matches your CMS environment&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Problem:&lt;/strong&gt; The deploy to Test1 was successful, but all published pages showed a 404 error. Visual Builder preview on Test1 worked.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cause:&lt;/strong&gt; Each Frontend Hosting environment is linked to a CMS environment. The platform gives each front-end environment the CMS URL and the Graph keys of its linked CMS environment. My Test1 was linked to the CMS test environment. I set up Stride in the CMS Production environment, so Graph on Test1 had no Stride pages. Preview worked because the CMS sends its own preview token with each preview request.&lt;/p&gt;
&lt;p&gt;I found this with a temporary API route. The route showed the CMS URL that the site received, and a short hash of the Graph key. Both were different from the values in my &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;.env&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Put the content in the CMS environment that is linked to your front-end environment. You can deploy to the front-end environment that is linked to the CMS environment with your content, or copy the content to the other CMS environments. I copied the content to the CMS test environment, which is linked to Test1.&lt;/p&gt;
&lt;h3&gt;Copy the content to the other CMS environments&lt;/h3&gt;
&lt;p&gt;Do these steps in each CMS environment that does not have the content. I did them in the CMS test environment, which is linked to Test1.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create an API key for the CLI, and give it full access rights on the root.&lt;/li&gt;
&lt;li&gt;Import &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;stride_content.episerverdata&lt;/span&gt; at the root. Do this before the first &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;config:push&lt;/span&gt;.&lt;/li&gt;
&lt;li&gt;Push the configuration with the CMS URL, the API key and the &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;APPLICATION_HOST&lt;/span&gt; of that environment.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I keep the values for the test environment in a second file, &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;.env.cms-test&lt;/span&gt;, and use a second script:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;&quot;config:push:test&quot;: &quot;node --env-file=.env.cms-test node_modules/@optimizely/cms-cli/bin/run.js config push optimizely.config.mjs&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The CLI loads &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;.env&lt;/span&gt; with dotenv, which does not replace variables that are already set. So the values from &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;.env.cms-test&lt;/span&gt; are used. In &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;optimizely.config.mjs&lt;/span&gt;, the primary host comes from &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;APPLICATION_HOST&lt;/span&gt;, so one file works for all environments:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const host = new URL(process.env.APPLICATION_HOST.trim()).host;
// ...
hosts: [{ authority: host, type: &#39;primary&#39;, preferredUrlScheme: &#39;https&#39; }],&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you import before the first push, the empty Home page from step 3 does not occur. The push creates the application with the imported Home page as the start page. Graph showed the new URLs a few minutes later. Test1 then returned status 200 for all pages, without a new deploy, because the site reads Graph at request time.&lt;/p&gt;
&lt;h3&gt;Set the hostname and APPLICATION_HOST&lt;/h3&gt;
&lt;p&gt;Graph makes all page URLs from the primary host of the application. The SDK finds a page only when &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;APPLICATION_HOST&lt;/span&gt; is the same as that host. If they are different, all pages show a 404 error.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Get the hostname of the front-end environment from the &lt;strong&gt;Hostnames&lt;/strong&gt; tab in the DXP management portal.&lt;/li&gt;
&lt;li&gt;In &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;optimizely.config.mjs&lt;/span&gt;, set this hostname as the only host, with the type &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;primary&lt;/span&gt;.&lt;/li&gt;
&lt;li&gt;Run &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;npm run config:push&lt;/span&gt;.&lt;/li&gt;
&lt;li&gt;In your local &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;.env&lt;/span&gt;, set &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;APPLICATION_HOST&lt;/span&gt; to &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;https://&amp;lt;hostname&amp;gt;&lt;/span&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Local development still works, because &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;APPLICATION_HOST&lt;/span&gt; is only a filter for Graph. CMS preview now opens on this front-end environment.&lt;/p&gt;
&lt;p&gt;Note: Do not add &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;localhost:3000&lt;/span&gt; as a second host. Headless applications accept only Primary, Preview and Media hosts. The CMS rejects other types.&lt;/p&gt;
&lt;h3&gt;Add APPLICATION_HOST as an App Setting&lt;/h3&gt;
&lt;p&gt;In the DXP management portal, open the &lt;strong&gt;App Settings&lt;/strong&gt; tab. Add &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;APPLICATION_HOST&lt;/span&gt; for each environment, with the hostname of that environment. App Settings apply at the next deployment, so add them before you deploy.&lt;/p&gt;
&lt;p&gt;Do not add the CMS URL or the Graph keys. The platform gives these five variables to the build and to the site (&lt;a href=&quot;https://docs.optimizely.com/cms-saas/docs/host-a-front-end-with-optimizely&quot;&gt;Frontend Hosting documentation&lt;/a&gt;):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;OPTIMIZELY_CMS_URL&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;OPTIMIZELY_GRAPH_GATEWAY&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;OPTIMIZELY_GRAPH_SINGLE_KEY&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;OPTIMIZELY_GRAPH_APP_KEY&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;OPTIMIZELY_GRAPH_SECRET&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The App Settings list does not show these variables. When I tried to add &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;OPTIMIZELY_CMS_URL&lt;/span&gt;, the portal said that the name already exists.&lt;/p&gt;
&lt;p&gt;&lt;img style=&quot;max-width: 100%;&quot; src=&quot;/link/0953c45a79d94108bf1dbb53238e3f00.aspx&quot; alt=&quot;The App Settings tab: OPTIMIZELY_CMS_URL already exists, and only APPLICATION_HOST is in the list&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;7. Deploy from GitHub Actions&lt;/h2&gt;
&lt;p&gt;The &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;cms-create-app&lt;/span&gt; package can make a workflow with the &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;--ci github&lt;/span&gt; option. I added my workflow later. It uses the same file, with two changes. It does not deploy when a commit changes only Markdown files. A manual run lets you select the environment.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;name: Deploy to Optimizely

on:
  push:
    branches: [main]
    paths-ignore: [&#39;**.md&#39;, &#39;*.episerverdata&#39;]
  workflow_dispatch:
    inputs:
      environment:
        type: choice
        options: [Test1, Test2, Production1]
        default: Test1

concurrency:
  group: optimizely-deploy-${{ inputs.environment || &#39;Test1&#39; }}
  cancel-in-progress: false

jobs:
  deploy:
    runs-on: ubuntu-latest
    env:
      DEPLOY_ENVIRONMENT: ${{ inputs.environment || &#39;Test1&#39; }}
      OPTIMIZELY_DXP_PROJECT_ID: ${{ secrets.OPTIMIZELY_DXP_PROJECT_ID }}
      OPTIMIZELY_DXP_CLIENT_KEY: ${{ secrets.OPTIMIZELY_DXP_CLIENT_KEY }}
      OPTIMIZELY_DXP_CLIENT_SECRET: ${{ secrets.OPTIMIZELY_DXP_CLIENT_SECRET }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npx optimizely-cms-cli deploy --env &quot;$DEPLOY_ENVIRONMENT&quot; --yes&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add the three &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;OPTIMIZELY_DXP_*&lt;/span&gt; values as repository secrets. Keep &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;cancel-in-progress: false&lt;/span&gt;. If you cancel the job, the deployment on the platform does not stop.&lt;/p&gt;
&lt;p&gt;I do not run &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;config:push&lt;/span&gt; in CI. It needs the CMS client secret in GitHub, and it changes the content model at each merge. I run it myself when a content type changes.&lt;/p&gt;
&lt;h2&gt;The prompt&lt;/h2&gt;
&lt;p&gt;Use this prompt with Claude Code to do the same setup. It contains all the fixes in this post. It has two rules. The agent does not handle your secrets, and it stops for you at each step in a portal. Change the folder and repository names. Then start Claude Code in an empty folder and paste the prompt.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Set up the Optimizely Stride sample site on a new Optimizely CMS (SaaS) instance.
Deploy the front end to Optimizely Frontend Hosting. Do one step at a time. Stop for
me at each step that needs the CMS UI, the DXP management portal or a secret.

Rules
- Do not ask me to paste keys, secrets or tokens into the chat. I put them in .env,
  in the portal or in GitHub. Do not print .env values. Only check that each one is set.
- Read the current sources before you use what you remember: the Stride template and
  @optimizely/cms-create-app in github.com/episerver/content-js-sdk, and the source of
  the CLI deploy command. Tell me if a step below does not agree with them.
- Commit after each step. Do not commit .env.

1. Create the project
   - Run: npx @optimizely/cms-create-app &amp;lt;folder&amp;gt; --template nextjs-stride --pm npm --ci none
   - Use npm. Frontend Hosting accepts only package-lock.json or yarn.lock.
   - If .gitignore or .env.example is missing, copy it from templates/stride.
     Make sure that .gitignore includes /node_modules/ and does not hide .env.example.
   - Add .gitattributes with &quot;* text=auto eol=lf&quot;.
   - Create a private GitHub repository &amp;lt;owner&amp;gt;/&amp;lt;name&amp;gt; and push.

2. Connect to the CMS
   - Create .env from .env.example. Tell me which values to add.
   - Tell me to give the CLI API key full access rights on the root (Settings &amp;gt;
     Set Access Rights, apply settings for all subitems).
   - Run npm run config:push.

3. Import the content
   - Tell me to import stride_content.episerverdata at Settings &amp;gt; Import Data. Use the
     root as the destination. Select &quot;Update existing content items with matching ID&quot;.
   - Errors about the &quot;Validators&quot; property of one form are not a problem.
   - Tell me to set the start page of the Stride application to the imported Home(2)
     page (Settings &amp;gt; Applications &amp;gt; Stride) and to delete the empty Home page.
   - In optimizely.config.mjs, remove the &quot;content&quot; array. Set the Stride entryPoint to
     the imported start page: &#39;cms://content/a8ff42c351e74f65a2f26223950c54ab&#39;. First
     make sure this GUID is the start page in stride_content.episerverdata.
   - Send a query to Graph with the single key. Make sure the pages are published and
     the URLs start with /en/ and not /en/home2/.

4. Run the site and check all pages
   - Start the dev server. If HTTPS needs &quot;mkcert -install&quot;, do not run it. Use http
     and tell me.
   - Get all page URLs from Graph. Request each page and show me the status codes.
   - If Graph reports unknown OptiForms*Element types, compare the types on the
     instance (npx optimizely-cms-cli config pull -r -j) with the types that
     initForms() registers. Replace initForms() with registration of only the Forms
     types that the instance has. Restart the dev server after the change.

5. Frontend Hosting
   - First do a test: npx optimizely-cms-cli deploy --output &amp;lt;temporary folder&amp;gt;.
     Make sure the zip file has no .env file and no node_modules folder.
   - Ask me which front-end environment is linked to the CMS environment that has
     the content, and for its hostname (Hostnames tab in the portal). In
     optimizely.config.mjs, make it the only host, with the type primary. Headless
     applications accept only Primary, Preview and Media hosts. Run config:push.
     Set APPLICATION_HOST in .env to https://&amp;lt;that hostname&amp;gt;.
   - Tell me to add APPLICATION_HOST as an App Setting for that environment. The platform
     gives OPTIMIZELY_CMS_URL and the Graph keys.
   - Run npm run deploy -- --env &amp;lt;that environment&amp;gt;. Then check the live pages and the preview
     from the CMS. If the published pages show 404 but preview works, the front-end
     environment is linked to a CMS environment that does not have the content.

6. GitHub Actions
   - Add a workflow that runs npx optimizely-cms-cli deploy when I push to main
     (Test1), and on a manual run (I select the environment). Set cancel-in-progress
     to false. Tell me to add the three OPTIMIZELY_DXP_* repository secrets.
     Do not run config:push in CI.

At the end, give me a short list: what is done, what you checked, and what is not done.&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Lessons&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Change &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;optimizely.config.mjs&lt;/span&gt; after the import. Each &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;config:push&lt;/span&gt; writes the start page and the hosts from this file.&lt;/li&gt;
&lt;li&gt;If one page works and the other pages show a 500 error, read the GraphQL errors. One unknown type stops the full query.&lt;/li&gt;
&lt;li&gt;Find out which CMS environment each front-end environment is linked to before you deploy. The platform supplies the CMS URL and the Graph keys, and you cannot change them in App Settings.&lt;/li&gt;
&lt;li&gt;Make &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;APPLICATION_HOST&lt;/span&gt; and the primary host the same.&lt;/li&gt;
&lt;li&gt;The deploy is fast on your computer and slow on the platform. The build, zip and upload took a few minutes. Most of the time is the build on the platform.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Claude Code read the SDK and CLI source in &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;node_modules&lt;/span&gt; to find the cause of each problem: the Forms registration, the preview tokens and the start page in &lt;span style=&quot;font-family: Consolas, Menlo, monospace;&quot;&gt;config:push&lt;/span&gt;. It sent queries to Graph to see which pages were published. It stopped at each step that needed me in a portal or needed a secret.&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/Minesh-Shah/Dates/2026/11/stride-on-saas-cms-and-frontend-hosting-with-claude-code/</guid>            <pubDate>Sat, 07 Nov 2026 21:42:54 GMT</pubDate>           <category>Blog post</category></item><item> <title>What changed in Optimizely LanguageManager 5.5.4</title>            <link>https://www.gulla.net/en/blog/what-changed-in-optimizely-languagemanager-5.5.4/</link>            <description>LanguageManager 5.5.4 for Optimizely CMS 12 has no public release notes. I compared it to 5.5.3 and found no new features, but an axios upgrade that fixes 36 known vulnerabilities.</description>            <guid>https://www.gulla.net/en/blog/what-changed-in-optimizely-languagemanager-5.5.4/</guid>            <pubDate>Sat, 07 Nov 2026 08:20:49 GMT</pubDate>           <category>Blog post</category></item><item> <title>Log your Optimizely Graph queries</title>            <link>https://krompaco.nu/2026/11/log-optimizely-graph-queries/</link>            <description>Log Graph calls from .NET in in a way where it&#39;s easy to copy the query and parameters to GraphiQL.</description>            <guid>https://krompaco.nu/2026/11/log-optimizely-graph-queries/</guid>            <pubDate>Fri, 06 Nov 2026 07:10:00 GMT</pubDate>           <category>Blog post</category></item><item> <title>Introducing Optimizely Feature Flagging: Flag Your Content Model</title>            <link>https://www.technicaldogsbody.com/blog/introducing-optimizely-feature-flagging</link>            <description>Feature flag Optimizely CMS content properties without code changes: visibility, validation, editors and whole content types, with any flag provider.</description>            <guid>https://www.technicaldogsbody.com/blog/introducing-optimizely-feature-flagging</guid>            <pubDate>Tue, 03 Nov 2026 11:30:18 GMT</pubDate>           <category>Blog post</category></item><item> <title>Introducing Optimizely Content Seeder: Seed Your CMS from Code</title>            <link>https://www.technicaldogsbody.com/blog/introducing-optimizely-content-seeder</link>            <description>Seed Optimizely CMS 12 and 13 with pages, blocks, media and Visual Builder sections from a fluent C# definition, with optional AI-written content.</description>            <guid>https://www.technicaldogsbody.com/blog/introducing-optimizely-content-seeder</guid>            <pubDate>Tue, 03 Nov 2026 11:30:18 GMT</pubDate>           <category>Blog post</category></item><item> <title>Optimizely DXP Alert: Are You Affected by the October 2026 Node.js Security Vulnerabilities?</title>            <link>https://madhuanbalagan.com/?p=5125</link>            <description>&lt;p&gt;On October 27, 2026, Optimizely published a security advisory regarding recently disclosed Node.js vulnerabilities affecting Node.js 22.x, 24.x, and 26.x release lines. Optimizely confirmed there&amp;#46;&amp;#46;&amp;#46;&lt;/p&gt;
&lt;p&gt;The post &lt;a href=&quot;https://madhuanbalagan.com/optimizely-dxp-alert-are-you-affected-by-the-october-2026-node-js-security-vulnerabilities&quot;&gt;Optimizely DXP Alert: Are You Affected by the October 2026 Node.js Security Vulnerabilities?&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;
</description>            <guid>https://madhuanbalagan.com/?p=5125</guid>            <pubDate>Mon, 02 Nov 2026 19:12:54 GMT</pubDate>           <category>Blog post</category></item><item> <title>Build Your First Opal Tool in C#</title>            <link>https://www.adnanzameer.com/2026/10/build-your-first-opal-tool-in-csharp.html</link>            <description>&lt;!--excerpt: Optimizely Opal is being rebranded to Agent Platform, personified as Mark, but the developer-facing SDK and docs still use Opal naming for now. This walkthrough shows how to build your first custom Opal tool in C# using the current Optimizely.Opal.Tools NuGet package, from a working tool method to registering it in your Opal instance.--&gt;

Optimizely ships a C# SDK for building custom</description>            <guid>https://www.adnanzameer.com/2026/10/build-your-first-opal-tool-in-csharp.html</guid>            <pubDate>Mon, 02 Nov 2026 11:14:19 GMT</pubDate>           <category>Blog post</category></item><item> <title>Introducing Publishing Timeline for Optimizely CMS 13</title>            <link>https://world.optimizely.com/blogs/mardy-abdelkareem/dates/2026/10/introducing-publishing-timeline-for-optimizely-cms-13/</link>            <description>&lt;p&gt;When working with scheduled content in Optimizely CMS, having a clear overview of what&#39;s going live and when can be really helpful, especially when managing content across multiple languages and timezones.&lt;/p&gt;
&lt;p&gt;Following my &lt;a style=&quot;color: #0066cc; text-decoration: underline;&quot; href=&quot;/link/ac89958d132547f9bf5ebfb16e4de0b4.aspx&quot;&gt;previous add-on for scheduling content across different timezones&lt;/a&gt;, I wanted to explore another part of the publishing experience: making it easier for editors to see what&#39;s coming up.&lt;/p&gt;
&lt;p&gt;That led me to build &lt;strong&gt;Publishing Timeline&lt;/strong&gt;, a free, open-source add-on for Optimizely CMS 13.&lt;/p&gt;
&lt;h2&gt;What does Publishing Timeline do?&lt;/h2&gt;
&lt;p&gt;Publishing Timeline adds a panel below the native &lt;strong&gt;Workflow Status&lt;/strong&gt; gadget on the CMS Dashboard. It appears when the &lt;strong&gt;Scheduled&lt;/strong&gt; tab is selected, without replacing or modifying the existing gadget.&lt;/p&gt;
&lt;p&gt;It provides two ways to explore upcoming publications.&lt;/p&gt;
&lt;h3&gt;Table view&lt;/h3&gt;
&lt;p&gt;The table provides a list of scheduled content, including the publishing date, language, and who last modified each item. Editors can filter the results and open content directly in the CMS editor.&lt;/p&gt;
&lt;figure style=&quot;max-width: 900px; margin: 20px auto; text-align: center;&quot;&gt;&lt;img style=&quot;display: block; width: 100%; height: auto; border: 1px solid #e2e2e2; border-radius: 6px;&quot; src=&quot;/link/98e878a2e8814d1ba6ae63a22a45cafb.aspx&quot; alt=&quot;Publishing Timeline table showing scheduled content, languages, publishing dates, modified by, and edit links.&quot; /&gt;
&lt;figcaption style=&quot;margin-top: 8px; color: #666; font-size: 13px;&quot;&gt;Publishing Timeline &amp;mdash; Table view&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h3&gt;Calendar view&lt;/h3&gt;
&lt;p&gt;The calendar displays scheduled publications in a monthly view, making it easier to see what&#39;s planned for a particular day or across the month. When multiple items are scheduled for the same day, the &lt;strong&gt;+N more&lt;/strong&gt; option opens a popup showing additional publications and their scheduled times.&lt;/p&gt;
&lt;figure style=&quot;max-width: 900px; margin: 20px auto; text-align: center;&quot;&gt;&lt;img style=&quot;display: block; width: 100%; height: auto; border: 1px solid #e2e2e2; border-radius: 6px;&quot; src=&quot;/link/d59c9a8a43b645e29e968324710c4e68.aspx&quot; alt=&quot;Publishing Timeline monthly calendar showing scheduled publications and a popup with additional scheduled items.&quot; /&gt;
&lt;figcaption style=&quot;margin-top: 8px; color: #666; font-size: 13px;&quot;&gt;Publishing Timeline &amp;mdash; Calendar view with additional scheduled items&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Both views share the same language, date, and timezone filters, so editors can switch between them without losing their selections.&lt;/p&gt;
&lt;h2&gt;Working across timezones&lt;/h2&gt;
&lt;p&gt;One of the features I wanted to include was the ability to view scheduled publishing times in different timezones.&lt;/p&gt;
&lt;p&gt;For example, if you&#39;re coordinating content across Cairo, London, and New York, you can switch the timeline to see when each item is scheduled to go live in the selected timezone.&lt;/p&gt;
&lt;p&gt;This only changes how the publishing times are displayed. The actual publishing schedule remains unchanged.&lt;/p&gt;
&lt;h2&gt;Getting started&lt;/h2&gt;
&lt;p&gt;Publishing Timeline supports &lt;strong&gt;Optimizely CMS 13.4.0 and later (below 14.0.0)&lt;/strong&gt; and .NET 10.&lt;/p&gt;
&lt;p&gt;The add-on is now available on the &lt;a style=&quot;color: #0066cc; text-decoration: underline;&quot; href=&quot;https://nuget.optimizely.com/packages/mardy.optimizely.publishingtimeline/1.0.0&quot;&gt;Optimizely NuGet feed&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Install the package using the .NET CLI:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dotnet add package Mardy.Optimizely.PublishingTimeline --version 1.0.0&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Make sure the Optimizely NuGet feed is configured as a package source in your project or development environment.&lt;/p&gt;
&lt;p&gt;No additional startup configuration is required. After installation, open the CMS Dashboard and navigate to &lt;strong&gt;Workflow Status &amp;rarr; Scheduled&lt;/strong&gt;. The Publishing Timeline panel will appear below the native gadget.&lt;/p&gt;
&lt;p&gt;The add-on is read-only and doesn&#39;t change how Optimizely schedules or publishes content.&lt;/p&gt;
&lt;h2&gt;Source code and feedback&lt;/h2&gt;
&lt;p&gt;The project is open source under the MIT license.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a style=&quot;color: #0066cc; text-decoration: underline;&quot; href=&quot;https://github.com/mardy-awd/Optimizely.PublishingTimeline&quot;&gt;Mardy.Optimizely.PublishingTimeline&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;NuGet package:&lt;/strong&gt; &lt;a style=&quot;color: #0066cc; text-decoration: underline;&quot; href=&quot;https://nuget.optimizely.com/packages/mardy.optimizely.publishingtimeline/1.0.0&quot;&gt;Optimizely NuGet feed (v1.0.0)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I&#39;d be happy to hear your feedback or ideas for future improvements!&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/mardy-abdelkareem/dates/2026/10/introducing-publishing-timeline-for-optimizely-cms-13/</guid>            <pubDate>Mon, 02 Nov 2026 09:36:42 GMT</pubDate>           <category>Blog post</category></item><item> <title>What else changed in Optimizely CMS 13.5.1</title>            <link>https://www.gulla.net/en/blog/what-else-changed-in-optimizely-cms-13.5.1/</link>            <description>Optimizely CMS 13.5.1 lists a single Graph fix in its release notes. I decompiled the packages and found more: a new setting that lets editors find expired content, and more!</description>            <guid>https://www.gulla.net/en/blog/what-else-changed-in-optimizely-cms-13.5.1/</guid>            <pubDate>Sun, 01 Nov 2026 10:57:23 GMT</pubDate>           <category>Blog post</category></item><item> <title>New logo and login page in Optimizely CMS 13.5</title>            <link>https://www.gulla.net/en/blog/new-logo-and-login-page-in-optimizely-cms-13.5/</link>            <description>CMS 13.5.0 quietly ships a new logo and a redesigned login page.</description>            <guid>https://www.gulla.net/en/blog/new-logo-and-login-page-in-optimizely-cms-13.5/</guid>            <pubDate>Fri, 30 Oct 2026 19:56:21 GMT</pubDate>           <category>Blog post</category></item><item> <title>Is your website agent-ready? Using Cloudflare&#39;s readiness tool to assess your website</title>            <link>https://world.optimizely.com/blogs/scott-reed/dates/2026/10/assessing-agent-readiness-with-cloudflares-tools/</link>            <description>&lt;p&gt;One of my friends and fellow MVPs, Minesh Shah recently blogged about optimising one of his websites for full AX and agent readiness &lt;a href=&quot;https://www.linkedin.com/pulse/designing-ax-what-agent-ready-website-actually-needs-minesh-shah-woi4e/&quot;&gt;https://www.linkedin.com/pulse/designing-ax-what-agent-ready-website-actually-needs-minesh-shah-woi4e/&lt;/a&gt; and this led me to have a look at some of my own sites and also to re-explore what tools are available. I found this amazing Cloudflare tool that&#39;s been around for a number of months. It can really help guide the path between assessment of agent readiness and action, as it also generates AI prompts to help you solve the gaps.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;In this blog post, I&#39;ll go through a little bit about agent readiness, and also how to use the tool and how to connect it into your AI harness using the skill (it has an MCP too)&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;Part one: the visitor you didn&#39;t design for&lt;/h2&gt;
&lt;p&gt;For twenty-five years we have built websites for two audiences: people, and the search engines that send people to us. Every navigation decision, every page template and every SEO checklist assumes that, sooner or later, a human being is going to look at the page.&lt;/p&gt;
&lt;p&gt;That assumption is breaking. A growing share of the research, comparison and shortlisting that used to happen on our sites now happens inside an AI assistant. A customer asks ChatGPT, Claude, Gemini or Copilot to compare three products, summarise a returns policy or find a supplier that meets a specification. The assistant goes out, reads the web on their behalf, and comes back with an answer. Sometimes that answer includes a link to you. Often the customer acts on it without ever seeing your homepage.&lt;/p&gt;
&lt;p&gt;In &lt;a href=&quot;/link/b433cd48d8324a928140106f38b7c85a.aspx&quot;&gt;my last post on WebMCP&lt;/a&gt; I looked at the far end of this journey: giving agents structured actions they can take on your site. This post is about the step that comes before any of that. Before an agent can &lt;em&gt;act&lt;/em&gt; on your site, it has to be able to &lt;em&gt;find&lt;/em&gt; it, &lt;em&gt;read&lt;/em&gt; it and &lt;em&gt;understand what it is allowed to do with it&lt;/em&gt;. That is what being agent-ready means, and most sites, including plenty of well-built Optimizely sites, are not there yet.&lt;/p&gt;
&lt;h3&gt;Why this is a leadership conversation, not just a developer ticket&lt;/h3&gt;
&lt;p&gt;It is tempting to file this under &quot;technical SEO&quot; and hand it to the development team. That undersells it. Agent-readiness raises questions that belong with marketing, digital and legal leadership as much as with engineering:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Do we want AI assistants to recommend us?&lt;/strong&gt; If so, the crawlers that feed AI answers need to be able to reach and read our content.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Are we happy for our content to train AI models?&lt;/strong&gt; That is a separate decision from appearing in AI answers, and most organisations have never made it explicitly.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What does an agent see when it reads our product and service pages?&lt;/strong&gt; If it can only parse half the page, it will fill the gaps from somewhere else, possibly a competitor or an out-of-date review.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Who owns this?&lt;/strong&gt; Today, in most organisations, the honest answer is &quot;nobody&quot;, which means the decision is being made by default settings.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The sites that will do well in the next few years are the ones that make these choices deliberately rather than discovering them by accident.&lt;/p&gt;
&lt;h3&gt;The business benefits of getting it right&lt;/h3&gt;
&lt;div style=&quot;margin: 16px 0 8px 0;&quot;&gt;
&lt;div style=&quot;display: inline-block; vertical-align: top; box-sizing: border-box; width: 23%; min-width: 190px; min-height: 190px; margin: 0 1% 16px 0; padding: 16px; background: #F0F0E6; border-top: 4px solid #FFE628; border-radius: 6px;&quot;&gt;
&lt;p style=&quot;margin: 0 0 6px 0; font-size: 13px; font-weight: bold; letter-spacing: 2px; color: #87878a;&quot;&gt;01&lt;/p&gt;
&lt;p style=&quot;margin: 0 0 8px 0; font-size: 19px; line-height: 1.25; font-weight: bold; color: #16171a;&quot;&gt;Be found&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.5; color: #444;&quot;&gt;Appear in the AI answers where customers now research and shortlist.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;display: inline-block; vertical-align: top; box-sizing: border-box; width: 23%; min-width: 190px; min-height: 190px; margin: 0 1% 16px 0; padding: 16px; background: #F0F0E6; border-top: 4px solid #FFE628; border-radius: 6px;&quot;&gt;
&lt;p style=&quot;margin: 0 0 6px 0; font-size: 13px; font-weight: bold; letter-spacing: 2px; color: #87878a;&quot;&gt;02&lt;/p&gt;
&lt;p style=&quot;margin: 0 0 8px 0; font-size: 19px; line-height: 1.25; font-weight: bold; color: #16171a;&quot;&gt;Be described accurately&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.5; color: #444;&quot;&gt;Content agents can read means they get your story right.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;display: inline-block; vertical-align: top; box-sizing: border-box; width: 23%; min-width: 190px; min-height: 190px; margin: 0 1% 16px 0; padding: 16px; background: #F0F0E6; border-top: 4px solid #FFE628; border-radius: 6px;&quot;&gt;
&lt;p style=&quot;margin: 0 0 6px 0; font-size: 13px; font-weight: bold; letter-spacing: 2px; color: #87878a;&quot;&gt;03&lt;/p&gt;
&lt;p style=&quot;margin: 0 0 8px 0; font-size: 19px; line-height: 1.25; font-weight: bold; color: #16171a;&quot;&gt;Own your AI policy&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.5; color: #444;&quot;&gt;Decide search, AI answers and training separately, in machine-readable form.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;display: inline-block; vertical-align: top; box-sizing: border-box; width: 23%; min-width: 190px; min-height: 190px; margin: 0 1% 16px 0; padding: 16px; background: #F0F0E6; border-top: 4px solid #FFE628; border-radius: 6px;&quot;&gt;
&lt;p style=&quot;margin: 0 0 6px 0; font-size: 13px; font-weight: bold; letter-spacing: 2px; color: #87878a;&quot;&gt;04&lt;/p&gt;
&lt;p style=&quot;margin: 0 0 8px 0; font-size: 19px; line-height: 1.25; font-weight: bold; color: #16171a;&quot;&gt;Serve agents for less&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.5; color: #444;&quot;&gt;Lightweight Markdown is cheaper to serve and easier to use accurately.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;display: inline-block; vertical-align: top; box-sizing: border-box; width: 23%; min-width: 190px; min-height: 190px; margin: 0 1% 16px 0; padding: 16px; background: #F0F0E6; border-top: 4px solid #FFE628; border-radius: 6px;&quot;&gt;
&lt;p style=&quot;margin: 0 0 6px 0; font-size: 13px; font-weight: bold; letter-spacing: 2px; color: #87878a;&quot;&gt;05&lt;/p&gt;
&lt;p style=&quot;margin: 0 0 8px 0; font-size: 19px; line-height: 1.25; font-weight: bold; color: #16171a;&quot;&gt;Ready for agentic commerce&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.5; color: #444;&quot;&gt;Discovery and readability come before any agent can check out.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;display: inline-block; vertical-align: top; box-sizing: border-box; width: 23%; min-width: 190px; min-height: 190px; margin: 0 1% 16px 0; padding: 16px; background: #F0F0E6; border-top: 4px solid #FFE628; border-radius: 6px;&quot;&gt;
&lt;p style=&quot;margin: 0 0 6px 0; font-size: 13px; font-weight: bold; letter-spacing: 2px; color: #87878a;&quot;&gt;06&lt;/p&gt;
&lt;p style=&quot;margin: 0 0 8px 0; font-size: 19px; line-height: 1.25; font-weight: bold; color: #16171a;&quot;&gt;Measure progress&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.5; color: #444;&quot;&gt;A 0&amp;ndash;5 readiness score leadership can baseline, target and track.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;display: inline-block; vertical-align: top; box-sizing: border-box; width: 23%; min-width: 190px; min-height: 190px; margin: 0 1% 16px 0; padding: 16px; background: #F0F0E6; border-top: 4px solid #FFE628; border-radius: 6px;&quot;&gt;
&lt;p style=&quot;margin: 0 0 6px 0; font-size: 13px; font-weight: bold; letter-spacing: 2px; color: #87878a;&quot;&gt;07&lt;/p&gt;
&lt;p style=&quot;margin: 0 0 8px 0; font-size: 19px; line-height: 1.25; font-weight: bold; color: #16171a;&quot;&gt;Quick wins first&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.5; color: #444;&quot;&gt;The early levels are mostly configuration, not new development.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;display: inline-block; vertical-align: top; box-sizing: border-box; width: 23%; min-width: 190px; min-height: 190px; margin: 0 1% 16px 0; padding: 16px; background: #0F0F14; border-top: 4px solid #FFE628; border-radius: 6px;&quot;&gt;
&lt;p style=&quot;margin: 0 0 6px 0; font-size: 13px; font-weight: bold; letter-spacing: 2px; color: #ffe628;&quot;&gt;&amp;rarr;&lt;/p&gt;
&lt;p style=&quot;margin: 0 0 8px 0; font-size: 19px; line-height: 1.25; font-weight: bold; color: #ffe628;&quot;&gt;Start today&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.5; color: #e7e7e8;&quot;&gt;Scan your site in under a minute and share the result internally.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3&gt;Treat it like mobile-friendly in 2012&lt;/h3&gt;
&lt;p&gt;There is a useful parallel with the shift to mobile. For a few years, a mobile-friendly site was a differentiator. Then it became table stakes, and the sites that hadn&#39;t adapted were quietly penalised. Agent-readiness is on the same curve, and we are early on it. The standards are still settling and the scoring will change, but the direction is clear. The cost of starting now is low; the cost of starting late is being invisible to a growing share of your customers&#39; journeys.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Part two: how to measure it and fix it&lt;/h2&gt;
&lt;p&gt;The good news is that you no longer have to guess where you stand. Two tools make this practical.&lt;/p&gt;
&lt;h3&gt;Step 1: Scan your site with isitagentready.com&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://isitagentready.com/&quot;&gt;Is It Agent Ready?&lt;/a&gt; is a free scanner from Cloudflare. Enter a URL and it runs a set of checks against the emerging standards for AI agents, grouped into five categories:&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin: 12px 0 20px 0; font-size: 15px;&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;th style=&quot;border: 1px solid #d0d7de; padding: 8px 10px; background: #f7f7f5; text-align: left;&quot;&gt;Category&lt;/th&gt;
&lt;th style=&quot;border: 1px solid #d0d7de; padding: 8px 10px; background: #f7f7f5; text-align: left;&quot;&gt;What it looks at&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #d0d7de; padding: 8px 10px; vertical-align: top;&quot;&gt;Discoverability&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d0d7de; padding: 8px 10px; vertical-align: top;&quot;&gt;robots.txt, sitemap, Link response headers, DNS for AI Discovery (DNS-AID)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #d0d7de; padding: 8px 10px; vertical-align: top;&quot;&gt;Content accessibility&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d0d7de; padding: 8px 10px; vertical-align: top;&quot;&gt;Whether the site returns Markdown when an agent asks for it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #d0d7de; padding: 8px 10px; vertical-align: top;&quot;&gt;Bot access control&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d0d7de; padding: 8px 10px; vertical-align: top;&quot;&gt;AI bot rules in robots.txt, Content Signals, Web Bot Auth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #d0d7de; padding: 8px 10px; vertical-align: top;&quot;&gt;API, auth, MCP and skill discovery&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d0d7de; padding: 8px 10px; vertical-align: top;&quot;&gt;API Catalog, OAuth discovery, MCP Server Card, A2A Agent Card, Agent Skills index, WebMCP and related standards&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #d0d7de; padding: 8px 10px; vertical-align: top;&quot;&gt;Commerce (scored only for e-commerce sites)&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d0d7de; padding: 8px 10px; vertical-align: top;&quot;&gt;Agentic payment and commerce protocols such as x402, MPP, UCP and ACP&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The result is a readiness level from 0 to 5:&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin: 12px 0 20px 0; font-size: 15px;&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;th style=&quot;border: 1px solid #d0d7de; padding: 8px 10px; background: #f7f7f5; text-align: left;&quot;&gt;Level&lt;/th&gt;
&lt;th style=&quot;border: 1px solid #d0d7de; padding: 8px 10px; background: #f7f7f5; text-align: left;&quot;&gt;Name&lt;/th&gt;
&lt;th style=&quot;border: 1px solid #d0d7de; padding: 8px 10px; background: #f7f7f5; text-align: left;&quot;&gt;What it takes&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #d0d7de; padding: 8px 10px; vertical-align: top;&quot;&gt;0&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d0d7de; padding: 8px 10px; vertical-align: top;&quot;&gt;Not Ready&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d0d7de; padding: 8px 10px; vertical-align: top;&quot;&gt;Fails the basic checks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #d0d7de; padding: 8px 10px; vertical-align: top;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d0d7de; padding: 8px 10px; vertical-align: top;&quot;&gt;Basic Web Presence&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d0d7de; padding: 8px 10px; vertical-align: top;&quot;&gt;Two of: robots.txt, sitemap, Link headers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #d0d7de; padding: 8px 10px; vertical-align: top;&quot;&gt;2&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d0d7de; padding: 8px 10px; vertical-align: top;&quot;&gt;Bot-Aware&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d0d7de; padding: 8px 10px; vertical-align: top;&quot;&gt;Level 1, plus AI bot rules and Content Signals in robots.txt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #d0d7de; padding: 8px 10px; vertical-align: top;&quot;&gt;3&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d0d7de; padding: 8px 10px; vertical-align: top;&quot;&gt;Agent-Readable&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d0d7de; padding: 8px 10px; vertical-align: top;&quot;&gt;Level 2, plus Markdown content negotiation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #d0d7de; padding: 8px 10px; vertical-align: top;&quot;&gt;4&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d0d7de; padding: 8px 10px; vertical-align: top;&quot;&gt;Agent-Integrated&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d0d7de; padding: 8px 10px; vertical-align: top;&quot;&gt;Level 3, plus one of: MCP Server Card, A2A Agent Card, Agent Skills index, API Catalog&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #d0d7de; padding: 8px 10px; vertical-align: top;&quot;&gt;5&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d0d7de; padding: 8px 10px; vertical-align: top;&quot;&gt;Agent-Native&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #d0d7de; padding: 8px 10px; vertical-align: top;&quot;&gt;Level 4, plus two of: Web Bot Auth, all integrations, auth metadata&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The most useful part of the report is the &quot;how to implement&quot; section: for each failing check it gives you a ready-made prompt to paste into a coding agent such as Claude Code, Cursor or GitHub Copilot. As Cloudflare points out on the page, these are AI-generated recommendations, so treat them as a starting point and review them like any other change.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;My advice: run it against your production site today.&lt;/strong&gt; It takes under a minute, and the result is usually a useful conversation starter with stakeholders.&lt;/p&gt;
&lt;h3&gt;Step 2: Bring it into your workflow with the agent-ready-cloudflare skill&lt;/h3&gt;
&lt;p&gt;The web scanner is great for a one-off check. For ongoing work, and especially if you look after more than one site, the &lt;a href=&quot;https://github.com/fabricioctelles/skills/tree/main/skills/agent-ready-cloudflare&quot;&gt;agent-ready-cloudflare skill&lt;/a&gt; is the more powerful option. It is a community-published Agent Skill that wraps the same scanner so you can drive it from Claude (or any agent that supports skills).&lt;/p&gt;
&lt;p&gt;What it gives you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Scanning from your agent.&lt;/strong&gt; Ask &quot;scan example.com for agent readiness&quot; and the skill calls the scanner&#39;s API and produces a structured Markdown report: your level, a pass/fail summary per category, and what you need for the next level.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fix prompts for every failing check.&lt;/strong&gt; The scanner&#39;s API only returns an implementation prompt for the next level up. The skill includes templates for all the checks, so its report rebuilds the full set of web-UI style prompts (goal, issue, fix, links to the relevant RFCs and specs) for everything that fails.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;25 implementation sub-skills.&lt;/strong&gt; Each standard (robots.txt, Content Signals, Markdown negotiation, Link headers, API Catalog, MCP Server Card, Agent Skills index, WebMCP and so on) has its own short guide covering requirements, the Cloudflare shortcut where one exists, and how to validate.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Batch scanning.&lt;/strong&gt; It documents how to scan a list of domains with a polite delay between requests, which is ideal for agencies, multi-brand groups or anyone running several Optimizely sites.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The scanner is also exposed as an MCP server (https://isitagentready.com/mcp, with a scan_site tool), so you can connect it directly to an MCP-capable agent if you prefer.&lt;/p&gt;
&lt;p&gt;A practical workflow looks like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Install the skill in Claude.&lt;/li&gt;
&lt;li&gt;Ask it to scan each of your sites and save the reports.&lt;/li&gt;
&lt;li&gt;Pick the target level for each site (Level 3 is a sensible first goal).&lt;/li&gt;
&lt;li&gt;Hand the generated fix prompts to your development team, or to a coding agent working in your solution, and review the output.&lt;/li&gt;
&lt;li&gt;Re-scan to confirm the checks now pass, and repeat on a regular cadence.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Start today&lt;/h3&gt;
&lt;p&gt;Run the scan, share the result, and agree who owns your AI content policy. The report and the skill will tell you exactly what to fix; the harder and more valuable work is deciding what you want agents to be able to do with your site.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;How can Niteco help you?&lt;/h2&gt;
&lt;p&gt;Niteco is the largest global certified Optimizely CMS and Commerce partner, with deep expertise in the Optimizely Agent Platform.&lt;/p&gt;
&lt;div style=&quot;margin: 20px 0 8px 0;&quot;&gt;
&lt;div style=&quot;display: inline-block; vertical-align: top; box-sizing: border-box; width: 23%; min-width: 190px; margin: 0 1% 16px 0; padding: 16px; background: #f7f7f5; border-top: 4px solid #F8D749; border-radius: 4px;&quot;&gt;
&lt;p style=&quot;margin: 0 0 4px 0; font-size: 44px; line-height: 1.1; font-weight: 300; color: #16171a;&quot;&gt;#1&lt;/p&gt;
&lt;p style=&quot;margin: 0 0 10px 0; font-size: 13px; font-weight: bold; letter-spacing: 3px; text-transform: uppercase; color: #16171a;&quot;&gt;Optimizely&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.5; color: #444;&quot;&gt;The largest global partner. Powered by technical excellence and innovation. Accelerated by AI.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;display: inline-block; vertical-align: top; box-sizing: border-box; width: 23%; min-width: 190px; margin: 0 1% 16px 0; padding: 16px; background: #f7f7f5; border-top: 4px solid #F8D749; border-radius: 4px;&quot;&gt;
&lt;p style=&quot;margin: 0 0 4px 0; font-size: 44px; line-height: 1.1; font-weight: 300; color: #16171a;&quot;&gt;400+&lt;/p&gt;
&lt;p style=&quot;margin: 0 0 10px 0; font-size: 13px; font-weight: bold; letter-spacing: 3px; text-transform: uppercase; color: #16171a;&quot;&gt;AI-trained experts&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.5; color: #444;&quot;&gt;Certified by leading platforms like Optimizely, Microsoft, Contentful &amp;amp; Adobe.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;display: inline-block; vertical-align: top; box-sizing: border-box; width: 23%; min-width: 190px; margin: 0 1% 16px 0; padding: 16px; background: #f7f7f5; border-top: 4px solid #F8D749; border-radius: 4px;&quot;&gt;
&lt;p style=&quot;margin: 0 0 4px 0; font-size: 44px; line-height: 1.1; font-weight: 300; color: #16171a;&quot;&gt;2000+&lt;/p&gt;
&lt;p style=&quot;margin: 0 0 10px 0; font-size: 13px; font-weight: bold; letter-spacing: 3px; text-transform: uppercase; color: #16171a;&quot;&gt;Successful projects&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.5; color: #444;&quot;&gt;We have vast experience from years of delivering digital platforms for clients in diverse sectors.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;display: inline-block; vertical-align: top; box-sizing: border-box; width: 23%; min-width: 190px; margin: 0 1% 16px 0; padding: 16px; background: #f7f7f5; border-top: 4px solid #F8D749; border-radius: 4px;&quot;&gt;
&lt;p style=&quot;margin: 0 0 4px 0; font-size: 44px; line-height: 1.1; font-weight: 300; color: #16171a;&quot;&gt;9&lt;/p&gt;
&lt;p style=&quot;margin: 0 0 10px 0; font-size: 13px; font-weight: bold; letter-spacing: 3px; text-transform: uppercase; color: #16171a;&quot;&gt;Optimizely MVPs&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.5; color: #444;&quot;&gt;Largest team with OMVPs. Meaning the best &amp;amp; most experienced Optimizely minds will work with you.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;As Optimizely&#39;s largest certified partner in Optimizely CMS, Commerce, and the agent platform, we can help you with any of your strategic initiatives globally around content, commerce, or your organisational AI.&lt;/p&gt;
&lt;p&gt;If you would like help making your Optimizely site agent-ready, get in touch with us at Niteco: &lt;a href=&quot;https://niteco.com/contact-us/&quot;&gt;https://niteco.com/contact-us/&lt;/a&gt;&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/scott-reed/dates/2026/10/assessing-agent-readiness-with-cloudflares-tools/</guid>            <pubDate>Thu, 29 Oct 2026 17:55:56 GMT</pubDate>           <category>Blog post</category></item><item> <title>Visual Builder without Next.js, Part 1: What Astro Has to Rebuild on CMS 13 PaaS</title>            <link>https://world.optimizely.com/blogs/piotr-nowak---optimizely--azure/dates/2026/10/visual-builder-without-next.js-part-1-what-astro-has-to-rebuild-on-cms-13-paas</link>            <description>&lt;p&gt;&lt;em&gt;Series:&lt;/em&gt; &lt;strong&gt;Part 1: What Astro Has to Rebuild on CMS 13 PaaS&lt;/strong&gt; &amp;middot; Part 2: Does It Survive DXP? (Redis-backed ISR, Graph webhooks, Cloudflare and preview at scale)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Status:&lt;/strong&gt; built and tested in October 2026 against an Alloy-based CMS 13.4.2, then 13.5.0, with Optimizely Graph, &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;@optimizely/cms-sdk&lt;/span&gt; and &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;@optimizely/cms-cli&lt;/span&gt; 3.1.0-beta.0, Astro 7.4.2 and Node 24.19.0. The three problems that cost the most were reproduced again on 29 October against the packages as published on npm. Everything here is either measured, read in the SDK&#39;s source and guides, or a design choice; where it matters, the text says which. The code, the checks and the CMS setup notes are in the companion repository, &lt;a href=&quot;https://github.com/pino-labs/astro-visual-builder-cms13&quot;&gt;pino-labs/astro-visual-builder-cms13&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;The question&lt;/h2&gt;
&lt;p&gt;Do we have to pick Next.js to get Visual Builder? No. Astro renders it. But rendering was the easy part. Taking Next.js away also takes away what it quietly does around the SDK, and each of those pieces has to be supplied by hand: who owns the content model, how a composition becomes HTML, how a draft reaches the preview frame, which application a path belongs to, and how one editor&#39;s preview stays out of a visitor&#39;s page.&lt;/p&gt;
&lt;p&gt;&quot;Without Next.js&quot; does not mean without React: the demo deliberately mixes &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;.astro&lt;/span&gt;, React and Svelte components behind one registry. This part is about what had to be rebuilt. Part 2 asks whether the same architecture survives production on DXP: Redis-backed ISR, Graph webhooks, Cloudflare and preview at scale.&lt;/p&gt;
&lt;h2&gt;What changed, and what I found in practice&lt;/h2&gt;
&lt;p&gt;Two releases opened the door. On 31 August Optimizely added Astro to DXP front-end hosting, and on 24 September the CMS (SaaS) release notes for JavaScript SDK 3.0 called the SDK core framework-neutral and published an Astro sample. The SDK&#39;s README still positions React and Next.js as its first-class integrations; what the core offers Astro is primitives, not an integration.&lt;/p&gt;
&lt;p&gt;The version boundary matters in practice. At the time of testing, the framework-neutral entry point, &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;@optimizely/cms-sdk/core&lt;/span&gt;, and the &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;platform: &#39;paas&#39;&lt;/span&gt; option were only available in the 3.1.0 beta: &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;latest&lt;/span&gt; is 3.0.2, and its package exports neither. In the packages I inspected, both first appear in 3.1.0-beta.0, published on 20 October under the &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;beta&lt;/span&gt; tag. The Astro sample takes the SDK from its own monorepo (&lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;workspace:*&lt;/span&gt;). Copy it into a project with 3.0.2 and its Svelte route, which is built on &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;@optimizely/cms-sdk/core&lt;/span&gt;, does not resolve, because 3.0.2 does not export that entry point. Everything below runs on the beta, pinned to the exact version; when 3.1.0 ships, re-run the checks before you upgrade.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Before you start:&lt;/strong&gt; CMS 13 PaaS with Graph, 13.4.2 or later, and Node 22.23.4 or later on the 22 line, or Node 24 or 26 (Astro 7.4.0 raised the floor to 22.22.0, and the 7.4.2 patch to 22.23.4). The 13.4.2 minimum is not an Astro requirement: 13.4.1 fixes a culture-specific data-loss bug (CMS-57780), and the 13.4.2 database update restores the values lost before the fix. I tested on CMS 13.4.2 and 13.5.0 with Astro 7.4.2 on Node 24.19.0. DXP front-end hosting is needed only for the production checks in part 2. This is not a CMS 12 tutorial: the implementation depends on the CMS 13 REST API and the CMS 13 Graph schema.&lt;/p&gt;
&lt;h2&gt;The architecture&lt;/h2&gt;
&lt;p&gt;CMS 13 owns composition: editors arrange sections, rows, columns and elements in Visual Builder and pick display templates. Graph serves that composition as data. Astro turns it into HTML through one component registry, where &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;.astro&lt;/span&gt;, React and Svelte components sit side by side and render on the server; only components marked as islands (&lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;client:*&lt;/span&gt;) are hydrated in the browser.&lt;/p&gt;
&lt;figure style=&quot;margin: 24px auto; text-align: center; max-width: 800px;&quot;&gt;&lt;img style=&quot;max-width: 100%; height: auto; border: 1px solid #e1e4e8; border-radius: 6px;&quot; src=&quot;https://pino-labs.com/blog/visual-builder-without-nextjs-part-1/architecture-light.svg&quot; alt=&quot;Figure 1: The architecture&quot; /&gt;
&lt;figcaption style=&quot;font-size: 13px; color: #666; margin-top: 6px;&quot;&gt;&lt;em&gt;Figure 1. CMS 13 owns the composition, Graph serves it as data, Astro renders it through one registry. Only components marked as islands are hydrated in the browser.&lt;/em&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h3&gt;Who owns the content model?&lt;/h3&gt;
&lt;p&gt;This is the first architectural decision, and CMS 13 gives you two answers. A content type can be owned by C# and mirrored into TypeScript, or owned by TypeScript and pushed into the CMS.&lt;/p&gt;
&lt;figure style=&quot;margin: 24px auto; text-align: center; max-width: 800px;&quot;&gt;&lt;img style=&quot;max-width: 100%; height: auto; border: 1px solid #e1e4e8; border-radius: 6px;&quot; src=&quot;https://pino-labs.com/blog/visual-builder-without-nextjs-part-1/who-owns-the-type-light.svg&quot; alt=&quot;Figure 2: Who owns the content model&quot; /&gt;
&lt;figcaption style=&quot;font-size: 13px; color: #666; margin-top: 6px;&quot;&gt;&lt;em&gt;Figure 2. Two directions: C# owns the type and &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;config pull&lt;/span&gt; mirrors it, or TypeScript owns the type and &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;config push&lt;/span&gt; creates it. One source of truth per type.&lt;/em&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Both work. Pick one per type. The CMS REST API treats code-managed types as read-only: &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;config pull&lt;/span&gt; gets them, and generates typed files for them, only with &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;--includeReadOnly&lt;/span&gt;, and the CMS rejects a push that includes one. My recommendation after testing both models: keep legacy page types in C#, model everything Visual Builder needs (experiences, sections, elements, display templates) in the frontend repository, and regenerate the TypeScript mirror of the C# types with &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;config pull&lt;/span&gt;. In CLI 3.1.0-beta.0 that has to happen in a terminal (ask me how I know): without one, the CLI prints the manifest as JSON and writes no files, even with &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;--output&lt;/span&gt;. The fix (CMS-57658) is on the SDK&#39;s &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;main&lt;/span&gt; branch, so with the next CLI release CI can regenerate the mirror too.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;One source of truth per type, never two.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;On the Alloy-based CMS I tested against, the rule was not theoretical. Alloy ships a C# &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;ArticlePage&lt;/span&gt;, the same name as the TypeScript &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;ArticlePage&lt;/span&gt;, so I renamed the C# type to &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;AlloyArticlePage&lt;/span&gt; before the first push. Same GUID, content untouched.&lt;/p&gt;
&lt;p&gt;Optimizely&#39;s FAQ for CMS 13.0.0 recommends managing types where they are primarily used, names .NET code as the typical case for PaaS headless, and calls full PaaS headless support an area of ongoing development. API-managed Visual Builder types worked in the CMS 13 setup I tested; the broader PaaS headless story is still evolving. Two PaaS gotchas are easy to miss. &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;OPTIMIZELY_CMS_URL&lt;/span&gt; is the runtime and preview URL, while the CLI talks to the CMS REST API through &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;OPTIMIZELY_CMS_API_URL&lt;/span&gt; (or &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;--host&lt;/span&gt;). And the token the CMS issues to the CLI&#39;s OAuth client carries the client id and the &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;api:admin&lt;/span&gt; scope, but no roles (decoded on 13.4.2, with a client defined in configuration): enough for content types, display templates and the application, but &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;config push&lt;/span&gt; also creates the application&#39;s start content, which is checked against the content ACL. On a root that grants Everyone only Read, the push stops with a 403 until the client gets Read and Create there. Everything on the CMS side (the Graph instance, the OAuth client and its access, the Headless application and its preview URL) is in the repository&#39;s &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;web/docs/cms13-setup.md&lt;/span&gt;; the code paths below are relative to &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;web/&lt;/span&gt;.&lt;/p&gt;
&lt;h3&gt;Configure the SDK for PaaS&lt;/h3&gt;
&lt;pre style=&quot;background: #f6f8fa; border: 1px solid #e1e4e8; border-radius: 6px; padding: 16px; overflow: auto; font-family: Consolas,Monaco,monospace; font-size: 13px; line-height: 1.5;&quot;&gt;// src/lib/optimizely.ts (excerpt)
  config({
    apiKey: OPTIMIZELY_GRAPH_SINGLE_KEY,
    graphUrl: OPTIMIZELY_GRAPH_GATEWAY, // a URL with no path gets /content/v2 appended
    platform: &#39;paas&#39;, // CMS 13 PaaS; the SDK does not detect the product. Needs SDK &amp;gt;= 3.1.0-beta.0.
    fragment: { richTextFormat: &#39;both&#39; }, // 3.x fetches json only by default
    // Path lookups also match _metadata.url.base. Without it every application on the CMS answers for
    // the same path, and the first hit may be another site&#39;s start page. The value is the Headless
    // application&#39;s primary host as Graph stores it, scheme included (https://localhost:3001).
    ...(OPTIMIZELY_APPLICATION_HOST ? { query: { host: OPTIMIZELY_APPLICATION_HOST } } : {}),
  });
&lt;/pre&gt;
&lt;p&gt;The SDK does not infer the hosting model from the Graph endpoint, so select the PaaS platform explicitly; the SDK&#39;s PaaS guide gives the reason: without it, queries that use a SaaS-only feature fail against PaaS. In 3.1.0-beta.0 the two adapters still build the same queries. A comment in the SDK source says why the PaaS adapter is a copy of the SaaS one: so that SaaS changes don&#39;t reach PaaS.&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;query.host&lt;/span&gt; matters as soon as the CMS has more than one application. Astro often joins a CMS that already serves a site, as a second application. Every application&#39;s start page then matches the same path. The SDK&#39;s troubleshooting guide covers this case (&lt;em&gt;I get content from the wrong site&lt;/em&gt;); on my test CMS the symptom was less obvious: the first hit was Alloy&#39;s start page, a type Astro does not register. The page rendered empty. Bind the Headless application&#39;s host to a language as well (&lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;locale: &#39;en&#39;&lt;/span&gt; in &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;optimizely.config.mjs&lt;/span&gt;). Otherwise CMS 13 puts the start page at &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;/en/&lt;/span&gt;.&lt;/p&gt;
&lt;h2&gt;Rendering Visual Builder without React&lt;/h2&gt;
&lt;p&gt;This is the heart of the series. The official sample&#39;s main routes render a composition with React&#39;s &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;OptimizelyComposition&lt;/span&gt; from &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;@optimizely/cms-sdk/react/server&lt;/span&gt;, which renders every node through the async server component &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;OptimizelyComponent&lt;/span&gt;. It works, but it hands the whole page to one React render, and Astro cannot place an island anywhere inside it. The sample&#39;s newer &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;/svelte/&lt;/span&gt; route uses the core&#39;s planning, but again hands the whole page to one framework and does not cover preview. The core entry point exposes the same planning as plain functions, so Astro can own the page instead.&lt;/p&gt;
&lt;p&gt;The bridge is one registry for every framework:&lt;/p&gt;
&lt;pre style=&quot;background: #f6f8fa; border: 1px solid #e1e4e8; border-radius: 6px; padding: 16px; overflow: auto; font-family: Consolas,Monaco,monospace; font-size: 13px; line-height: 1.5;&quot;&gt;// src/lib/astroRegistry.ts
import { ComponentRegistry } from &#39;@optimizely/cms-sdk/core&#39;;
import LandingExperience from &#39;@/components/LandingExperience.astro&#39;;
import ArticlePage from &#39;@/components/ArticlePage.astro&#39;;
import BlankSection from &#39;@/components/BlankSection.astro&#39;;
import Hero from &#39;@/components/react/Hero&#39;;
import Tile, { SquareTile } from &#39;@/components/react/Tile&#39;;
import CallToAction from &#39;@/components/react/CallToAction&#39;;
import MonthlySpecial from &#39;@/components/svelte/MonthlySpecial.svelte&#39;;
import FAQIsland from &#39;@/components/islands/FAQIsland.astro&#39;;
import Spotlight from &#39;@/components/Spotlight.astro&#39;;

// One registry for .astro, React and Svelte components. Everything renders on the server;
// the only client JavaScript comes from the wrappers in src/components/islands/.
export const registry = new ComponentRegistry({
  LandingExperience,
  ArticlePage,
  BlankSection,
  Hero,
  Tile: { default: Tile, tags: { Square: SquareTile } },
  CallToAction,
  MonthlySpecial,
  FAQ: FAQIsland,
  Spotlight,
});
&lt;/pre&gt;
&lt;p&gt;&lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;resolveContentComponent(content, { registry })&lt;/span&gt; looks components up in this registry and nothing else (passing a registry replaces the SDK&#39;s global component registries, with no fallback), so nothing in the application imports &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;@optimizely/cms-sdk/react/server&lt;/span&gt;. A display template with a tag selects a variant. When the &lt;em&gt;Square&lt;/em&gt; style is picked on a tile, the node carries that display template&#39;s key. The planner (&lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;planGridSection()&lt;/span&gt; for a tile inside a section, &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;planComposition()&lt;/span&gt; at the top level) maps it to the tag &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;Square&lt;/span&gt; through the templates registered with &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;initDisplayTemplateRegistry()&lt;/span&gt;. That registry is global, and the &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;registry&lt;/span&gt; option does not replace it. &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;tags.Square&lt;/span&gt; renders the tile.&lt;/p&gt;
&lt;p&gt;The composition itself is about twenty lines of Astro:&lt;/p&gt;
&lt;pre style=&quot;background: #f6f8fa; border: 1px solid #e1e4e8; border-radius: 6px; padding: 16px; overflow: auto; font-family: Consolas,Monaco,monospace; font-size: 13px; line-height: 1.5;&quot;&gt;---
// src/components/OptimizelyComposition.astro
import { planComposition, resolveContentComponent, isWrappedComponent, type ExperienceNode } from &#39;@optimizely/cms-sdk/core&#39;;
import { registry } from &#39;@/lib/astroRegistry&#39;;

interface Props { nodes: ExperienceNode[] }
const { nodes } = Astro.props;
const items = planComposition(nodes);
---
{items.map((item) =&amp;gt; {
  if (item.kind === &#39;unknown&#39;) return null;
  const r = resolveContentComponent(item.content, { tag: item.tag, registry });
  if (!r.component) return null;
  const Component = r.component as any;
  const props = { content: r.contentProps, displaySettings: item.displaySettings };
  // A component node gets the preview wrapper; a section node carries its own data-epi-* attributes.
  return isWrappedComponent(item)
    ? &amp;lt;div {...(item.previewAttrs ?? {})}&amp;gt;&amp;lt;Component {...props} /&amp;gt;&amp;lt;/div&amp;gt;
    : &amp;lt;Component {...props} {...(item.previewAttrs ?? {})} /&amp;gt;;
})}
&lt;/pre&gt;
&lt;p&gt;&lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;planComposition()&lt;/span&gt; turns the experience&#39;s nodes into a flat plan; &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;isWrappedComponent()&lt;/span&gt; separates component nodes, which get a &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;&amp;lt;div&amp;gt;&lt;/span&gt; carrying the preview attributes, from sections, which take the attributes themselves. Sections hand their rows and columns to &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;planGridSection()&lt;/span&gt;, and a recursive &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;GridItems.astro&lt;/span&gt; renders the grid; row and column display templates arrive as &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;displaySettings&lt;/span&gt; and become CSS classes, so the design system, not the editor, decides what &quot;large padding&quot; looks like.&lt;/p&gt;
&lt;p&gt;One Astro rule shapes the rest. A dynamic tag such as &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;&amp;lt;Component /&amp;gt;&lt;/span&gt; cannot carry a &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;client:*&lt;/span&gt; directive, because Astro must know at build time what to bundle. So each island is a small static wrapper in &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;src/components/islands/&lt;/span&gt;, registered under its content type, and &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;grep -rn &quot;client:&quot; src/components&lt;/span&gt; lists every component that ships JavaScript. In the demo that is one Svelte FAQ, while the React tiles, a Svelte price card and an &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;.astro&lt;/span&gt; Spotlight next to them render on the server and ship nothing.&lt;/p&gt;
&lt;p&gt;The practical consequence is the one that matters for a CMS team: the CMS content model no longer has to be tied to a single frontend framework. A component can be reimplemented in React, Svelte or &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;.astro&lt;/span&gt; while its content type and the Visual Builder composition stay the same, as long as the replacement keeps the props contract the registry passes it.&lt;/p&gt;
&lt;h2&gt;Live preview&lt;/h2&gt;
&lt;p&gt;Preview is one route, &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;src/pages/preview.astro&lt;/span&gt;. The CMS opens it in an iframe with the content key, version, locale, context and a short-lived preview token; &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;getPreviewContent()&lt;/span&gt; fetches the draft through Graph, and the page renders it through the same registry as the public site, so editors see exactly the components visitors get.&lt;/p&gt;
&lt;figure style=&quot;margin: 24px auto; text-align: center; max-width: 800px;&quot;&gt;&lt;img style=&quot;max-width: 100%; height: auto; border: 1px solid #e1e4e8; border-radius: 6px;&quot; src=&quot;https://pino-labs.com/blog/visual-builder-without-nextjs-part-1/cms13-editor-astro-preview.png&quot; alt=&quot;Figure 3: The Landing experience in the CMS 13 editor, with the Astro preview route in the frame&quot; /&gt;
&lt;figcaption style=&quot;font-size: 13px; color: #666; margin-top: 6px;&quot;&gt;&lt;em&gt;Figure 3. The Landing experience in CMS 13.5.0: the editor&#39;s properties on the left, the Astro preview route in the frame on the right. The hero, the Spotlight and the FAQ come from the same registry as the public site; the hero and the Spotlight render on the server, the FAQ (outlined by the editor&#39;s overlay) is the one Svelte island.&lt;/em&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure style=&quot;margin: 24px auto; text-align: center; max-width: 800px;&quot;&gt;&lt;img style=&quot;max-width: 100%; height: auto; border: 1px solid #e1e4e8; border-radius: 6px;&quot; src=&quot;https://pino-labs.com/blog/visual-builder-without-nextjs-part-1/preview-loop-light.svg&quot; alt=&quot;Figure 4: Live preview&quot; /&gt;
&lt;figcaption style=&quot;font-size: 13px; color: #666; margin-top: 6px;&quot;&gt;&lt;em&gt;Figure 4. One route, the same registry as the public site, and a full reload after every save. Graph checks the token; the public site never sees drafts.&lt;/em&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The CMS&#39;s &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;communicationinjector.js&lt;/span&gt; connects the frame to the editor, and &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;createContentSavedListener()&lt;/span&gt; from the core entry point navigates the frame, after each save, to the preview URL the CMS sends with the event, which carries a new token. It is a plain &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;&amp;lt;script&amp;gt;&lt;/span&gt; with no React on the client, and the reload is a full one, since this page does not use Astro&#39;s client router. The route sends &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;private&lt;/span&gt;, &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;no-store&lt;/span&gt;, &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;noindex&lt;/span&gt; and a &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;frame-ancestors&lt;/span&gt; policy for the CMS origin. Graph checks the token: a bad or expired one gets a 401 from Graph, and the route answers 401 without detail and caches nothing. It does the same for any other failure to fetch the draft; the reason goes to the server log.&lt;/p&gt;
&lt;p&gt;On the CMS side, keep Graph&#39;s &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;ContentVersionSynchronizationMode&lt;/span&gt; at its default, &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;All&lt;/span&gt;: the preview reads the exact version the CMS asks for, and &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;PublishedOnly&lt;/span&gt; would leave drafts out of the index. The public site reads with the single key, which by design never sees drafts, and here the unpublished start page was visible with the app key and secret and invisible with the single key.&lt;/p&gt;
&lt;p&gt;One expectation to set with editors coming from CMS 12: headless sites have no inline editing on the page. They get property overlays that open the field in the editor, as Optimizely&#39;s CMS 13 FAQ notes.&lt;/p&gt;
&lt;h2&gt;Request isolation: one observation&lt;/h2&gt;
&lt;p&gt;Without a per-request scope preview still renders, so nothing looks wrong. With &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;adapter.run()&lt;/span&gt; removed from the middleware of the built site (the adapter itself still configured), a sequential test against the real CMS 13.5.0 and Graph showed the problem: after one preview request with a token issued by the CMS, the next public render of the same article carried that token in a rich-text image URL (&lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;?preview_token=&amp;hellip;&lt;/span&gt;). On this Astro server path, the SDK&#39;s request context, where &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;getPreviewContent()&lt;/span&gt; keeps the token, was not scoped per server request. That shows preview context reaching public output; it does not show that the token could be used to read drafts, and I did not try. A preview token does not belong in public HTML either way. With &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;adapter.run()&lt;/span&gt; back, the same sequence leaves no token in public HTML.&lt;/p&gt;
&lt;figure style=&quot;margin: 24px auto; text-align: center; max-width: 800px;&quot;&gt;&lt;img style=&quot;max-width: 100%; height: auto; border: 1px solid #e1e4e8; border-radius: 6px;&quot; src=&quot;https://pino-labs.com/blog/visual-builder-without-nextjs-part-1/request-isolation-light.svg&quot; alt=&quot;Figure 5: Request isolation&quot; /&gt;
&lt;figcaption style=&quot;font-size: 13px; color: #666; margin-top: 6px;&quot;&gt;&lt;em&gt;Figure 5. The same two requests with and without a per-request scope. A configured adapter read outside &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;run()&lt;/span&gt; falls back to one shared store, silently.&lt;/em&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The SDK&#39;s request-context and framework-agnostic core guides prescribe exactly this: in frameworks other than React, configure an adapter and run each request inside it. Neither guide says what happens when a configured adapter is read outside &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;run()&lt;/span&gt;; only a doc comment in the adapter&#39;s source does. The adapter falls back to one process-wide store. No error, no warning. That is how a token set by one request reached another. On the SDK&#39;s &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;main&lt;/span&gt; branch (CMS-58169, merged on 28 October, not yet on npm) media URLs no longer carry the token unless the preview opts in with &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;forceMediaToken&lt;/span&gt;, which narrows this symptom; the shared fallback is still there. In short, this is a reproduced failure mode of one integration: SDK 3.1.0-beta.0 in an Astro server, with the adapter configured but &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;run()&lt;/span&gt; missing. On its own, it does not establish a vulnerability in the SDK. With the documented request-scoping pattern, the token stayed out of public HTML in every test described here.&lt;/p&gt;
&lt;p&gt;In this project the fix is the guide&#39;s pattern: an &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;AsyncLocalStorage&lt;/span&gt;, configured once and entered for every request:&lt;/p&gt;
&lt;pre style=&quot;background: #f6f8fa; border: 1px solid #e1e4e8; border-radius: 6px; padding: 16px; overflow: auto; font-family: Consolas,Monaco,monospace; font-size: 13px; line-height: 1.5;&quot;&gt;// src/lib/context.ts
import { AsyncLocalStorage } from &#39;node:async_hooks&#39;;
import { configureAdapter, MemoryAdapter } from &#39;@optimizely/cms-sdk/core&#39;;

export const adapter = new MemoryAdapter(new AsyncLocalStorage());
configureAdapter(adapter);
&lt;/pre&gt;
&lt;pre style=&quot;background: #f6f8fa; border: 1px solid #e1e4e8; border-radius: 6px; padding: 16px; overflow: auto; font-family: Consolas,Monaco,monospace; font-size: 13px; line-height: 1.5;&quot;&gt;// src/middleware.ts
import { defineMiddleware } from &#39;astro:middleware&#39;;
import { adapter } from &#39;@/lib/context&#39;;

// Wraps the whole render, so every await inside components still sees this request&#39;s context.
export const onRequest = defineMiddleware((_context, next) =&amp;gt; adapter.run(() =&amp;gt; next()));
&lt;/pre&gt;
&lt;p&gt;A check guards it: 12 preview requests interleaved with 12 public requests to the freshly purged article put no &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;preview_token&lt;/span&gt; in public HTML, the preview HTML still carries it, and the check fails when &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;adapter.run()&lt;/span&gt; is removed. Part 2 looks at the mechanism in depth: where the SDK keeps this context, how that differs between runtimes, and how it behaves on DXP.&lt;/p&gt;
&lt;h2&gt;So, why Astro?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Visual Builder:&lt;/strong&gt; framework-neutral planning lets Astro, React and Svelte components take part in the same composition. Only components marked as islands are hydrated in the browser.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Measured:&lt;/strong&gt; the article page ships no &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;&amp;lt;script&amp;gt;&lt;/span&gt;; the landing experience loads 14.6 kB gzip (14,647 bytes), the Svelte runtime and the FAQ, only when the FAQ scrolls into view, plus 2.2 kB of inline island loader; the React client that &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;@astrojs/react&lt;/span&gt; builds (68.2 kB gzip) is loaded by no page.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Architecture:&lt;/strong&gt; React becomes optional. The content model survives a change of framework.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This does not make Astro a replacement for Next.js. I would still choose Next.js for a site that is mostly application (authenticated dashboards, heavy client state), for a team already invested in React Server Components, or for a component library that is React-only and has no reason to change.&lt;/p&gt;
&lt;h2&gt;Caveats&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Beta SDK.&lt;/strong&gt; The &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;/core&lt;/span&gt; entry point this design rests on, and the &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;platform&lt;/span&gt; option, need 3.1.0-beta.0; the SDK itself has supported CMS 13 since 2.0 (May 2026), and I did not test 3.0.2 against PaaS. Pin the beta exactly and re-test at 3.1.0.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PaaS headless is still evolving.&lt;/strong&gt; The FAQ for CMS 13.0.0 describes it as an area of ongoing development. In 3.1.0-beta.0, &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;platform: &#39;paas&#39;&lt;/span&gt; selects an adapter that builds the same queries as SaaS; that is true of this version, and later ones may differ.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CMS 13 only.&lt;/strong&gt; CMS 12 with Graph does not qualify.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version mismatch to watch.&lt;/strong&gt; Astro 7.4.2 requires Node 22.23.4 or later. Below it the &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;astro&lt;/span&gt; CLI (&lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;dev&lt;/span&gt;, &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;build&lt;/span&gt;, &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;preview&lt;/span&gt;) exits with an error; the built server does not check. Optimizely&#39;s front-end hosting documentation still lists Node 22.12.0 or later for Astro. That is a mismatch in the documentation, and I have not seen an actual incompatibility; part 2 checks the Node version the DXP build slot actually runs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Expect no inline editing&lt;/strong&gt; on headless sites; editors get overlays.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Not exercised here:&lt;/strong&gt; Opti ID, the DXP build slot, Redis and Cloudflare. Part 2 covers DXP, with the checks to run on Test1.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The problems that cost the most&lt;/h2&gt;
&lt;p&gt;Three problems cost the most. Before publishing I reproduced each of them again, on 29 October, against the CMS 13 instance and the packages as published on npm:&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin: 20px 0; font-size: 14px;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px; text-align: left; background: #f5f5f4;&quot;&gt;Problem&lt;/th&gt;
&lt;th style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px; text-align: left; background: #f5f5f4;&quot;&gt;Why&lt;/th&gt;
&lt;th style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px; text-align: left; background: #f5f5f4;&quot;&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;&lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;npm install @optimizely/cms-sdk&lt;/span&gt; installs 3.0.2; importing &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;@optimizely/cms-sdk/core&lt;/span&gt; fails (&lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;ERR_PACKAGE_PATH_NOT_EXPORTED&lt;/span&gt; in Node), and nothing in that version knows &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;platform: &#39;paas&#39;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;the &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;/core&lt;/span&gt; entry point and the &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;platform&lt;/span&gt; option first ship in 3.1.0-beta.0, under the &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;beta&lt;/span&gt; tag; &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;latest&lt;/span&gt; is still 3.0.2&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;pin &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;@optimizely/cms-sdk&lt;/span&gt; and &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;@optimizely/cms-cli&lt;/span&gt; to exactly &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;3.1.0-beta.0&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;&lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;config push&lt;/span&gt; answers &lt;em&gt;The client credentials are invalid&lt;/em&gt;, although the CMS issues that client a token&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;without &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;OPTIMIZELY_CMS_API_URL&lt;/span&gt; or &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;--host&lt;/span&gt;, the CLI requests its token from the default SaaS gateway (&lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;api.cms.optimizely.com&lt;/span&gt;), which does not know a PaaS client; it never reads &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;OPTIMIZELY_CMS_URL&lt;/span&gt;. Documented in the CLI reference, but the error reads as a credentials problem&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;&lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;OPTIMIZELY_CMS_API_URL=https://&amp;lt;cms-host&amp;gt;&lt;/span&gt;; the CLI also loads &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;.env&lt;/span&gt; from the directory it runs in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;A TypeScript type with the key of a C# class is not saved: the CLI prints &lt;em&gt;✔ Configuration file uploaded&lt;/em&gt;, then &lt;em&gt;&amp;hellip;can only be updated by modifying this model&lt;/em&gt;, and exits with an error&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;types registered from .NET models (&lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;source: &quot;_server&quot;&lt;/span&gt;) are read-only through the REST API; the CMS answers 200 with an &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;errors&lt;/span&gt; list, and the CMS leaves those types out of &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;config pull&lt;/span&gt; unless you pass &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;--includeReadOnly&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;border: 1px solid #e0e0e0; padding: 8px 12px;&quot;&gt;change the type in C#, or give the TypeScript type its own key; pull C# types only into a folder that &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;config push&lt;/span&gt; does not read&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The full list, with the CMS setup and every version I pinned, is in the repository. That includes the 403 on the start content, the start-page routing and the relative media URLs.&lt;/p&gt;
&lt;h2&gt;How I tested it&lt;/h2&gt;
&lt;p&gt;Every code excerpt is from the companion repository. It type-checks with &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;astro check&lt;/span&gt; (0 errors) and passes 10 unit tests for the cache provider of part 2. Its 28 checks (HTTP-level assertions in eight groups) ran against the real CMS 13.5.0 and Optimizely Graph, on a test article whose draft has an image in its rich text, with a preview token issued by the CMS REST API (&lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;GET /_cms/v1/content/{key}/versions/{version}/previews&lt;/span&gt;). Checks 1 to 6 ran through the repository&#39;s &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;test/checks.sh&lt;/span&gt;. The repository&#39;s versions of checks 7 and 8 are written for local runs, so I ran those two with a separate script, whose logic is now in the repository as &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;test/real-checks-7-8.mjs&lt;/span&gt;: a real publish through the same REST API, after which Graph served the new version within about two seconds on this test instance (polled once a second; 1.5 to 1.6 s in three runs), and a scan of the client bundle for the real key values.&lt;/p&gt;
&lt;p&gt;The checks that matter for this part: the article page ships no &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;&amp;lt;script&amp;gt;&lt;/span&gt;, the landing page stays under 20 KiB of JavaScript, images load from the CMS, a bad preview token gets a 401 with &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;no-store&lt;/span&gt;, and no preview token leaks into public HTML under concurrency. That run also caught a bug. It was in my own code: on the stored-query path the SDK uses (&lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;?stored=true&lt;/span&gt;), Graph returns rich text&#39;s &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;json&lt;/span&gt; as a string. Without that parameter it is an object, and behind Graph&#39;s CDN either shape can come back. The SDK&#39;s &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;RichText&lt;/span&gt; accepts both shapes, but my media rewrite walked the raw value and missed images inside rich text until it parsed the string first.&lt;/p&gt;
&lt;p&gt;The instance is an Alloy-based CMS 13 on .NET 10 with ASP.NET Identity, with Astro added as a second, Headless application. On 13.4.2, &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;config push&lt;/span&gt; created the model, the start content and the application through the CMS REST API, and the pages rendered from Graph; the 403, start-page routing and media URLs came from that run. After upgrading the same CMS to 13.5.0, the preview frame in Visual Builder showed the Astro page (figure 3), and the server log showed the frame reloading about a second after every save and again after publishing. Two things I have not yet confirmed in the editor: the &lt;em&gt;Square&lt;/em&gt; style switching the component, and click-to-field focus. What the run does not cover (Opti ID, DXP, Redis and Cloudflare) is part 2&#39;s job, measured on Test1.&lt;/p&gt;
&lt;h2&gt;Verdict&lt;/h2&gt;
&lt;p&gt;Yes: with the 3.1.0 beta, Astro can render Visual Builder content on CMS 13 PaaS, but it is not a drop-in replacement for the Next.js integration. The framework-neutral SDK provides the primitives; the application still has to provide the integration around them: one registry for its components, a preview route and its reload, routing that knows which application a path belongs to, and a request scope for the SDK&#39;s context. For the platform team, the useful feedback is about failure modes. The SDK&#39;s guides cover each of these pieces. What they don&#39;t show is where a missing piece fails silently: a configured &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;MemoryAdapter&lt;/span&gt; read outside &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;run()&lt;/span&gt; falls back to a shared store, the CLI without &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;OPTIMIZELY_CMS_API_URL&lt;/span&gt; asks the SaaS gateway for its token and reports a credentials error, and &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;config push&lt;/span&gt; prints &lt;em&gt;✔ Configuration file uploaded&lt;/em&gt; before the CMS&#39;s errors.&lt;/p&gt;
&lt;p&gt;The interesting part is not replacing Next.js. It is separating Optimizely&#39;s content composition model from the frontend framework, and knowing what you have to rebuild when you do.&lt;/p&gt;
&lt;p&gt;Part 2 takes a closer look at request isolation and asks the production question: can the same architecture survive DXP (Redis-backed ISR, Graph webhooks, Cloudflare and preview) without a redeploy for every publish?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Repository:&lt;/strong&gt; &lt;a href=&quot;https://github.com/pino-labs/astro-visual-builder-cms13&quot;&gt;pino-labs/astro-visual-builder-cms13&lt;/a&gt;, the code, the checks and the CMS 13 setup notes.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Series:&lt;/em&gt; &lt;strong&gt;Part 1: What Astro Has to Rebuild on CMS 13 PaaS&lt;/strong&gt; &amp;middot; Part 2: Does It Survive DXP? (Redis-backed ISR, Graph webhooks, Cloudflare and preview at scale)&lt;/p&gt;
&lt;h2&gt;Sources&lt;/h2&gt;
&lt;p&gt;All links checked on 2026-10-29. The release notes moved from support.optimizely.com to docs.optimizely.com; the old addresses redirect.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.optimizely.com/cms-13/release-notes&quot;&gt;2026 Optimizely CMS 13 release notes&lt;/a&gt;: DXP support for Astro (31 Aug 2026), the JavaScript SDK on CMS 13, the CMS-57780 culture-specific data-loss fix in 13.4.1 and the restore of deleted values in 13.4.2 (26 Oct 2026), 13.5.0 (27 Oct 2026)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.optimizely.com/cms-saas/release-notes/2026-cms-saas-release-notes&quot;&gt;2026 CMS (SaaS) release notes&lt;/a&gt;: JavaScript SDK 3.0, the framework-neutral SDK core and the Astro sample (24 Sep 2026)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.npmjs.com/package/@optimizely/cms-sdk&quot;&gt;@optimizely/cms-sdk on npm&lt;/a&gt;: &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;latest&lt;/span&gt; 3.0.2, &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;beta&lt;/span&gt; 3.1.0-beta.0 (20 Oct 2026); &lt;a href=&quot;https://github.com/episerver/content-js-sdk/releases&quot;&gt;release notes on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.optimizely.com/cms-13/docs/faqs-for-cms-13&quot;&gt;FAQs for CMS 13&lt;/a&gt;: where content types live on PaaS headless, and no inline editing on headless sites&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/episerver/content-js-sdk&quot;&gt;episerver/content-js-sdk&lt;/a&gt;: Astro sample, CMS PaaS guide, framework-agnostic core, request context, CLI reference and troubleshooting; on the &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;main&lt;/span&gt; branch, changes CMS-58169 (media preview token opt-in, merged 28 Oct 2026) and CMS-57658 (&lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;config pull&lt;/span&gt; output flags outside a terminal, merged 26 Oct 2026)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.optimizely.com/cms-saas/docs/host-a-front-end-with-optimizely&quot;&gt;Host a frontend with Optimizely&lt;/a&gt;: Node.js 22.12.0 or later for Astro on DXP front-end hosting&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.optimizely.com/dxp/docs/astro-isr-caching-and-optimizely-graph-webhooks&quot;&gt;Astro ISR caching and Optimizely Graph webhooks&lt;/a&gt;: the DXP page for Astro, also listing Node.js 22.12.0 or later&lt;/li&gt;
&lt;li&gt;Prior work on SaaS CMS: Jacob Pretorius, &lt;a href=&quot;/link/ad0ab559cb97490491e9c4d448f232ca.aspx&quot;&gt;Build a headless blog with Astro and Optimizely SaaS CMS Part 4 - SSR &amp;amp; Visual Builder Experiences&lt;/a&gt; (21 July 2025)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;Stack: Optimizely CMS 13.4.2 and 13.5.0 (Alloy-based, .NET 10, ASP.NET Identity) with Optimizely Graph, &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;@optimizely/cms-sdk&lt;/span&gt; and &lt;span style=&quot;font-family: Consolas,Monaco,monospace; font-size: 0.88em; background: #f1f0ee; border-radius: 4px; padding: 2px 6px; color: #24292f;&quot;&gt;@optimizely/cms-cli&lt;/span&gt; 3.1.0-beta.0, Astro 7.4.2 on Node 24.19.0, React and Svelte components behind one registry. Optimizely, Astro, Next.js, React and Svelte are trademarks of their respective owners; this article is not affiliated with them.&lt;/em&gt;&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/piotr-nowak---optimizely--azure/dates/2026/10/visual-builder-without-next.js-part-1-what-astro-has-to-rebuild-on-cms-13-paas</guid>            <pubDate>Thu, 29 Oct 2026 13:16:53 GMT</pubDate>           <category>Blog post</category></item><item> <title>Optimizely certifications in 2026: the new programme explained</title>            <link>https://mattcdunn.com/blog/optimizely-certifications/</link>            <description>How Optimizely&#39;s 2026 certification programme works: Foundation, Core and Advanced tiers, 17 product tracks, renewal, and which path to take.</description>            <guid>https://mattcdunn.com/blog/optimizely-certifications/</guid>            <pubDate>Thu, 29 Oct 2026 00:00:00 GMT</pubDate>           <category>Blog post</category></item><item> <title>Visual Builder without Next.js, Part 1: What Astro Has to Rebuild on CMS 13 PaaS</title>            <link>https://pino-labs.com/blog/visual-builder-without-nextjs-part-1-astro-cms-13-paas/</link>            <description>A field report on the framework-neutral CMS SDK 3.1 beta: Visual Builder, the content model and live preview in Astro, on a real CMS 13.</description>            <guid>https://pino-labs.com/blog/visual-builder-without-nextjs-part-1-astro-cms-13-paas/</guid>            <pubDate>Thu, 29 Oct 2026 00:00:00 GMT</pubDate>           <category>Blog post</category></item><item> <title>What Mattered More Than the API. Part 5: Process Uptime and the Benchmark</title>            <link>https://szolkowski.github.io/2026/10/28/what-mattered-more-than-the-api-part-5-process-uptime-and-the-benchmark</link>            <description>A long-running app imported 7x slower than a freshly restarted one, the finding that beat every API optimization, plus runnable benchmark jobs.</description>            <guid>https://szolkowski.github.io/2026/10/28/what-mattered-more-than-the-api-part-5-process-uptime-and-the-benchmark</guid>            <pubDate>Wed, 28 Oct 2026 08:00:00 GMT</pubDate>           <category>Blog post</category></item><item> <title>Virtual Teammates in Optimizely Mark: a hands-on first look</title>            <link>https://world.optimizely.com/blogs/Minesh-Shah/Dates/2026/10/virtual-teammates-in-optimizely-mark-a-hands-on-first-look/</link>            <description>&lt;p&gt;Virtual Teammates are the most interesting thing Optimizely has shipped on its agent platform this year, and the hardest to evaluate from a demo. They are not another agent. They are persistent AI workers with their own login, mailbox, memory and job description, and they keep working when you close the tab.&lt;/p&gt;
&lt;p&gt;The Optimizely MVP organisation has just been given access, so I spent a day taking them apart: reading the docs, walking every screen in the directory, and enabling the SEO &amp;amp; AI Search Analyst against a real site with real Google Analytics 4 and Search Console data. Every screenshot below is from that session, unedited apart from cropping. This post covers what they are, how they are configured end to end, what my teammate actually found, where I think they earn their keep, and the gotchas I hit on the way.&lt;/p&gt;
&lt;p&gt;A quick note on naming, because it will trip you up. Opal is now the Optimizely Agent Platform, and the assistant and app you work in are called Mark. The UI says Mark, the docs mostly say Mark, and some help articles and the release notes page still say Opal. They are the same product. Virtual Teammates were released on 25/08/2026 with five roles. The Web Manager and Content Marketer arrived on 13/10/2026, and a Translation Specialist is now in the directory too.&lt;/p&gt;
&lt;h2&gt;Teammate, not agent&lt;/h2&gt;
&lt;p&gt;The difference is persistence and identity. An agent runs when you invoke it and forgets you when it finishes. A Virtual Teammate is hired once, given a job, and then runs that job on a schedule or in response to events, building up memory as it goes.&lt;/p&gt;
&lt;p&gt;Optimizely describes four layers, and they map neatly onto things you already know in Mark:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Tools.&lt;/strong&gt; Connectors to systems like GA4, Search Console, Semrush, HubSpot or Slack. The same connectors your agents already use.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Agents and workflows.&lt;/strong&gt; Packaged tasks the teammate runs from Mark, with your permission.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Jobs.&lt;/strong&gt; Workflows the teammate runs on its own, on a schedule or an event. This is the bit that makes it a teammate.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Teammate layer.&lt;/strong&gt; A persistent identity, memory, personality and job description wrapped around the rest.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The identity part is more literal than I expected. Each teammate gets its own Opti ID account, login and email address, flagged as a virtual user. It does not consume a product seat. It inherits nothing from the person who hired it, so you grant it access exactly as you would a new starter. Inside Optimizely products it acts as itself. Outside them it acts through accounts a human connects on its behalf, because a teammate cannot complete an OAuth consent itself.&lt;/p&gt;
&lt;p&gt;You work with it through &lt;strong&gt;Team Messages&lt;/strong&gt;, a Slack-style messaging layer inside Mark with direct messages, groups and channels. Teammates can message each other too, and can join Teams and Slack meetings to share summaries. The default autonomy model is sensible: the teammate drafts and proposes, you approve or redirect in Team Messages, and you widen what it can do unattended as trust builds.&lt;/p&gt;
&lt;h2&gt;The directory: eight teammates&lt;/h2&gt;
&lt;p&gt;The MVP organisation&#39;s directory lists eight teammates. Each has a CV-style card (&quot;Why hire me&quot;, &quot;Experience&quot;, &quot;Tools used&quot;) and a set of jobs you can switch on or off when enabling it. All jobs are on by default.&lt;/p&gt;
&lt;p style=&quot;text-align: center; margin: 24px 0 4px 0;&quot;&gt;&lt;img style=&quot;width: 100%; max-width: 100%; height: auto; border: 1px solid #d9d9d9; border-radius: 4px;&quot; src=&quot;/link/3834f247b43f46a9986580411f2d6cf6.aspx&quot; alt=&quot;The Virtual Teammates directory in Mark. Each card is a &amp;quot;resume&amp;quot; with the jobs it does and the tools it works with.&quot; width=&quot;1100&quot; height=&quot;470&quot; /&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center; font-size: 0.9em; color: #5c5c5c; margin: 0 0 24px 0;&quot;&gt;&lt;em&gt;The Virtual Teammates directory in Mark. Each card is a &quot;resume&quot; with the jobs it does and the tools it works with.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The table below is taken from the enable screens in the MVP instance on 27/10/2026.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; margin: 16px 0 24px 0; font-size: 0.92em;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;text-align: left; vertical-align: bottom; padding: 8px 10px; background-color: #f0f2ee; border-bottom: 2px solid #c9ccc5;&quot;&gt;Teammate&lt;/th&gt;
&lt;th style=&quot;text-align: left; vertical-align: bottom; padding: 8px 10px; background-color: #f0f2ee; border-bottom: 2px solid #c9ccc5;&quot;&gt;Jobs&lt;/th&gt;
&lt;th style=&quot;text-align: left; vertical-align: bottom; padding: 8px 10px; background-color: #f0f2ee; border-bottom: 2px solid #c9ccc5;&quot;&gt;Connectors it asks for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;vertical-align: top; padding: 8px 10px; border-bottom: 1px solid #e3e3e3; width: 22%; font-weight: bold; hyphens: none; word-break: normal;&quot;&gt;SEO &amp;amp; AI Search Analyst&lt;/td&gt;
&lt;td style=&quot;vertical-align: top; padding: 8px 10px; border-bottom: 1px solid #e3e3e3;&quot;&gt;Audit AEO readiness (daily), Research industry trends, Monitor performance (SEO and GEO anomaly scan), Research keywords (daily, from Conductor and Search Console)&lt;/td&gt;
&lt;td style=&quot;vertical-align: top; padding: 8px 10px; border-bottom: 1px solid #e3e3e3;&quot;&gt;GA4, Search Console at enablement. Conductor and Semrush at hire.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;vertical-align: top; padding: 8px 10px; border-bottom: 1px solid #e3e3e3; width: 22%; font-weight: bold; hyphens: none; word-break: normal;&quot;&gt;Marketing Analyst&lt;/td&gt;
&lt;td style=&quot;vertical-align: top; padding: 8px 10px; border-bottom: 1px solid #e3e3e3;&quot;&gt;Traffic and acquisition pulse (weekly), Content performance (weekly), Conversion summary (monthly), Anomaly detection (daily), Marketing Analyst actions, Self evaluation&lt;/td&gt;
&lt;td style=&quot;vertical-align: top; padding: 8px 10px; border-bottom: 1px solid #e3e3e3;&quot;&gt;GA4, Search Console, Tag Manager&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;vertical-align: top; padding: 8px 10px; border-bottom: 1px solid #e3e3e3; width: 22%; font-weight: bold; hyphens: none; word-break: normal;&quot;&gt;CRO Manager&lt;/td&gt;
&lt;td style=&quot;vertical-align: top; padding: 8px 10px; border-bottom: 1px solid #e3e3e3;&quot;&gt;Generate experimentation roadmap (PIE scored), Write test plans, Monitor experiment portfolio (daily SRM and stat sig checks), Analyse and report results, Configure experiments&lt;/td&gt;
&lt;td style=&quot;vertical-align: top; padding: 8px 10px; border-bottom: 1px solid #e3e3e3;&quot;&gt;GA4. Works natively in Web Experimentation.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;vertical-align: top; padding: 8px 10px; border-bottom: 1px solid #e3e3e3; width: 22%; font-weight: bold; hyphens: none; word-break: normal;&quot;&gt;Personalization Strategist&lt;/td&gt;
&lt;td style=&quot;vertical-align: top; padding: 8px 10px; border-bottom: 1px solid #e3e3e3;&quot;&gt;Identify opportunities, Design campaign plan, Build campaign, Monitor performance, Impact overview&lt;/td&gt;
&lt;td style=&quot;vertical-align: top; padding: 8px 10px; border-bottom: 1px solid #e3e3e3;&quot;&gt;GA4, Search Console, Salesforce, Google Ads, Figma&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;vertical-align: top; padding: 8px 10px; border-bottom: 1px solid #e3e3e3; width: 22%; font-weight: bold; hyphens: none; word-break: normal;&quot;&gt;Content Marketer&lt;/td&gt;
&lt;td style=&quot;vertical-align: top; padding: 8px 10px; border-bottom: 1px solid #e3e3e3;&quot;&gt;Monitor industry trends (daily), Track competitor content (weekly), Surface pipeline gaps (weekly, from Salesforce), Self evaluation&lt;/td&gt;
&lt;td style=&quot;vertical-align: top; padding: 8px 10px; border-bottom: 1px solid #e3e3e3;&quot;&gt;Salesforce&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;vertical-align: top; padding: 8px 10px; border-bottom: 1px solid #e3e3e3; width: 22%; font-weight: bold; hyphens: none; word-break: normal;&quot;&gt;Web Manager&lt;/td&gt;
&lt;td style=&quot;vertical-align: top; padding: 8px 10px; border-bottom: 1px solid #e3e3e3;&quot;&gt;Perform audit compliance, Manage site health, Manage website updates, Audit report actions&lt;/td&gt;
&lt;td style=&quot;vertical-align: top; padding: 8px 10px; border-bottom: 1px solid #e3e3e3;&quot;&gt;None. Works natively in CMS.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;vertical-align: top; padding: 8px 10px; border-bottom: 1px solid #e3e3e3; width: 22%; font-weight: bold; hyphens: none; word-break: normal;&quot;&gt;Translation Specialist&lt;/td&gt;
&lt;td style=&quot;vertical-align: top; padding: 8px 10px; border-bottom: 1px solid #e3e3e3;&quot;&gt;Create translations, Audit translations&lt;/td&gt;
&lt;td style=&quot;vertical-align: top; padding: 8px 10px; border-bottom: 1px solid #e3e3e3;&quot;&gt;None. Works natively in CMS.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;vertical-align: top; padding: 8px 10px; border-bottom: 1px solid #e3e3e3; width: 22%; font-weight: bold; hyphens: none; word-break: normal;&quot;&gt;Chief of Staff&lt;/td&gt;
&lt;td style=&quot;vertical-align: top; padding: 8px 10px; border-bottom: 1px solid #e3e3e3;&quot;&gt;Daily brief, Weekly brief, Meeting prep, Meeting recap, Competitive intel&lt;/td&gt;
&lt;td style=&quot;vertical-align: top; padding: 8px 10px; border-bottom: 1px solid #e3e3e3;&quot;&gt;Teams, Outlook mail and calendar, OneDrive and SharePoint, or Gmail, Drive and Google Calendar. Plus Salesforce.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Two things stand out. First, the Marketing Analyst and Content Marketer both ship a &lt;strong&gt;Self evaluation&lt;/strong&gt; job that scores each run against expected results and logs what worked. That is the memory story in practice, and it is the job I would watch most closely. Second, the Web Manager and Translation Specialist need no third-party connectors at all. For CMS-heavy clients they are the lowest-friction place to start.&lt;/p&gt;
&lt;h2&gt;Configuration, end to end&lt;/h2&gt;
&lt;p&gt;Setup happens in two halves: an admin enables a teammate for the organisation, then any permitted user hires their own copy of it. It touches four places: Mark, Connect Platform (the old ODP app directory), Opti ID, and the third-party tool itself. Here is the full path I took, in order.&lt;/p&gt;
&lt;h3&gt;1. Before you start: email domain (admin)&lt;/h3&gt;
&lt;p&gt;Check &lt;strong&gt;Settings &amp;gt; Email Domain&lt;/strong&gt; in Mark. Every teammate gets a mailbox on your Mark email domain, so this must be configured and verified first. In the MVP org it already was.&lt;/p&gt;
&lt;p style=&quot;text-align: center; margin: 24px 0 4px 0;&quot;&gt;&lt;img style=&quot;width: 100%; max-width: 100%; height: auto; border: 1px solid #d9d9d9; border-radius: 4px;&quot; src=&quot;/link/60e8418f413c4e00b19fa3d5147e950b.aspx&quot; alt=&quot;Settings &amp;gt; Email Domain. The verified domain is where each teammate&#39;s mailbox lives.&quot; width=&quot;1320&quot; height=&quot;230&quot; /&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center; font-size: 0.9em; color: #5c5c5c; margin: 0 0 24px 0;&quot;&gt;&lt;em&gt;Settings &amp;gt; Email Domain. The verified domain is where each teammate&#39;s mailbox lives.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;2. Enable the teammate and choose its jobs (admin)&lt;/h3&gt;
&lt;p&gt;Go to &lt;strong&gt;Directory &amp;gt; Virtual Teammates&lt;/strong&gt;, or &lt;strong&gt;Grow your team &amp;gt; Browse the directory&lt;/strong&gt; from the empty Teammates page, and select &lt;strong&gt;Details&lt;/strong&gt; on a card. This drops you straight into the enable wizard, where every job is on by default. Turn off any you don&#39;t want offered. Once a teammate is enabled, the same list lives under the cog on its card as &lt;strong&gt;Manage jobs&lt;/strong&gt;, next to &lt;strong&gt;Freeze hiring&lt;/strong&gt;.&lt;/p&gt;
&lt;p style=&quot;text-align: center; margin: 24px 0 4px 0;&quot;&gt;&lt;img style=&quot;width: 100%; max-width: 100%; height: auto; border: 1px solid #d9d9d9; border-radius: 4px;&quot; src=&quot;/link/511c87e01bb748f1af3a64f3ee6ab839.aspx&quot; alt=&quot;The SEO &amp;amp; AI Search Analyst&#39;s four jobs. Admins can switch any of them off for the whole organisation.&quot; width=&quot;710&quot; height=&quot;540&quot; /&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center; font-size: 0.9em; color: #5c5c5c; margin: 0 0 24px 0;&quot;&gt;&lt;em&gt;The SEO &amp;amp; AI Search Analyst&#39;s four jobs. Admins can switch any of them off for the whole organisation.&lt;/em&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center; margin: 24px 0 4px 0;&quot;&gt;&lt;img style=&quot;width: 100%; max-width: 100%; height: auto; border: 1px solid #d9d9d9; border-radius: 4px;&quot; src=&quot;/link/b35d027bacec4150b4584280816f204d.aspx&quot; alt=&quot;After enablement the card shows &amp;quot;1 hired&amp;quot; and a Hire button. The cog holds the organisation-level controls.&quot; width=&quot;730&quot; height=&quot;595&quot; /&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center; font-size: 0.9em; color: #5c5c5c; margin: 0 0 24px 0;&quot;&gt;&lt;em&gt;After enablement the card shows &quot;1 hired&quot; and a Hire button. The cog holds the organisation-level controls.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;3. Install the connectors (admin)&lt;/h3&gt;
&lt;p&gt;Select &lt;strong&gt;Set up connectors&lt;/strong&gt;. The wizard lists the third-party tools the teammate needs, with a tick for anything already installed and &lt;strong&gt;Add connector&lt;/strong&gt; for anything missing. This example is the Marketing Analyst, where GA4 and Search Console were already in place and Tag Manager was not.&lt;/p&gt;
&lt;p style=&quot;text-align: center; margin: 24px 0 4px 0;&quot;&gt;&lt;img style=&quot;width: 100%; max-width: 100%; height: auto; border: 1px solid #d9d9d9; border-radius: 4px;&quot; src=&quot;/link/cb4735184aef4e69ac01528df2269483.aspx&quot; alt=&quot;The connector step of the enable wizard.&quot; width=&quot;680&quot; height=&quot;630&quot; /&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center; font-size: 0.9em; color: #5c5c5c; margin: 0 0 24px 0;&quot;&gt;&lt;em&gt;The connector step of the enable wizard.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Add connector&lt;/strong&gt; opens the connector&#39;s app in Connect Platform in a new tab. GA4 showed as installed when the tab opened; Search Console needed &lt;strong&gt;Install App&lt;/strong&gt;. Installing accepts the third-party add-on terms.&lt;/p&gt;
&lt;p style=&quot;text-align: center; margin: 24px 0 4px 0;&quot;&gt;&lt;img style=&quot;width: 100%; max-width: 100%; height: auto; border: 1px solid #d9d9d9; border-radius: 4px;&quot; src=&quot;/link/95a85a09cb164a119f5676a130ba2cb3.aspx&quot; alt=&quot;The Search Console connector is an app in Connect Platform&#39;s App Directory.&quot; width=&quot;880&quot; height=&quot;368&quot; /&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center; font-size: 0.9em; color: #5c5c5c; margin: 0 0 24px 0;&quot;&gt;&lt;em&gt;The Search Console connector is an app in Connect Platform&#39;s App Directory.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;4. Configure GA4 in Connect Platform (admin)&lt;/h3&gt;
&lt;p&gt;Search Console needs no org-level configuration because each user authenticates with OAuth. GA4 does. Open the GA4 app&#39;s &lt;strong&gt;Settings&lt;/strong&gt; tab and you get four sections.&lt;/p&gt;
&lt;p style=&quot;text-align: center; margin: 24px 0 4px 0;&quot;&gt;&lt;img style=&quot;width: 100%; max-width: 100%; height: auto; border: 1px solid #d9d9d9; border-radius: 4px;&quot; src=&quot;/link/727b1cd6f416457bb19e3c50728f90e7.aspx&quot; alt=&quot;The GA4 Mark app settings. The instance-level OAuth toggle is off by default, so each person uses their own Google login.&quot; width=&quot;1040&quot; height=&quot;605&quot; /&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center; font-size: 0.9em; color: #5c5c5c; margin: 0 0 24px 0;&quot;&gt;&lt;em&gt;The GA4 Mark app settings. The instance-level OAuth toggle is off by default, so each person uses their own Google login.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Under &lt;strong&gt;GA4 Properties&lt;/strong&gt;, add the property ID (from GA4 under Admin &amp;gt; Property details) and an optional nickname. I added realyield.co.uk.&lt;/p&gt;
&lt;p style=&quot;text-align: center; margin: 24px 0 4px 0;&quot;&gt;&lt;img style=&quot;width: 100%; max-width: 100%; height: auto; border: 1px solid #d9d9d9; border-radius: 4px;&quot; src=&quot;/link/7fe66656d3c441e48c3b56377a22bb20.aspx&quot; alt=&quot;The property ID in Google Analytics, under Admin &amp;gt; Property details.&quot; width=&quot;995&quot; height=&quot;390&quot; /&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center; font-size: 0.9em; color: #5c5c5c; margin: 0 0 24px 0;&quot;&gt;&lt;em&gt;The property ID in Google Analytics, under Admin &amp;gt; Property details.&lt;/em&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center; margin: 24px 0 4px 0;&quot;&gt;&lt;img style=&quot;width: 100%; max-width: 100%; height: auto; border: 1px solid #d9d9d9; border-radius: 4px;&quot; src=&quot;/link/d9197da7f72745cab0f502d64ed41e11.aspx&quot; alt=&quot;The property registered in the connector, with its nickname.&quot; width=&quot;1040&quot; height=&quot;785&quot; /&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center; font-size: 0.9em; color: #5c5c5c; margin: 0 0 24px 0;&quot;&gt;&lt;em&gt;The property registered in the connector, with its nickname.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Global Filter&lt;/strong&gt; lets you pin every standard report to one hostname. It is always applied and Mark cannot override it, which is useful when one GA4 property tracks several sites. It does not apply to realtime reports.&lt;/p&gt;
&lt;p style=&quot;text-align: center; margin: 24px 0 4px 0;&quot;&gt;&lt;img style=&quot;width: 100%; max-width: 100%; height: auto; border: 1px solid #d9d9d9; border-radius: 4px;&quot; src=&quot;/link/2909088b4fab4f20a0242dd94b3dee04.aspx&quot; alt=&quot;The optional hostname filter.&quot; width=&quot;740&quot; height=&quot;665&quot; /&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center; font-size: 0.9em; color: #5c5c5c; margin: 0 0 24px 0;&quot;&gt;&lt;em&gt;The optional hostname filter.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Finally, under &lt;strong&gt;Add to Mark&lt;/strong&gt;, select &lt;strong&gt;Add to Mark&lt;/strong&gt; to register the tools with your Mark account. The spinner gave no feedback and reverted, but it had worked: about a minute later the row showed a tick with Remove from Mark and Sync, and the Mark Connectors page had flipped from &quot;Add connector&quot; to &quot;Connect&quot;.&lt;/p&gt;
&lt;p style=&quot;text-align: center; margin: 24px 0 4px 0;&quot;&gt;&lt;img style=&quot;width: 100%; max-width: 100%; height: auto; border: 1px solid #d9d9d9; border-radius: 4px;&quot; src=&quot;/link/92664178d846445cabf41406c6d07ea5.aspx&quot; alt=&quot;Registered. Do the same for the Search Console app.&quot; width=&quot;1040&quot; height=&quot;455&quot; /&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center; font-size: 0.9em; color: #5c5c5c; margin: 0 0 24px 0;&quot;&gt;&lt;em&gt;Registered. Do the same for the Search Console app.&lt;/em&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center; margin: 24px 0 4px 0;&quot;&gt;&lt;img style=&quot;width: 100%; max-width: 100%; height: auto; border: 1px solid #d9d9d9; border-radius: 4px;&quot; src=&quot;/link/5ebb0019a68a4bf48b41d1acb204c56d.aspx&quot; alt=&quot;The Connectors page in Mark once both Google connectors were registered.&quot; width=&quot;1105&quot; height=&quot;785&quot; /&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center; font-size: 0.9em; color: #5c5c5c; margin: 0 0 24px 0;&quot;&gt;&lt;em&gt;The Connectors page in Mark once both Google connectors were registered.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Back in the wizard, select &lt;strong&gt;Enable teammate&lt;/strong&gt;. The card in the directory now says &lt;strong&gt;Hire&lt;/strong&gt;.&lt;/p&gt;
&lt;h3&gt;5. Hire your teammate (any permitted user)&lt;/h3&gt;
&lt;p&gt;Hiring is a four-screen wizard:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Name it.&lt;/strong&gt; Select &lt;strong&gt;Hire&lt;/strong&gt; and give it a name. Mine is Rankin. A name with no spaces keeps the email address clean. You can change the avatar here too.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Choose where it works.&lt;/strong&gt; Pick the product and instance. The MVP org offered cms and oa. I left it unset, as realyield does not run on Optimizely. The screen is honest that this only tells it where to work. It does not grant access.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Connect its tools.&lt;/strong&gt; Under &lt;strong&gt;Day one essentials&lt;/strong&gt;, select &lt;strong&gt;Connect&lt;/strong&gt; for each tool. This opens a Google OAuth pop-up that a human must complete, because the teammate cannot consent for itself. Conductor and Semrush appear here too, but as &quot;Set up its tools on the Connectors page first&quot;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Start working together.&lt;/strong&gt; Once both show Connected, select &lt;strong&gt;Start working together&lt;/strong&gt;, then &lt;strong&gt;Go to your teammate&lt;/strong&gt;. You land in a Team Messages DM.&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: center; margin: 24px 0 4px 0;&quot;&gt;&lt;img style=&quot;width: 100%; max-width: 100%; height: auto; border: 1px solid #d9d9d9; border-radius: 4px;&quot; src=&quot;/link/135329bbf0824f45baacc9074d305c47.aspx&quot; alt=&quot;The hire wizard, from naming to connected tools.&quot; width=&quot;1158&quot; height=&quot;1000&quot; /&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center; font-size: 0.9em; color: #5c5c5c; margin: 0 0 24px 0;&quot;&gt;&lt;em&gt;The hire wizard, from naming to connected tools.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;6. Grant access inside Optimizely products (Opti ID admin)&lt;/h3&gt;
&lt;p&gt;If the teammate will work inside Optimizely products, give its Opti ID user a role on each instance in the Opti ID Admin Center. For CMS 13 and CMS SaaS, also add it under &lt;strong&gt;Settings &amp;gt; Set Access Rights&lt;/strong&gt; with only the rights its jobs need (Read, Create, Change, Delete, Publish or Administer). I skipped this, as Rankin only needed Google data.&lt;/p&gt;
&lt;h2&gt;Managing a teammate&lt;/h2&gt;
&lt;p&gt;Every hired teammate appears under &lt;strong&gt;Virtual Teammates &amp;gt; Teammates&lt;/strong&gt; with its own email address, managers, status and start date.&lt;/p&gt;
&lt;p style=&quot;text-align: center; margin: 24px 0 4px 0;&quot;&gt;&lt;img style=&quot;width: 100%; max-width: 100%; height: auto; border: 1px solid #d9d9d9; border-radius: 4px;&quot; src=&quot;/link/89a0c15fbc2c4b3d9af804b8b03010b8.aspx&quot; alt=&quot;Rankin on the Teammates list, with its own address on the Mark email domain.&quot; width=&quot;1330&quot; height=&quot;400&quot; /&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center; font-size: 0.9em; color: #5c5c5c; margin: 0 0 24px 0;&quot;&gt;&lt;em&gt;Rankin on the Teammates list, with its own address on the Mark email domain.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Each teammate has two areas. &lt;strong&gt;Supervise&lt;/strong&gt; shows Activity (upcoming and completed job runs, with status, priority and trigger) and Work (everything it has produced).&lt;/p&gt;
&lt;p style=&quot;text-align: center; margin: 24px 0 4px 0;&quot;&gt;&lt;img style=&quot;width: 100%; max-width: 100%; height: auto; border: 1px solid #d9d9d9; border-radius: 4px;&quot; src=&quot;/link/1b616769ed764883971ee04afaa4dc03.aspx&quot; alt=&quot;Activity. The four scheduled jobs are queued, with Audit AEO readiness next up.&quot; width=&quot;1305&quot; height=&quot;665&quot; /&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center; font-size: 0.9em; color: #5c5c5c; margin: 0 0 24px 0;&quot;&gt;&lt;em&gt;Activity. The four scheduled jobs are queued, with Audit AEO readiness next up.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Settings&lt;/strong&gt; covers Identity, Tools, Product Connections and Permissions. Identity holds the basics and a personality, which shapes how it writes and reasons. There are four templates, or you can build your own.&lt;/p&gt;
&lt;p style=&quot;text-align: center; margin: 24px 0 4px 0;&quot;&gt;&lt;img style=&quot;width: 100%; max-width: 100%; height: auto; border: 1px solid #d9d9d9; border-radius: 4px;&quot; src=&quot;/link/ea432a6c11c543b2ab81ecaba9d6dcf1.aspx&quot; alt=&quot;Identity: name, email and start date.&quot; width=&quot;1305&quot; height=&quot;595&quot; /&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center; font-size: 0.9em; color: #5c5c5c; margin: 0 0 24px 0;&quot;&gt;&lt;em&gt;Identity: name, email and start date.&lt;/em&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center; margin: 24px 0 4px 0;&quot;&gt;&lt;img style=&quot;width: 100%; max-width: 100%; height: auto; border: 1px solid #d9d9d9; border-radius: 4px;&quot; src=&quot;/link/45e32531c90c4d2592d68e4fc3309449.aspx&quot; alt=&quot;The personality templates: the tastemaker, the hype machine, the pragmatist and the stress tester.&quot; width=&quot;1035&quot; height=&quot;605&quot; /&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center; font-size: 0.9em; color: #5c5c5c; margin: 0 0 24px 0;&quot;&gt;&lt;em&gt;The personality templates: the tastemaker, the hype machine, the pragmatist and the stress tester.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Tools shows what it can reach. Connections you own can be disconnected here; anything an admin has not set up shows as &quot;Contact Admin&quot;.&lt;/p&gt;
&lt;p style=&quot;text-align: center; margin: 24px 0 4px 0;&quot;&gt;&lt;img style=&quot;width: 100%; max-width: 100%; height: auto; border: 1px solid #d9d9d9; border-radius: 4px;&quot; src=&quot;/link/65076206db1c4a95bd99c5377a4afa25.aspx&quot; alt=&quot;Rankin&#39;s tools. GA4 and Search Console are connected; Semrush and Conductor need an admin.&quot; width=&quot;1090&quot; height=&quot;490&quot; /&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center; font-size: 0.9em; color: #5c5c5c; margin: 0 0 24px 0;&quot;&gt;&lt;em&gt;Rankin&#39;s tools. GA4 and Search Console are connected; Semrush and Conductor need an admin.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Permissions is where the governance lives. Roles set who manages the teammate and which other people can message it.&lt;/p&gt;
&lt;p style=&quot;text-align: center; margin: 24px 0 4px 0;&quot;&gt;&lt;img style=&quot;width: 100%; max-width: 100%; height: auto; border: 1px solid #d9d9d9; border-radius: 4px;&quot; src=&quot;/link/359b7e1c59f3400b83d3fde04a73d641.aspx&quot; alt=&quot;Roles. Managers get full access; teammates only what you allow.&quot; width=&quot;1035&quot; height=&quot;725&quot; /&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center; font-size: 0.9em; color: #5c5c5c; margin: 0 0 24px 0;&quot;&gt;&lt;em&gt;Roles. Managers get full access; teammates only what you allow.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Conversation guardrails set what it shares with each audience: managers, named teammates, and anyone in the organisation.&lt;/p&gt;
&lt;p style=&quot;text-align: center; margin: 24px 0 4px 0;&quot;&gt;&lt;img style=&quot;width: 100%; max-width: 100%; height: auto; border: 1px solid #d9d9d9; border-radius: 4px;&quot; src=&quot;/link/82aca7a591c447b8865bd1ae41352f1b.aspx&quot; alt=&quot;Conversation guardrails per audience.&quot; width=&quot;1035&quot; height=&quot;430&quot; /&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center; font-size: 0.9em; color: #5c5c5c; margin: 0 0 24px 0;&quot;&gt;&lt;em&gt;Conversation guardrails per audience.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Jobs can be reordered, switched off, or added. &lt;strong&gt;Add job&lt;/strong&gt; offers Create job or Install from catalog, so you are not limited to the defaults.&lt;/p&gt;
&lt;p style=&quot;text-align: center; margin: 24px 0 4px 0;&quot;&gt;&lt;img style=&quot;width: 100%; max-width: 100%; height: auto; border: 1px solid #d9d9d9; border-radius: 4px;&quot; src=&quot;/link/acfefe33c2b84eb591dbf25072d98f2e.aspx&quot; alt=&quot;Jobs, with the Add job menu open.&quot; width=&quot;1035&quot; height=&quot;432&quot; /&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center; font-size: 0.9em; color: #5c5c5c; margin: 0 0 24px 0;&quot;&gt;&lt;em&gt;Jobs, with the Add job menu open.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Interactions lists the channels it listens on. By default that is instant messaging, and the description shows the tool set it uses to read and reply in Team Messages. Deactivation sits at the bottom and is permanent.&lt;/p&gt;
&lt;p style=&quot;text-align: center; margin: 24px 0 4px 0;&quot;&gt;&lt;img style=&quot;width: 100%; max-width: 100%; height: auto; border: 1px solid #d9d9d9; border-radius: 4px;&quot; src=&quot;/link/64e0ccbeebd245ee932398ccd8cfea40.aspx&quot; alt=&quot;Interactions and deactivation.&quot; width=&quot;1035&quot; height=&quot;495&quot; /&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center; font-size: 0.9em; color: #5c5c5c; margin: 0 0 24px 0;&quot;&gt;&lt;em&gt;Interactions and deactivation.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Hands-on: what Rankin found on realyield.co.uk&lt;/h2&gt;
&lt;p&gt;Rankin found a likely GA4 tracking break on its first run, and that alone made the setup worthwhile. realyield.co.uk is a UK buy-to-let analysis side project of mine. It is a small site, which makes it a good test: there is not much data to hide behind.&lt;/p&gt;
&lt;h3&gt;The onboarding interview&lt;/h3&gt;
&lt;p&gt;The first message was not a report. It was six questions: domain, competitors, business goals and how SEO contributes to each, priority content areas, keyword focus, and which actions it may take without approval.&lt;/p&gt;
&lt;p style=&quot;text-align: center; margin: 24px 0 4px 0;&quot;&gt;&lt;img style=&quot;width: 100%; max-width: 100%; height: auto; border: 1px solid #d9d9d9; border-radius: 4px;&quot; src=&quot;/link/d15cc8321786490bafba875490a9e404.aspx&quot; alt=&quot;Rankin&#39;s opening message in Team Messages.&quot; width=&quot;950&quot; height=&quot;475&quot; /&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center; font-size: 0.9em; color: #5c5c5c; margin: 0 0 24px 0;&quot;&gt;&lt;em&gt;Rankin&#39;s opening message in Team Messages.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I answered in one message and told it to propose everything first. It played the configuration back, flagged that I had not given a fixed keyword list, and said it had no data yet so would not invent findings.&lt;/p&gt;
&lt;p style=&quot;text-align: center; margin: 24px 0 4px 0;&quot;&gt;&lt;img style=&quot;width: 100%; max-width: 100%; height: auto; border: 1px solid #d9d9d9; border-radius: 4px;&quot; src=&quot;/link/7809126a70d74e6c90156b7ffb6de411.aspx&quot; alt=&quot;My answers, and Rankin&#39;s confirmation of its working configuration.&quot; width=&quot;950&quot; height=&quot;655&quot; /&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center; font-size: 0.9em; color: #5c5c5c; margin: 0 0 24px 0;&quot;&gt;&lt;em&gt;My answers, and Rankin&#39;s confirmation of its working configuration.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;The first snapshot&lt;/h3&gt;
&lt;p&gt;I asked for the last 28 days against the previous 28. The reply came six minutes later, clearly labelled as proposals with nothing actioned. The headline was not a ranking problem at all:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GA4 Organic Search sessions fell 59% (164 against 398) while Search Console clicks fell only 17% (209 against 251).&lt;/li&gt;
&lt;li&gt;Every GA4 channel dropped together: 381 sessions against 934 in total, with Direct, Organic Social and AI Assistant traffic all falling at once.&lt;/li&gt;
&lt;li&gt;Organic sessions ran at 12 to 20 a day until 1 October, then 1 to 7 a day from 2 October. Search Console clicks show no such step. The GA4-to-GSC ratio went from 1.6x to 0.5x overnight.&lt;/li&gt;
&lt;li&gt;It concluded this is probably a tag or consent change around 1 to 2 October, marked it as an unconfirmed hypothesis, and told me to treat GA4 page-level data as unreliable until it is checked.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: center; margin: 24px 0 4px 0;&quot;&gt;&lt;img style=&quot;width: 100%; max-width: 100%; height: auto; border: 1px solid #d9d9d9; border-radius: 4px;&quot; src=&quot;/link/2c27d56e5d6d4536a281daeded7894b0.aspx&quot; alt=&quot;The first half of the snapshot: the tracking flag and the Search Console scoreboard.&quot; width=&quot;950&quot; height=&quot;710&quot; /&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center; font-size: 0.9em; color: #5c5c5c; margin: 0 0 24px 0;&quot;&gt;&lt;em&gt;The first half of the snapshot: the tracking flag and the Search Console scoreboard.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;That is exactly the cross-source reasoning a human analyst should do and usually doesn&#39;t. The raw data backs it up. GA4&#39;s traffic acquisition report shows every channel falling off a cliff at the start of October:&lt;/p&gt;
&lt;p style=&quot;text-align: center; margin: 24px 0 4px 0;&quot;&gt;&lt;img style=&quot;width: 100%; max-width: 100%; height: auto; border: 1px solid #d9d9d9; border-radius: 4px;&quot; src=&quot;/link/a40f459956f9470c963533092055c5c6.aspx&quot; alt=&quot;GA4 traffic acquisition for realyield.co.uk, last 90 days. Every channel steps down at the start of October.&quot; width=&quot;1005&quot; height=&quot;560&quot; /&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center; font-size: 0.9em; color: #5c5c5c; margin: 0 0 24px 0;&quot;&gt;&lt;em&gt;GA4 traffic acquisition for realyield.co.uk, last 90 days. Every channel steps down at the start of October.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Search Console over the same period shows no such step in clicks:&lt;/p&gt;
&lt;p style=&quot;text-align: center; margin: 24px 0 4px 0;&quot;&gt;&lt;img style=&quot;width: 100%; max-width: 100%; height: auto; border: 1px solid #d9d9d9; border-radius: 4px;&quot; src=&quot;/link/17632a1427a54539bef81fc9af4d4426.aspx&quot; alt=&quot;Search Console performance for realyield.co.uk, last three months. Clicks hold steady through October.&quot; width=&quot;1000&quot; height=&quot;500&quot; /&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center; font-size: 0.9em; color: #5c5c5c; margin: 0 0 24px 0;&quot;&gt;&lt;em&gt;Search Console performance for realyield.co.uk, last three months. Clicks hold steady through October.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The Search Console analysis was just as specific. Clicks were within one standard deviation of the daily mean since 1 July (8.7 a day, SD 3.8, against 7.5 now). 41% of the impression loss was a single query, &quot;rent vs buy calculator&quot;, spiking to 6,536 impressions and falling back to 41. The real drift was position: impression-weighted average position went from 16.0 to 21.7 over four weeks, with desktop slipping from 16.6 to 22.4 while mobile improved from 15.4 to 13.6.&lt;/p&gt;
&lt;p&gt;It then broke down top pages and found the anomalies worth acting on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;/tools/personal-vs-ltd-company&lt;/strong&gt; lost 45% of the total click loss on its own. Position held and impressions fell only 9%, so CTR halved from 3.7% to 1.9%. A CTR problem, not a ranking one.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Location pages:&lt;/strong&gt; 270 URLs with impressions (5,053 impressions, 29 clicks), and 90% of them with no clicks at all. &quot;average rent in&quot; queries had 381 impressions at position 9.7 and no clicks.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The core net yield calculator&lt;/strong&gt; sits at position 55.7 with no clicks.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Housekeeping:&lt;/strong&gt; the login subdomain appears in search and should be noindex.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: center; margin: 24px 0 4px 0;&quot;&gt;&lt;img style=&quot;width: 100%; max-width: 100%; height: auto; border: 1px solid #d9d9d9; border-radius: 4px;&quot; src=&quot;/link/c33445ce006044628872b0abe0e687df.aspx&quot; alt=&quot;Top pages, top queries and the other anomalies.&quot; width=&quot;950&quot; height=&quot;580&quot; /&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center; font-size: 0.9em; color: #5c5c5c; margin: 0 0 24px 0;&quot;&gt;&lt;em&gt;Top pages, top queries and the other anomalies.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;The action plan&lt;/h3&gt;
&lt;p&gt;Five prioritised proposals, each with a first step: fix GA4 tracking and add calculator completion as a key event; rework the core calculator page for &quot;rental yield calculator&quot; and &quot;net yield calculator&quot;; pilot a new location page template on six named cities and read CTR after four weeks; recover CTR on the page that halved; and build a manual AI-citation baseline of about 20 questions because no citation tool is connected. It then listed two further opportunities outside my stated priorities and asked for a steer rather than assuming.&lt;/p&gt;
&lt;p style=&quot;text-align: center; margin: 24px 0 4px 0;&quot;&gt;&lt;img style=&quot;width: 100%; max-width: 100%; height: auto; border: 1px solid #d9d9d9; border-radius: 4px;&quot; src=&quot;/link/f3861225c12c4962a7dcc1c21e8193fa.aspx&quot; alt=&quot;AI visibility, the five proposed actions, and the questions it wanted answered.&quot; width=&quot;950&quot; height=&quot;450&quot; /&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center; font-size: 0.9em; color: #5c5c5c; margin: 0 0 24px 0;&quot;&gt;&lt;em&gt;AI visibility, the five proposed actions, and the questions it wanted answered.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;Where it fell short&lt;/h3&gt;
&lt;p&gt;It was honest about it. With no Conductor, Semrush or AI-citation connector, it could not report competitor rankings or citation share, which is half of the &quot;AI Search&quot; in its job title. It also could not find a Team Messages channel to post into, so it put everything in our DM. And at the time of writing none of its four scheduled jobs had run yet, so the daily cadence, memory and self-improvement claims are still untested. I&#39;ll follow up once it has a fortnight of runs behind it.&lt;/p&gt;
&lt;h2&gt;Where they earn their keep&lt;/h2&gt;
&lt;p&gt;The pattern that fits is recurring, data-heavy work that nobody owns and everybody means to do. Teammates are weakest where the work is a one-off, and strongest where the value comes from turning up every day. Here is where I would start with clients, roughly in order of effort.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Search and AEO monitoring for marketing teams without an SEO.&lt;/strong&gt; The SEO &amp;amp; AI Search Analyst gives a daily AEO readiness audit, a keyword digest and anomaly alerts off GA4 and Search Console. For mid-market Optimizely clients who rely on an agency retainer for this, it is a real change in cadence.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Weekly reporting that people actually read.&lt;/strong&gt; The Marketing Analyst replaces the Monday GA4 export. Its 15% channel-movement threshold and &quot;content win of the week&quot; are opinionated defaults, which is what you want from a report.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Content governance on CMS 13 and SaaS.&lt;/strong&gt; The Web Manager scans content at publish time for brand, legal, style and accessibility issues and stages fixes for approval. Pair it with the Translation Specialist for multi-locale sites. Neither needs a third-party connector, so the only real setup is CMS access rights.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Experimentation hygiene.&lt;/strong&gt; The CRO Manager&#39;s daily SRM, zero-conversion and overrun checks are the boring safety net most programmes lack. Its test plans include sample size and runtime, and it stages experiments in Web Experimentation as drafts for QA.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Personalisation programmes that stall after launch.&lt;/strong&gt; The Personalization Strategist runs the whole loop from opportunity to campaign build to impact overview, all staged for review.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Leadership admin.&lt;/strong&gt; The Chief of Staff does daily and weekly briefs, meeting prep and recaps across Microsoft 365 or Google Workspace. Useful, but it competes with Copilot and Gemini, which most clients already pay for.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For partners there is a second angle. A teammate is a packaged, governed role you can hand a client in week one of a build, before their own team is in place. That changes the shape of a launch: the site goes live with an always-on analyst and a content governance check already running.&lt;/p&gt;
&lt;h2&gt;Governance, credits and gotchas&lt;/h2&gt;
&lt;p&gt;The governance model is the strongest part of the design. Teammates are real Opti ID users, so access is granted, audited and revoked like any starter&#39;s. Managers get full access; other teammates see only what conversation guardrails allow; jobs can be switched off per teammate by its manager, or per organisation by an admin, who can also freeze hiring. Pausing keeps history. Deactivating is permanent and deletes the teammate&#39;s memory and context, so pause unless you mean it.&lt;/p&gt;
&lt;p&gt;Credits are the open question. Mark uses pooled credits across eligible products, and Scott Reed&#39;s &lt;a style=&quot;color: #1a5fb4; text-decoration: underline;&quot; href=&quot;/link/8233085025bc4502b3e8322367f77592.aspx&quot;&gt;October state of play&lt;/a&gt; notes that scheduled, webhook and email triggers bill to whoever created the trigger, at an average of 2 credits for chat up to 200 for an XL agent run. The Virtual Teammates docs do not yet say how a teammate&#39;s daily jobs are metered or who pays. A teammate running four daily jobs could be a meaningful line item, so model it before you put one in front of a client.&lt;/p&gt;
&lt;p&gt;The gotchas I hit:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Org-level data, personal teammate.&lt;/strong&gt; GA4 property IDs are registered on the connector app for the whole organisation, so everyone in the org sees the list. Data access still runs through each person&#39;s own Google OAuth by default, so the exposure is the property name and ID, not the data. That changes if an admin turns on the app&#39;s instance-level OAuth option. Think about both before listing client properties in a shared partner sandbox.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No feedback on registration.&lt;/strong&gt; &quot;Add to Mark&quot; in Connect Platform spins, then silently reverts. It had worked; the Mark Connectors page just took a minute to catch up. The enable wizard also does not refresh connector status until you reload it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tool lists disagree.&lt;/strong&gt; The SEO card shows four tools, but enablement asks for only GA4 and Search Console. Conductor and Semrush then appear at hire time as &quot;Set up its tools on the Connectors page first&quot;, and on the teammate&#39;s Tools page as &quot;Contact Admin&quot;, even though Semrush shows as installed on the Connectors page.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Choosing an instance is not access.&lt;/strong&gt; The UI says so clearly, but it is still an extra trip to the Opti ID Admin Center, and for CMS a second trip to Set Access Rights.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OAuth needs a human.&lt;/strong&gt; Every third-party connection is a pop-up a person must complete. Plan this into onboarding sessions with clients rather than assuming an admin can do it alone.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Three names, one product.&lt;/strong&gt; Opal, Agent Platform and Mark all appear in the UI, docs and connector descriptions. Brief clients on this up front.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Verdict&lt;/h2&gt;
&lt;p&gt;Virtual Teammates are worth taking seriously, and worth piloting now on one narrow job. The identity and governance model is properly thought through, the onboarding interview is better than most agency kick-offs, and the first piece of analysis I got back was specific, cautious and correct about the thing that mattered most.&lt;/p&gt;
&lt;p&gt;The weak spots are setup friction and cost visibility. Enablement spans Mark, Connect Platform, Opti ID and the CMS, with status that lags and tool lists that disagree. And until Optimizely documents how a teammate&#39;s scheduled jobs consume credits, I would not promise a client a monthly figure.&lt;/p&gt;
&lt;p&gt;My recommendation for partners: pick one client with a clear recurring gap, usually search monitoring or content governance, hire one teammate with propose-only permissions, and review its output weekly for a month before widening its autonomy. That is how you would onboard a junior hire, and it is the right mental model here too.&lt;/p&gt;
&lt;h2&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a style=&quot;color: #1a5fb4; text-decoration: underline;&quot; href=&quot;https://docs.optimizely.com/agent-platform/docs/virtual-teammates-overview&quot;&gt;Virtual Teammates overview&lt;/a&gt;, Optimizely docs&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #1a5fb4; text-decoration: underline;&quot; href=&quot;https://docs.optimizely.com/agent-platform/docs/enable-virtual-teammates&quot;&gt;Enable Virtual Teammates&lt;/a&gt;, Optimizely docs&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #1a5fb4; text-decoration: underline;&quot; href=&quot;https://support.optimizely.com/hc/en-us/articles/47212988975885-Hire-a-Virtual-Teammate&quot;&gt;Hire a Virtual Teammate&lt;/a&gt;, Optimizely support&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #1a5fb4; text-decoration: underline;&quot; href=&quot;https://support.optimizely.com/hc/en-us/articles/48317514197645-Manage-your-Virtual-Teammate&quot;&gt;Manage your Virtual Teammate&lt;/a&gt;, Optimizely support&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #1a5fb4; text-decoration: underline;&quot; href=&quot;https://support.optimizely.com/hc/en-us/articles/48314434860429-Manage-Virtual-Teammates-for-your-organization&quot;&gt;Manage Virtual Teammates for your organization&lt;/a&gt;, Optimizely support&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #1a5fb4; text-decoration: underline;&quot; href=&quot;https://docs.optimizely.com/agent-platform/release-notes&quot;&gt;2026 release notes&lt;/a&gt;, Optimizely docs&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #1a5fb4; text-decoration: underline;&quot; href=&quot;https://www.optimizely.com/no/field-notes/articles/virtual-teammates-vs-ai-agents-whats-the-difference&quot;&gt;Virtual Teammates vs AI agents&lt;/a&gt;, Optimizely Field Notes&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #1a5fb4; text-decoration: underline;&quot; href=&quot;/link/8233085025bc4502b3e8322367f77592.aspx&quot;&gt;Optimizely Agent Platform: October state of play&lt;/a&gt;, Scott Reed&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #1a5fb4; text-decoration: underline;&quot; href=&quot;https://www.cmswire.com/digital-experience/optimizely-retires-dxp-label-bets-everything-on-agent-platform/&quot;&gt;Optimizely retires DXP label, bets everything on agent platform&lt;/a&gt;, CMSWire&lt;/li&gt;
&lt;/ul&gt;</description>            <guid>https://world.optimizely.com/blogs/Minesh-Shah/Dates/2026/10/virtual-teammates-in-optimizely-mark-a-hands-on-first-look/</guid>            <pubDate>Tue, 27 Oct 2026 09:44:35 GMT</pubDate>           <category>Blog post</category></item><item> <title>I Turned My Tech Bookshelf Into an MCP Server (in .NET)</title>            <link>https://world.optimizely.com/blogs/kennyg/dates/2026/10/i-turned-my-tech-bookshelf-into-an-mcp-server-in-.net</link>            <description>&lt;h2&gt;Overview&lt;/h2&gt;
&lt;p&gt;If you are like me you have been buying tech ebooks for years. Manning early access books, Packt and Apress bundles, Humble Bundles, a few Leanpub books. Mine were scattered across OneDrive, an old Calibre library, a Dropbox export and my Downloads folder. Some of them I owned three times. Some of them I had paid for and never even downloaded.&lt;/p&gt;
&lt;p&gt;A handful of them are about our platform: &lt;em&gt;EPiServer 7 CMS Development&lt;/em&gt;, &lt;em&gt;Episerver: The Missing Manual&lt;/em&gt;, &lt;em&gt;Pro Episerver Commerce&lt;/em&gt; and the CMS Development Fundamentals course book.&lt;/p&gt;
&lt;p&gt;We have been using Claude Code against our CMS 12 / Commerce 14 solution for a while now, and I kept thinking the answers to a lot of my questions were sitting in books I already paid for. I didn&#39;t want &quot;what the model remembers from the internet.&quot; I wanted &lt;em&gt;my&lt;/em&gt; books, with page numbers, so I could go check.&lt;/p&gt;
&lt;p&gt;So I built a small RAG server over them that any MCP client can call. Along the way I hit a problem that may be familiar if you have pointed an AI at Optimizely content.&lt;/p&gt;
&lt;p&gt;(Full disclosure: the part that reads the PDFs and EPUBs is a few Python scripts because the libraries for that are better there. The server that the agent actually talks to is ASP.NET Core on .NET 10. It all runs locally on one machine.)&lt;/p&gt;
&lt;h2&gt;The Mess&lt;/h2&gt;
&lt;p&gt;Before any of this was searchable, here is what I was working with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1,656 ebook files spread across several locations&lt;/li&gt;
&lt;li&gt;609 of them were byte-for-byte duplicates (turns out the old Calibre library was built from the same Dropbox folder)&lt;/li&gt;
&lt;li&gt;Files named &lt;strong&gt;_ - Unknown.pdf&lt;/strong&gt; and a whole series of comics where every PDF&#39;s title was &lt;strong&gt;Layout 1&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;64 Kindle files named only by their ASIN&lt;/li&gt;
&lt;li&gt;Several versions of the same Manning early access (MEAP) book&lt;/li&gt;
&lt;li&gt;An ASP.NET 3.5 book sitting right next to &lt;em&gt;ASP.NET Core in Action, Third Edition&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Point an AI at that pile and you get a confident answer from the wrong edition of the wrong book.&lt;/p&gt;
&lt;h2&gt;Getting It Into Shape&lt;/h2&gt;
&lt;p&gt;Most of the work was here. The search part turned out to be easy.&lt;/p&gt;
&lt;p&gt;First, every file gets scanned into a SQLite catalog (path, size, hash and whatever metadata is in the file). Then duplicates are found by SHA-256 hash, not by file name. Same name doesn&#39;t mean same book and a different name doesn&#39;t mean a different book.&lt;/p&gt;
&lt;p&gt;Next, files get grouped into books by ISBN when there is one, otherwise by title and first author. Then each book gets enriched from Open Library for subjects, publication year and real titles (for the files named things like &lt;strong&gt;appsandserviceswithnet10.pdf&lt;/strong&gt;).&lt;/p&gt;
&lt;p&gt;Then every tech book gets a relevance tier of &lt;strong&gt;current&lt;/strong&gt;, &lt;strong&gt;dated&lt;/strong&gt; or &lt;strong&gt;legacy&lt;/strong&gt;, based on the publication year and the tech versions in the title. I wasn&#39;t ready to delete my old ASP.NET 4 books, so legacy just means they are hidden by default.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/0db175aa443941d7a6b6502b4b916233.aspx?1792984575907&quot; alt=&quot;The Shelf library browser filtered to Episerver, showing four books with a Relevance filter for Current, Dated and Legacy. Three of the four books carry a Dated badge.&quot; width=&quot;800&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The Shelf browser filtered to Episerver. Three of the four books are tagged Dated.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Finally, nothing gets moved on its own. The organizer writes a manifest (move this, remove that duplicate or superseded early access version, here&#39;s why), I read it, and only then does it run. Every move is logged with its old and new path, so I can reverse it by hand. The only things it deletes are byte-identical duplicates (re-hashed first) and older MEAP versions.&lt;/p&gt;
&lt;p&gt;The manifest helped right away. The first dry run had &lt;strong&gt;C PROGRAMMING COOK.pdf&lt;/strong&gt;, &lt;strong&gt;HTML5 Cook.epub&lt;/strong&gt; and &lt;strong&gt;jQuery and JavaScript Phras.pdf&lt;/strong&gt; in it. The regex I wrote to strip &quot;(eBook)&quot; off the end of titles was also eating the end of &quot;Cook&lt;strong&gt;book&lt;/strong&gt;&quot; and &quot;Phrase&lt;strong&gt;book&lt;/strong&gt;&quot;. It also had all of the Locke &amp;amp; Key comics headed for a &lt;strong&gt;_Not books&lt;/strong&gt; folder thanks to that &lt;strong&gt;Layout 1&lt;/strong&gt; title. Both were small fixes, and I found them by reading a CSV instead of wondering weeks later why search results looked odd.&lt;/p&gt;
&lt;h2&gt;The Index&lt;/h2&gt;
&lt;p&gt;Once the files were cleaned up the index was almost boring:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;One passage per PDF page, so citations point to a PDF page you can open (the number printed in the book can be a few pages lower because of front matter). EPUBs get split into roughly 2,000-character chunks.&lt;/li&gt;
&lt;li&gt;SQLite FTS5 with the Porter stemmer, ranked by BM25&lt;/li&gt;
&lt;li&gt;1,119 books and documents (about 1,050 of them tech books) and 315,000 passages in a single file&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;No vector database and no embeddings. Plain keyword search with page numbers is cheap and easy to check, and it works well enough for tech books because the questions are full of exact names like &lt;strong&gt;IContentLoader&lt;/strong&gt;, &lt;strong&gt;IOrderEvents&lt;/strong&gt; or &lt;strong&gt;ISelectionFactory&lt;/strong&gt;. I can add embeddings later if keyword search misses things.&lt;/p&gt;
&lt;h2&gt;The .NET MCP Server&lt;/h2&gt;
&lt;p&gt;The server is an ASP.NET Core app using the official &lt;a href=&quot;https://github.com/modelcontextprotocol/csharp-sdk&quot;&gt;MCP C# SDK&lt;/a&gt; (&lt;strong&gt;ModelContextProtocol.AspNetCore&lt;/strong&gt; 2.3.0) and &lt;strong&gt;Microsoft.Data.Sqlite&lt;/strong&gt;. The whole thing is around 250 lines.&lt;/p&gt;
&lt;p&gt;The HTTP part of Program.cs is short (the real file also has a &lt;strong&gt;--stdio&lt;/strong&gt; branch, covered below):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSingleton&amp;lt;BookIndex&amp;gt;();
builder.Services.AddMcpServer()
    .WithHttpTransport()
    .WithTools&amp;lt;BookTools&amp;gt;();

var app = builder.Build();
app.MapMcp(&quot;/mcp&quot;);
app.Run();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It exposes three read-only tools: &lt;strong&gt;search_books&lt;/strong&gt;, &lt;strong&gt;read_passage&lt;/strong&gt; and &lt;strong&gt;find_books&lt;/strong&gt;. The tool description is the only documentation the agent sees, so I use it to explain what the tiers mean:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;[McpServerTool(Name = &quot;search_books&quot;, ReadOnly = true)]
[Description(&quot;&quot;&quot;
    Full-text search across a personal library of ~1,100 books and documents ... Books marked legacy
    (outdated technology, e.g. ASP.NET Web Forms or books published in 2012 or earlier) are excluded unless
    includeLegacy is true; passages from &#39;dated&#39; books may describe older APIs, so check them
    against current docs.
    &quot;&quot;&quot;)]
public string SearchBooks(string query, int limit = 8, bool includeLegacy = false)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Agents search like developers, which means queries like &lt;strong&gt;IContentLoader.Get&amp;lt;T&amp;gt;&lt;/strong&gt;. That isn&#39;t valid FTS5 syntax and would throw an error. So the search tries the query as written, then all of the words, then any of the words:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;private static IEnumerable&amp;lt;string&amp;gt; MatchExpressions(string query)
{
    yield return query;                                    // as written
    var words = Regex.Matches(query, @&quot;[\p{L}\p{N}_]+&quot;).Select(m =&amp;gt; $&quot;\&quot;{m.Value}\&quot;&quot;).ToList();
    if (words.Count == 0) yield break;
    yield return string.Join(&quot; &quot;, words);                  // every word
    if (words.Count &amp;gt; 1) yield return string.Join(&quot; OR &quot;, words);   // any word
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One gotcha in case you try something similar: the catalog is in a second SQLite file, so each connection runs &lt;strong&gt;ATTACH DATABASE ... AS cat&lt;/strong&gt;. &lt;strong&gt;Microsoft.Data.Sqlite&lt;/strong&gt; pools connections, so the second request got a connection that already had &lt;strong&gt;cat&lt;/strong&gt; attached and threw &lt;em&gt;&quot;database cat is already in use&quot;&lt;/em&gt;. Check &lt;strong&gt;pragma_database_list&lt;/strong&gt; before you attach.&lt;/p&gt;
&lt;p&gt;Searches take a few milliseconds when the terms match (a loose any-word query can take tens). It only listens on localhost and the databases are opened read-only.&lt;/p&gt;
&lt;h2&gt;The Version Problem&lt;/h2&gt;
&lt;p&gt;Here are the top three of the seven passages &lt;strong&gt;search_books&lt;/strong&gt; returns for &lt;strong&gt;IContentLoader.Get&amp;lt;T&amp;gt;&lt;/strong&gt; (trimmed for space):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1. Episerver The Missing Manual &amp;mdash; Jon D Jones (2021), p. 41
   &amp;hellip; IContentLoader is a read-only API, meaning it only exposes methods t&amp;hellip;
2. Episerver Commerce: A problem - solution approach &amp;mdash; Quan Mai (2014), p. 151 [dated: may describe older APIs]
   &amp;hellip; this(ServiceLocator.Current.GetInstance&amp;lt;IContentLoader&amp;gt;()) &amp;hellip;
3. EPiServer 7 CMS Development &amp;mdash; Joel Abrahamsson (2013), p. 97 [dated: may describe older APIs]
   &amp;hellip; IContentLoader.Get&amp;lt;T&amp;gt;(ContentReference) : T Retrieves a &amp;hellip;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;All three are right for when they were written. But two of them would steer a CMS 12 developer the wrong way, because &lt;strong&gt;ServiceLocator.Current.GetInstance&amp;lt;&amp;gt;()&lt;/strong&gt; is exactly the pattern we are trying to get rid of on CMS 12 in favor of constructor injection. Without the tier flag the agent sees three sources that agree with each other and goes with it. With the flag it calls it out: the 2013 book uses the service locator, on CMS 12 inject it.&lt;/p&gt;
&lt;p&gt;This isn&#39;t really about books. Optimizely sites live for years. Content, internal wikis, old blog posts (yes, including mine) and codebases have EPiServer 7, CMS 11 and CMS 12 answers sitting side by side. If you point an AI at that, whether it&#39;s Opal, Copilot or something you built yourself, it will happily hand you the 2013 answer if the 2013 answer matches your words best.&lt;/p&gt;
&lt;p&gt;A few things that worked for me:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I cleaned up and deduped before indexing. Duplicate content doesn&#39;t just take up space, it gets two votes.&lt;/li&gt;
&lt;li&gt;I tagged things by version and freshness, and archived instead of deleting. My old ASP.NET books are still there, they are just out of the default search.&lt;/li&gt;
&lt;li&gt;I put the tier in the result, not just in the ranking. The agent can only say &quot;this might be outdated&quot; if the result says so.&lt;/li&gt;
&lt;li&gt;I dry-ran anything that writes, and read the manifest.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Hooking It Up&lt;/h2&gt;
&lt;p&gt;The server can run two ways. For Claude Code I use stdio, which means Claude Code starts the server itself whenever it needs it and there&#39;s nothing to remember to run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;dotnet publish -c Release -o publish
claude mcp add shelf -s user -- dotnet C:\path\to\publish\ShelfServer.dll --stdio --Shelf:DataDir=C:\path\to\data
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(One gotcha there: .NET&#39;s command line config treats &lt;strong&gt;--stdio&lt;/strong&gt; as a key that wants a value, so it swallowed the next argument, which was my data folder. I strip &lt;strong&gt;--stdio&lt;/strong&gt; out before handing the args to the host builder.)&lt;/p&gt;
&lt;p&gt;For VS Code, run the server with &lt;strong&gt;dotnet run&lt;/strong&gt; (it listens on localhost:5199) and add a &lt;strong&gt;.vscode/mcp.json&lt;/strong&gt; to your solution:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{ &quot;servers&quot;: { &quot;shelf&quot;: { &quot;type&quot;: &quot;http&quot;, &quot;url&quot;: &quot;http://localhost:5199/mcp&quot; } } }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then from inside your CMS 12 solution ask something like &lt;em&gt;&quot;How should I load a page by ContentReference in a CMS 12 view component? Cite my books.&quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I asked exactly that, ending with &quot;Use my book library and cite book and page.&quot;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/932c9b4e6ed24d8eaca1206e3a79915b.aspx&quot; alt=&quot;Claude Code in an empty folder answering the question with three cited passages from the book library.&quot; width=&quot;800&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Claude Code answering from the shelf server. Page numbers are PDF pages.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The agent made 8 &lt;strong&gt;search_books&lt;/strong&gt; calls and quoted three passages. I checked each quote against the index:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;ASP.NET Core Recipes&lt;/em&gt; by John Ciliberti (2017), p. 148: in a view component, dependencies are passed in through the constructor, the same way as in a controller.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href=&quot;https://leanpub.com/episerverthemissingmanual&quot;&gt;Episerver: The Missing Manual&lt;/a&gt;&lt;/em&gt; by &lt;a href=&quot;/link/2a38bee27a114de9acfadea92a56d1a8.aspx&quot;&gt;Jon D Jones&lt;/a&gt; (2021), p. 42: constructor-injecting &lt;strong&gt;IContentLoader&lt;/strong&gt; is how you should access the Episerver APIs.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Episerver: The Missing Manual&lt;/em&gt; (2021), p. 41: &lt;strong&gt;Get&amp;lt;T&amp;gt;&lt;/strong&gt; with a specific page type saves you from casting. The same page says &lt;strong&gt;ServiceLocator&lt;/strong&gt; hides dependencies and makes code harder to test.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The answer: inject &lt;strong&gt;IContentLoader&lt;/strong&gt; in the view component&#39;s constructor and call &lt;strong&gt;Get&amp;lt;T&amp;gt;&lt;/strong&gt; with the &lt;strong&gt;ContentReference&lt;/strong&gt;. It combined a general ASP.NET Core book (for view components) with an Optimizely book (for &lt;strong&gt;IContentLoader&lt;/strong&gt;), which is what I wanted.&lt;/p&gt;
&lt;p&gt;Answers change from run to run. In an earlier run of the same question the agent cited a page that was one off from the real one. That is why I keep page numbers in the output: check them before you rely on them.&lt;/p&gt;
&lt;p&gt;Thank you to &lt;a href=&quot;/link/2925280c8c9c484db451cfe4b48f709e.aspx&quot;&gt;Quan Mai&lt;/a&gt;, &lt;a href=&quot;/link/2a38bee27a114de9acfadea92a56d1a8.aspx&quot;&gt;Jon D Jones&lt;/a&gt;, &lt;a href=&quot;/link/85fb228498694662aa020cc0924733cc.aspx&quot;&gt;Joel Abrahamsson&lt;/a&gt; (author of &lt;em&gt;&lt;a href=&quot;https://leanpub.com/episerver-7-cms-development&quot;&gt;EPiServer 7 CMS Development&lt;/a&gt;&lt;/em&gt;) and John Ciliberti for writing the books this post draws on.&lt;/p&gt;
&lt;h2&gt;A Few Caveats&lt;/h2&gt;
&lt;p&gt;This only works for DRM-free books. My Kindle and other DRM-locked purchases are in the catalog but aren&#39;t searchable (the indexer only reads DRM-free PDF and EPUB), and I&#39;m not stripping DRM to change that. Keyword search will miss some things that embeddings would catch. And &quot;dated&quot; is a guess based on publication years and version numbers in titles, so it&#39;s wrong sometimes. It is still better than an agent with no idea which decade a passage came from.&lt;/p&gt;
&lt;p&gt;If you would like to see the server code, let me know in the comments and I will share it.&lt;/p&gt;
&lt;p&gt;Is anyone else pointing an AI at their Optimizely content and running into the same version problem? How are you tagging what&#39;s current? Let me know in the comments.&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/kennyg/dates/2026/10/i-turned-my-tech-bookshelf-into-an-mcp-server-in-.net</guid>            <pubDate>Mon, 26 Oct 2026 21:45:00 GMT</pubDate>           <category>Blog post</category></item><item> <title>Time Zone-Aware Scheduled Publishing in Optimizely CMS 13</title>            <link>https://world.optimizely.com/blogs/mardy-abdelkareem/dates/2026/10/time-zone-aware-scheduled-publishing-in-optimizely-cms-13/</link>            <description>&lt;p&gt;Scheduling content for audiences in different time zones is a common requirement, especially for websites managed by editors across multiple regions.&lt;/p&gt;
&lt;p&gt;Optimizely CMS already supports scheduled publishing, but editors working across time zones may need to calculate the equivalent time in their own time zone before scheduling content.&lt;/p&gt;
&lt;p&gt;For example, an editor in Cairo might want a page to go live at 5:00 PM in London. Without a target time-zone selector, the editor needs to calculate the corresponding local time, taking daylight saving time changes into account.&lt;/p&gt;
&lt;p&gt;To simplify this workflow, I built &lt;strong&gt;Time Zone Publishing&lt;/strong&gt;, a lightweight, open-source add-on for Optimizely CMS 13.&lt;/p&gt;
&lt;h2&gt;How it works&lt;/h2&gt;
&lt;p&gt;The add-on introduces a new &lt;strong&gt;Schedule Publish by Time Zone&lt;/strong&gt; command alongside the existing scheduling option in the CMS Publish menu.&lt;/p&gt;
&lt;figure style=&quot;margin: 24px auto; text-align: center;&quot;&gt;
&lt;figcaption style=&quot;margin-top: 8px; font-size: 14px; color: #666; line-height: 1.5;&quot;&gt;&lt;img src=&quot;/link/0df4bf9ef957473d91f8607906568429.aspx&quot; /&gt;&lt;br /&gt;Figure 1: The new scheduling command in the native CMS Publish menu.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Editors can access the feature directly from the familiar publishing interface without navigating to a separate tool.&lt;/p&gt;
&lt;p&gt;Selecting the command opens a scheduling dialog where editors can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Search for and select an IANA time zone.&lt;/li&gt;
&lt;li&gt;Choose the intended publication date and time in that zone.&lt;/li&gt;
&lt;li&gt;See the equivalent time in their browser&#39;s local time zone.&lt;/li&gt;
&lt;li&gt;Confirm the schedule using the existing CMS publishing mechanism.&lt;/li&gt;
&lt;/ul&gt;
&lt;figure style=&quot;margin: 24px auto; text-align: center;&quot;&gt;
&lt;figcaption style=&quot;margin-top: 8px; font-size: 14px; color: #666; line-height: 1.5;&quot;&gt;&lt;img src=&quot;/link/4422c9d809354239aa0f2fa5bee5a52c.aspx&quot; /&gt;&lt;br /&gt;Figure 2: Scheduling publication in London with a local-time preview for Cairo.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;In this example, the page is scheduled for &lt;strong&gt;October 30, 2026, at 5:00 PM in London&lt;/strong&gt;, which corresponds to &lt;strong&gt;7:00 PM in Cairo&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Once the editor confirms the schedule, the add-on converts the selected local date and time to UTC and uses the native CMS scheduled publishing functionality.&lt;/p&gt;
&lt;p&gt;This keeps the scheduling workflow aligned with the platform rather than introducing a separate background job or publishing service.&lt;/p&gt;
&lt;h2&gt;Handling daylight saving time&lt;/h2&gt;
&lt;p&gt;One important consideration when scheduling across time zones is daylight saving time (DST).&lt;/p&gt;
&lt;p&gt;During DST transitions, certain local times may be:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Nonexistent:&lt;/strong&gt; When clocks move forward and skip a period of time.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ambiguous:&lt;/strong&gt; When clocks move backward and a local time occurs twice.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The add-on validates both scenarios and prevents editors from confirming a schedule with an invalid or ambiguous local time.&lt;/p&gt;
&lt;h2&gt;Key features&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Schedule publishing using a selected target time zone.&lt;/li&gt;
&lt;li&gt;Searchable IANA time-zone selection.&lt;/li&gt;
&lt;li&gt;Automatic conversion to UTC.&lt;/li&gt;
&lt;li&gt;Preview of the equivalent browser-local time.&lt;/li&gt;
&lt;li&gt;Validation of daylight saving time transitions.&lt;/li&gt;
&lt;li&gt;Integration with the native CMS Publish menu.&lt;/li&gt;
&lt;li&gt;No additional scheduling service or background job required.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Compatibility and installation&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Time Zone Publishing v0.1.1&lt;/strong&gt; targets &lt;strong&gt;.NET 10&lt;/strong&gt; and supports &lt;strong&gt;Optimizely CMS 13.0.0 or later&lt;/strong&gt;. The add-on has been successfully tested with &lt;strong&gt;CMS 13.0.0 and 13.4.0&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The add-on is available on the &lt;strong&gt;Optimizely NuGet feed&lt;/strong&gt; and can be installed directly into an existing CMS 13 application.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Installation:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;dotnet add package Mardy.Optimizely.TimeZonePublishing --version 0.1.1&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Source code&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a style=&quot;color: #0066cc; text-decoration: underline;&quot; href=&quot;https://github.com/mardy-awd/Optimizely.TimeZonePublishing&quot;&gt;Optimizely.TimeZonePublishing&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;NuGet package:&lt;/strong&gt; &lt;a style=&quot;color: #0066cc; text-decoration: underline;&quot; href=&quot;https://nuget.optimizely.com/packages/mardy.optimizely.timezonepublishing/0.1.1&quot;&gt;Optimizely NuGet feed&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The add-on is designed for CMS 13 and is not intended for direct installation into CMS 12 applications.&lt;/p&gt;
&lt;p&gt;Feedback, suggestions, and contributions are welcome.&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/mardy-abdelkareem/dates/2026/10/time-zone-aware-scheduled-publishing-in-optimizely-cms-13/</guid>            <pubDate>Mon, 26 Oct 2026 07:02:50 GMT</pubDate>           <category>Blog post</category></item></channel>
</rss>