<?xml version="1.0" encoding="utf-8"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/"><channel><language>en</language><title>Blog posts by Daniel Halse</title> <link>https://world.optimizely.com/blogs/daniel-halse/</link><description></description><ttl>60</ttl><generator>Optimizely World</generator><item> <title>Graph access with only JS and Fetch</title>            <link>https://world.optimizely.com/blogs/daniel-halse/dates/2026/2/graph-access-with-only-js-and-fetch/</link>            <description>&lt;p&gt;Postman is a popular tool for testing APIs. However, when testing an API like Optimizely Graph that I will be consuming in the front-end I prefer to do this with JS + Fetch.&lt;/p&gt;
&lt;p&gt;This allows me to identify any quirks involved in consuming the API in JavaScript. The code can then be passed to the front-end team for use in the relevant framework, and JSON results can be saved for Storybook mock data.&lt;/p&gt;
&lt;h2&gt;Example&lt;/h2&gt;
&lt;pre class=&quot;giallo&quot;&gt;&lt;code&gt;&lt;span class=&quot;giallo-l&quot;&gt;import projectConfig from &quot;../../CMS12/appsettings.json&quot; assert { type: &quot;json&quot; };&lt;/span&gt;

&lt;span class=&quot;giallo-l&quot;&gt;// Define the GraphQL query to fetch StandardPages in the &quot;Acme&quot; category with their associated form templates&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;const formQuery =&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;`fragment FormContainerBlock on FormContainerBlock { __typename FormRenderTemplate }&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;query MyQuery { &lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;    StandardPage( where: { PrimaryCategory: { Name: { eq: &quot;Acme&quot; } } } ) &lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;    { items { Name Url MainContent { ContentLink { Expanded { ...FormContainerBlock } } } } }&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;}`&lt;/span&gt;

&lt;span class=&quot;giallo-l&quot;&gt;// Wrap the query in the expected JSON structure for the Content Graph API Request Body&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;const queryWrapper = `{&quot;query&quot;:${JSON.stringify(formQuery)}}`;&lt;/span&gt;

&lt;span class=&quot;giallo-l&quot;&gt;// Public key access - Review security implications before using outside of local development&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;const publicKey = projectConfig.Optimizely.ContentGraph.SingleKey;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;const graphUrl = `https://cg.optimizely.com/content/v2?auth=${publicKey}`;&lt;/span&gt;

&lt;span class=&quot;giallo-l&quot;&gt;const outputData = (data) =&amp;gt; {&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;    const formPages = data.StandardPage.items&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;        .map(i =&amp;gt; { &lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;            return { &lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;                name: i.Name, &lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;                url: i.Url, &lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;                forms: i.MainContent&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;                    .filter(b =&amp;gt; b.ContentLink.Expanded.FormRenderTemplate)&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;                    .map(b =&amp;gt; b.ContentLink.Expanded.FormRenderTemplate) &lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;                }; &lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;        })&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;        .filter(i =&amp;gt; i.forms?.length);&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;    formPages.forEach(page =&amp;gt; page.forms.forEach(f =&amp;gt; console.log(f)));&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;giallo-l&quot;&gt;const executeQuery = async () =&amp;gt; {&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;    return fetch(graphUrl, {&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;        method: &quot;POST&quot;,&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;        headers: { &quot;Content-Type&quot;: &quot;application/json&quot; },&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;        body: queryWrapper&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;    })&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;    .then((response) =&amp;gt; {&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;        if (response.status &amp;gt;= 400) {&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;            throw new Error(`Error fetching data`, {cause: response});&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;        } else {&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;         return response.json();&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;        }&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;    })&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;    .then((data) =&amp;gt; outputData(data.data))&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;    .catch(async error =&amp;gt; {&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;        if (error.cause.status === 400) {&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;            // Graph returns JSON on most errors. Normally this is caused by a query syntax error.&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;            const result = await error.cause.json();&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;            console.log(`CODE: ${result.code}`);&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;            console.log(`DETAILS:\n${JSON.stringify(result.details)}`);&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;        }&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;    })&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;giallo-l&quot;&gt;const result = await executeQuery();&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;console.log(&quot;Completed&quot;);&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Skip to&amp;nbsp;&lt;a href=&quot;https://tech.halse.me.uk/graph-fetch-js/#breakdown&quot;&gt;breakdown&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;How to run&lt;/h3&gt;
&lt;p&gt;First of all there are some prerequisites:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Access to an Optimizely Graph index.&lt;/li&gt;
&lt;li&gt;Request a developer index.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We only need a public key for this example.&lt;/p&gt;
&lt;h3&gt;How to debug in VS Code&lt;/h3&gt;
&lt;p&gt;We can setup VS Code to debug the JavaScript. This gives us the usual access to variables to inspect and review.&lt;/p&gt;
&lt;h4&gt;Debug Current File&lt;/h4&gt;
&lt;p&gt;This should be added to the launch.json file in the .vscode folder in your source root. This allows you to run and debug the currently viewed JavaScript file.&lt;/p&gt;
&lt;pre class=&quot;giallo&quot;&gt;&lt;code&gt;&lt;span class=&quot;giallo-l&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;    &quot;version&quot;: &quot;0.2.0&quot;,&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;      &quot;configurations&quot;: [&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;        {&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;          &quot;type&quot;: &quot;node&quot;,&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;          &quot;request&quot;: &quot;launch&quot;,&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;          &quot;name&quot;: &quot;Launch Current Opened File&quot;,&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;          &quot;program&quot;: &quot;${file}&quot;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;        }&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;      ]&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt; }&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you are running a front-end framework then you will typically need to run the framework and attach the debugging using a standard configuration.&lt;/p&gt;
&lt;h2&gt;Breakdown&lt;/h2&gt;
&lt;p&gt;So I will do a breakdown of what is going on in this code.&lt;/p&gt;
&lt;h3&gt;Settings and Variables&lt;/h3&gt;
&lt;pre class=&quot;giallo&quot;&gt;&lt;code&gt;&lt;span class=&quot;giallo-l&quot;&gt;import projectConfig from &quot;../../CMS12/appsettings.json&quot; assert { type: &quot;json&quot; };&lt;/span&gt;

&lt;span class=&quot;giallo-l&quot;&gt;// Define the GraphQL query to fetch StandardPages in the &quot;Acme&quot; category with their associated form templates&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;const formQuery =&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;`fragment FormContainerBlock on FormContainerBlock { __typename FormRenderTemplate }&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;query MyQuery { &lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;    StandardPage( where: { PrimaryCategory: { Name: { eq: &quot;Acme&quot; } } } ) &lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;    { items { Name Url MainContent { ContentLink { Expanded { ...FormContainerBlock } } } } }&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;}`&lt;/span&gt;

&lt;span class=&quot;giallo-l&quot;&gt;// Wrap the query in the expected JSON structure for the Content Graph API Request Body&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;const queryWrapper = `{&quot;query&quot;:${JSON.stringify(formQuery)}}`;&lt;/span&gt;

&lt;span class=&quot;giallo-l&quot;&gt;// Public key access - Review security implications before using outside of local development&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;const publicKey = projectConfig.Optimizely.ContentGraph.SingleKey;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;const graphUrl = `https://cg.optimizely.com/content/v2?auth=${publicKey}`;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;First I am importing the config file from a project that contains the usual Optimizely Graph settings. You will want to target one with the developer settings if you have multiple configs.&lt;/p&gt;
&lt;p&gt;Then we have the GraphQL query. This can be copy and pasted from the GraphiQL page. This example query finds the Form Blocks within the Main Content of the Standard Page Type. This was written for the Headless Forms BETA which nested the form data in a property of the Form Container Block.&lt;/p&gt;
&lt;p&gt;The wrapper builds the standard request body that Optimizely Graph expects.&lt;/p&gt;
&lt;p&gt;Finally we extract the single key from the shared config and build the URL.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;CAUTION: See&amp;nbsp;&lt;a href=&quot;https://tech.halse.me.uk/graph-fetch-js/#security-considerations&quot;&gt;security concerns&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Render Function&lt;/h3&gt;
&lt;pre class=&quot;giallo&quot;&gt;&lt;code&gt;&lt;span class=&quot;giallo-l&quot;&gt;const outputData = (data) =&amp;gt; {&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;    const formPages = data.StandardPage.items&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;        .map(i =&amp;gt; { &lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;            return { &lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;                name: i.Name, &lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;                url: i.Url, &lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;                forms: i.MainContent&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;                    .filter(b =&amp;gt; b.ContentLink.Expanded.FormRenderTemplate)&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;                    .map(b =&amp;gt; b.ContentLink.Expanded.FormRenderTemplate) &lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;                }; &lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;        })&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;        .filter(i =&amp;gt; i.forms?.length);&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;    formPages.forEach(page =&amp;gt; page.forms.forEach(f =&amp;gt; console.log(f)));&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This function handles the Graph results data. As we are working in JavaScript and the results are JSON we can work with this dynamically. Using the debugger we can inspect the data structure then access it as needed. In this example I am drilling down to the FromRenderTemplate. In the Headless Forms beta this was a blob of JSON containing all of the form elements and form settings.&lt;/p&gt;
&lt;p&gt;For my testing I normally output to the console which will show up in VS Code. I would experiment mapping the data here using&amp;nbsp;&lt;code&gt;JSON.stringify()&lt;/code&gt;&amp;nbsp;to dump data to console or view via the debugger inspector.&lt;/p&gt;
&lt;h3&gt;Fetch -&amp;gt; Then -&amp;gt; Catch Chain Function&lt;/h3&gt;
&lt;pre class=&quot;giallo&quot;&gt;&lt;code&gt;&lt;span class=&quot;giallo-l&quot;&gt;const executeQuery = async () =&amp;gt; {&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;    return fetch(graphUrl, {&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;        method: &quot;POST&quot;,&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;        headers: { &quot;Content-Type&quot;: &quot;application/json&quot; },&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;        body: queryWrapper&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;    })&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;    .then((response) =&amp;gt; {&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;        if (response.status &amp;gt;= 400) {&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;            throw new Error(`Error fetching data`, {cause: response});&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;        } else {&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;         return response.json();&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;        }&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;    })&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;    .then((data) =&amp;gt; outputData(data.data))&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;    .catch(async error =&amp;gt; {&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;        if (error.cause.status === 400) {&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;            // Graph returns JSON on most errors. Normally this is caused by a query syntax error.&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;            const result = await error.cause.json();&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;            console.log(`CODE: ${result.code}`);&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;            console.log(`DETAILS:\n${JSON.stringify(result.details)}`);&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;        }&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;    })&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a fairly standard API call chain. If you drop the debugging it&#39;s very simple but we will get GraphQL syntax errors while experimenting, so this detects and logs these.&lt;/p&gt;
&lt;h4&gt;Fetch&lt;/h4&gt;
&lt;p&gt;The&amp;nbsp;&lt;code&gt;fetch&lt;/code&gt;&amp;nbsp;sends the POST call with the wrapped query to a public URL. If you have secured your Graph index then you will also need to add an authentication header in the Fetch call.&lt;/p&gt;
&lt;h5&gt;For example:&lt;/h5&gt;
&lt;pre class=&quot;giallo&quot;&gt;&lt;code&gt;&lt;span class=&quot;giallo-l&quot;&gt;return fetch(graphUrl, {&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;        method: &quot;POST&quot;,&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;        headers: { &lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;            &quot;Content-Type&quot;: &quot;application/json&quot;,&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;            &quot;Authorization&quot;: `Bearer ${projectConfig.Authorization_Token}` &lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;        },&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;        body: queryWrapper&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;    })&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you are working with an API that has a seperate call to request authorization then do that Fetch call, extract the token and pass to the main Fetch call. In production code you will want to cache the key based on lifetime the API states.&lt;/p&gt;
&lt;h4&gt;Then - Response&lt;/h4&gt;
&lt;p&gt;The first&amp;nbsp;&lt;code&gt;then&lt;/code&gt;&amp;nbsp;checks the status code. Most APIs will return data on 4xx responses but this will often be a different format to the working response. So here we raise an error so we can handle it separately.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;response.JSON()&lt;/code&gt;&amp;nbsp;grabs the body for the success path.&lt;/p&gt;
&lt;h3&gt;Then - Data&lt;/h3&gt;
&lt;p&gt;Here we delegate the data to the output function.&lt;/p&gt;
&lt;h3&gt;Catch&lt;/h3&gt;
&lt;p&gt;In the&amp;nbsp;&lt;code&gt;catch&lt;/code&gt;&amp;nbsp;function we expect to read the GraphQL errors so we log the data to the console.&lt;/p&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;This process can be used for any API you would consume on the front-end.&lt;/p&gt;
&lt;p&gt;Graph is a good example as both the queries and data responses can be complicated and verbose. You will find nested data has multiple wrappers before you hit the expected properties. Testing from JS you can also see how missing data affects the use of the data.&lt;/p&gt;
&lt;h3&gt;Next Steps&lt;/h3&gt;
&lt;p&gt;Next time I will expand this explanation out to TypeScript with simple types that allow dynamic processing of child objects in data and strong typing for the rendering in templates.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/danielhalse/tech-halse/blob/main/2026/Series-Graph/01-fetch.js&quot;&gt;Source File&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://optilearningcentre.adayinthelife.pro/courses/graph&quot;&gt;Optimizely Graph Learning Centre&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.developers.optimizely.com/platform-optimizely/docs/overview-of-optimizely-graph&quot;&gt;Optimizely Graph Dev Guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Security Considerations&lt;/h2&gt;
&lt;p&gt;Note that many production implementations will want to control and obfuscate access to the graph data. This can be done in a number of ways. For example pushing the keys and access to Graph to server components or SSG the page or site.&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/daniel-halse/dates/2026/2/graph-access-with-only-js-and-fetch/</guid>            <pubDate>Wed, 04 Feb 2026 17:15:08 GMT</pubDate>           <category>Blog post</category></item><item> <title>How to run Optimizely CMS on VS Code Dev Containers</title>            <link>https://world.optimizely.com/blogs/daniel-halse/dates/2026/1/how-to-run-optimizely-cms-on-vs-code-dev-containers/</link>            <description>&lt;p&gt;VS Code Dev Containers is an extension that allows you to use a Docker container as a full-featured development environment. Instead of installing tools like DotNet, SQL and Optimizely CMS directly on your local machine, you run them inside a container that VS Code connects to.&lt;/p&gt;
&lt;p&gt;For this example I set up an Optimizely CMS Dev Container on my M3 MacBook to keep everything isolated from my personal system.&lt;/p&gt;
&lt;h2&gt;Prerequisites&lt;/h2&gt;
&lt;p&gt;Before we start with we need to install a few prerequisite tools:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://code.visualstudio.com/download&quot;&gt;VS Code&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.docker.com/products/docker-desktop/&quot;&gt;Docker Desktop&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers&quot;&gt;Dev Containers Extension for VS Code&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Start Docker Desktop and VS Code. This tutorial is done from VS Code and Terminal sessions within VS Code.&lt;/p&gt;
&lt;h2&gt;Quick Start&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Click the container icon in the bottom left of VS Code.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tech.halse.me.uk/images/2026-01-29-001.png&quot; alt=&quot;Container Icon&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Select &quot;New Dev Container...&quot;&amp;nbsp;&lt;img src=&quot;https://tech.halse.me.uk/images/2026-01-29-002.png&quot; alt=&quot;New Container List&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Choose the C# (.NET) devcontainer&amp;nbsp;&lt;img src=&quot;https://tech.halse.me.uk/images/2026-01-29-003.png&quot; alt=&quot;Container List&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Then confirm that you want to create the dev container.&amp;nbsp;&lt;img src=&quot;https://tech.halse.me.uk/images/2026-01-29-004.png&quot; alt=&quot;Confirm&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If this is your first container of this type it will take a while to download. When it has finished you can open a terminal in VS Code and verify the file structure is different to your local PC.&amp;nbsp;&lt;code&gt;ls /&lt;/code&gt;&amp;nbsp;for example. If you open a terminal on your host system that will still be your local PC.&lt;/p&gt;
&lt;h2&gt;Missing Features&lt;/h2&gt;
&lt;p&gt;We now have an isolated .NET development environment in a Dev Container. However we are missing some features to work with CMS 12 and 13 preview.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;First we install additional .NET SDKs:
&lt;pre class=&quot;giallo&quot;&gt;&lt;code&gt;&lt;span class=&quot;giallo-l&quot;&gt;wget https://dot.net/v1/dotnet-install.sh -O dotnet-install.sh&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;chmod +x dotnet-install.sh&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;dotnet-install.sh --channel 8.0 --install-dir /usr/share/dotnet&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;dotnet-install.sh --channel 10.0 --install-dir /usr/share/dotnet&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;dotnet --list-sdks&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;Next we add the Optimizely tools:
&lt;pre class=&quot;giallo&quot;&gt;&lt;code&gt;&lt;span class=&quot;giallo-l&quot;&gt;dotnet new install EPiServer.Templates&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;dotnet dev-certs https --trust&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;dotnet tool install EPiServer.Net.Cli --global --add-source https://nuget.optimizely.com/feed/packages.svc/&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;We can now create a site and rebuild the container environment when needed. However we will need to rerun these commands after rebuilding the container so lets integrate them into a new devcontainer config.&lt;/p&gt;
&lt;h2&gt;Dev Container JSON&lt;/h2&gt;
&lt;p&gt;You will find this configuration file in .devcontainer above .github&lt;/p&gt;
&lt;p&gt;Replace your devcontainer.json content with this example:&lt;/p&gt;
&lt;pre class=&quot;giallo&quot;&gt;&lt;code&gt;&lt;span class=&quot;giallo-l&quot;&gt;// For format details, see https://aka.ms/devcontainer.json. For config options, see the&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;// README at: https://github.com/devcontainers/templates/tree/main/src/dotnet&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;    &quot;name&quot;: &quot;Optimizely (.NET)&quot;,&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;    // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;    &quot;image&quot;: &quot;mcr.microsoft.com/devcontainers/base:ubuntu&quot;,&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;    &quot;runArgs&quot;: [&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;        &quot;--network=host&quot;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;    ],&lt;/span&gt;

&lt;span class=&quot;giallo-l&quot;&gt;    // Features to add to the dev container. More info: https://containers.dev/features.&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;    &quot;features&quot;: {&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;        &quot;ghcr.io/devcontainers/features/dotnet:2.4.2&quot;: {&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;            &quot;version&quot;: &quot;8.0&quot;,&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;            &quot;additionalVersions&quot;: [&quot;9.0, 10.0&quot;]&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;        }&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;    },&lt;/span&gt;

&lt;span class=&quot;giallo-l&quot;&gt;    // Use &#39;forwardPorts&#39; to make a list of ports inside the container available locally.&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;    &quot;forwardPorts&quot;: [5000],&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;    &quot;portsAttributes&quot;: {&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;        &quot;5000&quot;: {&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;            &quot;protocol&quot;: &quot;https&quot;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;        }&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;    },&lt;/span&gt;

&lt;span class=&quot;giallo-l&quot;&gt;    // Use &#39;postCreateCommand&#39; to run commands after the container is created.&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;    &quot;postCreateCommand&quot;: {&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;        &quot;Templates&quot;: &quot;/usr/bin/dotnet new install EPiServer.Templates &amp;amp;&amp;amp; /usr/bin/dotnet dev-certs https --trust&quot;,&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;        &quot;ToolInstall&quot;: &quot;/usr/bin/dotnet tool install EPiServer.Net.Cli --global --add-source https://nuget.optimizely.com/feed/packages.svc/&quot;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;    }&lt;/span&gt;

&lt;span class=&quot;giallo-l&quot;&gt;    // Configure tool-specific properties.&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;    // &quot;customizations&quot;: {},&lt;/span&gt;

&lt;span class=&quot;giallo-l&quot;&gt;    // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;    // &quot;remoteUser&quot;: &quot;root&quot;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this example:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Multiple versions of .NET SDK installed for CMS 12 and 13 support.&lt;/li&gt;
&lt;li&gt;Ubuntu base image used for compatibility with the .NET tooling.&lt;/li&gt;
&lt;li&gt;Network access added to be able to connect to other services and containers.&lt;/li&gt;
&lt;li&gt;Port 5000 opened up for CMS site hosting.&lt;/li&gt;
&lt;li&gt;Post create commands added to setup Optimizely tools.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;When you save the configuration you will get a prompt in the bottom right to&amp;nbsp;&lt;em&gt;Rebuild&lt;/em&gt;. Click it. If you miss it then click the icon in the bottom left to get the an option list, select&amp;nbsp;&lt;em&gt;Rebuild Container&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;After rebuild we will be in the same state as before but with no manual steps required.&lt;/p&gt;
&lt;h2&gt;Alloy Tech Build Test&lt;/h2&gt;
&lt;p&gt;Now we can create and build an Optimizely Alloy Tech site in a VS code command prompt.&lt;/p&gt;
&lt;pre class=&quot;giallo&quot;&gt;&lt;code&gt;&lt;span class=&quot;giallo-l&quot;&gt;dotnet new epi-alloy-mvc -n alloy13preview&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;dotnet build&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;dotnet run&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When you run this in a Silicon Mac you will get an error that the local DB is not supported on this hardware.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tech.halse.me.uk/images/2026-01-29-005.png&quot; alt=&quot;Screenshot of Error&quot; /&gt;&lt;/p&gt;
&lt;p&gt;To resolve this we can connect to a SQL server on a container that does support Silicion Macs. The DevContainer is configured to use the host network so it can access any network shared database.&lt;/p&gt;
&lt;p&gt;Lets download Azure SQL Edge container for a multi-platform SQL server:&lt;/p&gt;
&lt;pre class=&quot;giallo&quot;&gt;&lt;code&gt;&lt;span class=&quot;giallo-l&quot;&gt;docker pull mcr.microsoft.com/azure-sql-edge:latest&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This requires some settings to run correctly. Review the below command and change details as required. We will need these values to build the Connection String in the CMS solution.&lt;/p&gt;
&lt;pre class=&quot;giallo&quot;&gt;&lt;code&gt;&lt;span class=&quot;giallo-l&quot;&gt;docker run --cap-add SYS_PTRACE -e &#39;ACCEPT_EULA=1&#39; -e &#39;MSSQL_SA_PASSWORD=s3cret-Ninja&#39; -p 1433:1433 --name azuresqledge -d mcr.microsoft.com/azure-sql-edge&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Install the SQL Server extension to VS Code. Note that this gets installed to the container not your host system.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://learn.microsoft.com/en-us/sql/tools/visual-studio-code-extensions/mssql/mssql-extension-visual-studio-code?view=sql-server-ver17&quot;&gt;MS SQL Extension VS Code&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Now we need to connect to the DB server using the settings we started the docker container with.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://learn.microsoft.com/en-us/sql/tools/visual-studio-code-extensions/mssql/mssql-extension-visual-studio-code?view=sql-server-ver17#connection-dialog&quot;&gt;Connection Dialog&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Once connected we create a database for the CMS to use. There is minimal GUI in this extension so an SQL script can be used for this.&lt;/p&gt;
&lt;pre class=&quot;giallo&quot;&gt;&lt;code&gt;&lt;span class=&quot;giallo-l&quot;&gt;-- Create a new database called &#39;CMS&#39;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;-- Connect to the &#39;master&#39; database to run this snippet&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;USE master&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;GO&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;-- Create the new database if it does not exist already&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;IF NOT EXISTS (&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;    SELECT name&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;        FROM sys.databases&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;        WHERE name = N&#39;CMS&#39;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;CREATE DATABASE CMS&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;GO&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we build up a standard connection string using the values from previous commands. Place this in appsettings.Development.json in the Optimizely solution.&lt;/p&gt;
&lt;pre class=&quot;giallo&quot;&gt;&lt;code&gt;&lt;span class=&quot;giallo-l&quot;&gt;  &quot;ConnectionStrings&quot;: {&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;    &quot;EPiServerDB&quot;: &quot;data source=tcp:localhost,1433;Database=CMS;User Id=sa;Password=s3cret-Ninja;Encrypt=True;TrustServerCertificate=True;Connect Timeout=30&quot;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;  },&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally setup database schema updates:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.developers.optimizely.com/content-management-system/docs/installing-database-schema#enable-automatic-creation-of-database-schemas&quot;&gt;Enable Automatic Creation of Database Schemas&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Now we can run the solution again and confirm it can start. Click the button prompt to open a host browser window for you while the solution and DB run on containers.&lt;/p&gt;
&lt;pre class=&quot;giallo&quot;&gt;&lt;code&gt;&lt;span class=&quot;giallo-l&quot;&gt;dotnet run&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We have port 5000 exposed to the host system so you access Optimizely admin setup and site as usual. You will see a 1 on the ports tab and can check that tab to review network ports enabled.&lt;/p&gt;
&lt;h2&gt;Finished for now&lt;/h2&gt;
&lt;p&gt;You are now ready to experiment, isolated away from your host system. Git can be used with the solution files as normal. In VS Code make sure you save the workspace, you may want to relocate the solution to your standard source / repos folder. The devcontainer.json is a lightweight way to share your container from Git.&lt;/p&gt;
&lt;p&gt;Personally I will be using this to experiment with CMS 13, extensions and add ons. Disposing and recreating them to work with different setups.&lt;/p&gt;
&lt;h2&gt;Clean up&lt;/h2&gt;
&lt;p&gt;If you experiment with the container configs you will have multiple container instances in Docker Desktop. In the images section there will be the base images used to build containers and the instance images. In volumes will be the drive mounts for the containers.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://code.visualstudio.com/docs/devcontainers/containers&quot;&gt;Developing inside a Container&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://containers.dev/features&quot;&gt;Dev Container Features&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://containers.dev/templates&quot;&gt;Dev Container Templates&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://tech.halse.me.uk/dev-container-cms/&quot;&gt;How to run Optimizely CMS on VS Code Dev Containers &amp;middot; Intuitive Tech&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>            <guid>https://world.optimizely.com/blogs/daniel-halse/dates/2026/1/how-to-run-optimizely-cms-on-vs-code-dev-containers/</guid>            <pubDate>Fri, 30 Jan 2026 10:06:10 GMT</pubDate>           <category>Blog post</category></item><item> <title>Prevent SQL error on CMS 12 minor version update</title>            <link>https://world.optimizely.com/blogs/daniel-halse/dates/2023/6/prevent-sql-error-on-cms-12-minor-version-update/</link>            <description>&lt;div&gt;
&lt;div&gt;If using an early version of CMS 12, like 12.0, you will get an SQL error after upgrading to newer patch versions.&lt;/div&gt;
&lt;div&gt;The fix for this is minor locally using SQL:&lt;/div&gt;
&lt;div class=&quot;copy-paste-block&quot;&gt;
&lt;p&gt;&lt;span style=&quot;font-family:&amp;#32;&amp;#39;courier&amp;#32;new&amp;#39;,&amp;#32;courier,&amp;#32;monospace;&quot;&gt;CREATE PROCEDURE dbo.netSoftLinksGetBroken&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family:&amp;#32;&amp;#39;courier&amp;#32;new&amp;#39;,&amp;#32;courier,&amp;#32;monospace;&quot;&gt;AS&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family:&amp;#32;&amp;#39;courier&amp;#32;new&amp;#39;,&amp;#32;courier,&amp;#32;monospace;&quot;&gt;BEGIN&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family:&amp;#32;&amp;#39;courier&amp;#32;new&amp;#39;,&amp;#32;courier,&amp;#32;monospace;&quot;&gt;SELECT 1;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family:&amp;#32;&amp;#39;courier&amp;#32;new&amp;#39;,&amp;#32;courier,&amp;#32;monospace;&quot;&gt;END&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family:&amp;#32;&amp;#39;courier&amp;#32;new&amp;#39;,&amp;#32;courier,&amp;#32;monospace;&quot;&gt;GO&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;This will create the missing stored procedure and allow it to proceed.&lt;/div&gt;
&lt;div&gt;But as this is a pain when dealing with DXP environments there is a better way to fix this before deployment:&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;Go to reports in the CMS&lt;/li&gt;
&lt;li&gt;Run the broken links report&lt;/li&gt;
&lt;li&gt;If this fails then you may need to get Optimizely support involved&lt;/li&gt;
&lt;li&gt;If this succeeds then the stored procedure above has been created correctly and used&lt;/li&gt;
&lt;li&gt;Optionally download the DB from PAAS portal and verify that the &lt;span&gt;dbo.netSoftLinksGetBroken stored procedure is present&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;div&gt;&lt;span&gt;This has been raised before:&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span&gt;&lt;a href=&quot;/link/e98029ffd9144bc1aa6f65e0851a935e.aspx&quot;&gt;EPiServer.Framework 12.13.0 - Failed to update database (optimizely.com)&lt;/a&gt;&lt;br /&gt;&lt;/span&gt;&lt;/div&gt;

&lt;div&gt;&lt;span&gt;The bug for which was closed without being fixed:&lt;br /&gt;&lt;a href=&quot;/link/1933ba72787346df9003b7a4c7d1cff8.aspx?epsremainingpath=bug/CMS-26818&quot;&gt;Bug - CMS-26818 (optimizely.com)&lt;/a&gt;&lt;br /&gt;&lt;/span&gt;&lt;/div&gt;

&lt;div&gt;&lt;span&gt;I suspect the issue is mainly visible on new sites / databases only so will more commonly be seen in early development.&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</description>            <guid>https://world.optimizely.com/blogs/daniel-halse/dates/2023/6/prevent-sql-error-on-cms-12-minor-version-update/</guid>            <pubDate>Thu, 08 Jun 2023 09:47:09 GMT</pubDate>           <category>Blog post</category></item></channel>
</rss>