<?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 David Drouin-Prince</title> <link>https://world.optimizely.com/blogs/david-drouin-prince/</link><description></description><ttl>60</ttl><generator>Optimizely World</generator><item> <title>VirtualText 2.0.0 beta for Optimizely CMS 13 previews</title>            <link>https://www.davidhome.net/blog/virtual-text-ready-v2-beta-for-cms-13-preview/</link>            <description>&lt;div&gt;&lt;div&gt;
&lt;div&gt;&lt;p&gt;&lt;code&gt;DavidHome.Optimizely.VirtualText&lt;/code&gt;&amp;nbsp;&lt;code&gt;2.0.0&lt;/code&gt; has been released as beta for Optimizely CMS 13 previews.&lt;/p&gt;
&lt;p&gt;This is a major technical release.&amp;nbsp;The focus is compatibility with CMS 13 previews,&amp;nbsp;updated routing behavior,&amp;nbsp;and alignment with Optimizely&#39;s newer application and shell APIs.&lt;/p&gt;
&lt;h2 id=&quot;what-changed&quot;&gt;What changed&lt;/h2&gt;
&lt;p&gt;VirtualText now relies more on Optimizely&#39;s application model and less on older site-definition-based assumptions.&amp;nbsp;Internally,&amp;nbsp;that means updated routing,&amp;nbsp;start page resolution,&amp;nbsp;and site context handling through APIs such as&amp;nbsp;&lt;code&gt;IApplicationRepository&lt;/code&gt;,&amp;nbsp;&lt;code&gt;IApplicationResolver&lt;/code&gt;,&amp;nbsp;and&amp;nbsp;&lt;code&gt;IRoutableApplication&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I also added&amp;nbsp;&lt;code&gt;ApplicationEventsSubscriber&lt;/code&gt;&amp;nbsp;so partial routers are registered from application lifecycle events,&amp;nbsp;and refactored several code paths from&amp;nbsp;&lt;code&gt;PageReference&lt;/code&gt;&amp;nbsp;to&amp;nbsp;&lt;code&gt;ContentReference&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;On the UI side,&amp;nbsp;the plugin and RobotsTxt extension layouts were updated to use the newer Optimizely shell resources and components.&lt;/p&gt;
&lt;h2 id=&quot;breaking-changes&quot;&gt;Breaking changes&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;2.0.0&lt;/code&gt;&amp;nbsp;beta is for CMS 13 previews only.&amp;nbsp;If your solution is on CMS 12,&amp;nbsp;stay on the previous VirtualText line.&lt;/p&gt;
&lt;p&gt;If you have custom code around&amp;nbsp;&lt;code&gt;SiteDefinition.Current&lt;/code&gt;,&amp;nbsp;&lt;code&gt;ISiteDefinitionRepository&lt;/code&gt;,&amp;nbsp;site-based routing assumptions,&amp;nbsp;or overridden plugin views,&amp;nbsp;review that code before upgrading.&lt;/p&gt;
&lt;p&gt;If you run a multi-site solution,&amp;nbsp;validate routing behavior explicitly after the upgrade.&amp;nbsp;Router registration now follows the application lifecycle.&lt;/p&gt;
&lt;p&gt;You should also expect small adjustments if your own code mirrors older&amp;nbsp;&lt;code&gt;PageReference&lt;/code&gt;-based assumptions.&lt;/p&gt;
&lt;h2 id=&quot;who-should-upgrade&quot;&gt;Who should upgrade&lt;/h2&gt;
&lt;p&gt;This beta is for teams already working with Optimizely CMS 13 previews and willing to validate a major-version upgrade.&lt;/p&gt;
&lt;p&gt;If that is your target,&amp;nbsp;this is the version to evaluate.&amp;nbsp;If not,&amp;nbsp;stay on the current stable line.&lt;/p&gt;
&lt;p&gt;Happy coding!&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>            <guid>https://www.davidhome.net/blog/virtual-text-ready-v2-beta-for-cms-13-preview/</guid>            <pubDate>Fri, 27 Mar 2026 02:39:59 GMT</pubDate>           <category>Blog post</category></item><item> <title>Upgrade RSS Feed Integration to Optimizely CMS 13 – v3.0.0 Beta</title>            <link>https://www.davidhome.net/blog/rss-feed-plugin-cms-13-ready/</link>            <description>&lt;div&gt;&lt;div&gt;
&lt;div&gt;&lt;p data-start=&quot;216&quot; data-end=&quot;318&quot;&gt;I&amp;rsquo;ve upgraded my&amp;nbsp;&lt;a href=&quot;https://www.davidhome.net/blog/optimizely-cms-easy-rss-feed-integration-library/&quot;&gt;RSS Feed Integration library&lt;/a&gt; for Optimizely CMS to support Optimizely CMS 13.&lt;/p&gt;
&lt;p data-start=&quot;320&quot; data-end=&quot;492&quot;&gt;&lt;a href=&quot;https://www.nuget.org/packages?q=DavidHome.RssFeed&quot;&gt;Version 3.0.0&lt;/a&gt; is currently released as a beta to allow early testing with the CMS 13 preview. This version will become stable once CMS 13 reaches its final release.&lt;/p&gt;
&lt;p data-start=&quot;494&quot; data-end=&quot;604&quot;&gt;If you&#39;re planning your CMS 12 &amp;rarr; 13 migration, this release allows you to validate RSS behavior ahead of time.&lt;/p&gt;
&lt;h2 data-start=&quot;611&quot; data-end=&quot;642&quot;&gt;Breaking Changes in 3.0.0&lt;/h2&gt;
&lt;p data-start=&quot;644&quot; data-end=&quot;691&quot;&gt;There are two important changes to be aware of.&lt;/p&gt;
&lt;h3 data-start=&quot;693&quot; data-end=&quot;734&quot;&gt;Scheduled Job Must Be Re-Executed&lt;/h3&gt;
&lt;p data-start=&quot;736&quot; data-end=&quot;843&quot;&gt;After upgrading, you must re-launch the scheduled job once in order to regenerate the RSS feed.&lt;/p&gt;
&lt;p data-start=&quot;845&quot; data-end=&quot;849&quot;&gt;Why?&lt;/p&gt;
&lt;p data-start=&quot;851&quot; data-end=&quot;990&quot;&gt;The compiled XML feed files will now be stored in a different location (see below). The plugin will not automatically detect the old files.&lt;/p&gt;
&lt;p data-start=&quot;992&quot; data-end=&quot;1051&quot;&gt;A single execution of the scheduled job resolves the issue.&lt;/p&gt;
&lt;h3 data-start=&quot;1058&quot; data-end=&quot;1123&quot;&gt;SiteDefinition &amp;rarr; Application (CMS 13 Architecture Change)&lt;/h3&gt;
&lt;p data-start=&quot;1125&quot; data-end=&quot;1250&quot;&gt;Under &lt;span class=&quot;whitespace-normal&quot;&gt;Optimizely CMS&lt;/span&gt;, the transition from &lt;code data-start=&quot;1190&quot; data-end=&quot;1206&quot;&gt;SiteDefinition&lt;/code&gt; to &lt;code data-start=&quot;1210&quot; data-end=&quot;1223&quot;&gt;Application&lt;/code&gt; removes the &lt;code data-start=&quot;1236&quot; data-end=&quot;1240&quot;&gt;Id&lt;/code&gt; property.&lt;/p&gt;
&lt;p data-start=&quot;1252&quot; data-end=&quot;1273&quot;&gt;In previous versions:&lt;/p&gt;
&lt;ul data-start=&quot;1275&quot; data-end=&quot;1364&quot;&gt;
&lt;li data-start=&quot;1275&quot; data-end=&quot;1364&quot;&gt;
&lt;p data-start=&quot;1277&quot; data-end=&quot;1364&quot;&gt;The &lt;code data-start=&quot;1281&quot; data-end=&quot;1300&quot;&gt;SiteDefinition.Id&lt;/code&gt; was used to determine where compiled RSS XML files were stored.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-start=&quot;1366&quot; data-end=&quot;1376&quot;&gt;In CMS 13:&lt;/p&gt;
&lt;ul&gt;
&lt;li data-start=&quot;1380&quot; data-end=&quot;1420&quot;&gt;&lt;code data-start=&quot;1380&quot; data-end=&quot;1393&quot;&gt;Application&lt;/code&gt; no longer exposes an &lt;code data-start=&quot;1415&quot; data-end=&quot;1419&quot;&gt;Id&lt;/code&gt;.&lt;/li&gt;
&lt;li data-start=&quot;1423&quot; data-end=&quot;1498&quot;&gt;The save path must now rely on the normalized application name instead.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-start=&quot;1500&quot; data-end=&quot;1510&quot;&gt;Impact&lt;/h3&gt;
&lt;p data-start=&quot;1512&quot; data-end=&quot;1535&quot;&gt;Because of this change:&lt;/p&gt;
&lt;ul&gt;
&lt;li data-start=&quot;1539&quot; data-end=&quot;1590&quot;&gt;The storage path of compiled RSS XML files changes.&lt;/li&gt;
&lt;li data-start=&quot;1539&quot; data-end=&quot;1590&quot;&gt;Existing RSS files generated under CMS 12 will not be found.&lt;/li&gt;
&lt;li data-start=&quot;1656&quot; data-end=&quot;1735&quot;&gt;The scheduled job must be executed once to regenerate them in the new location.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-start=&quot;1737&quot; data-end=&quot;1740&quot; /&gt;
&lt;h2 data-start=&quot;1742&quot; data-end=&quot;1762&quot;&gt;Migration Summary&lt;/h2&gt;
&lt;p data-start=&quot;1764&quot; data-end=&quot;1785&quot;&gt;If you are upgrading:&lt;/p&gt;
&lt;ol&gt;
&lt;li data-start=&quot;1790&quot; data-end=&quot;1822&quot;&gt;Update to version&lt;strong&gt; &lt;/strong&gt;3.0.0-beta&lt;/li&gt;
&lt;li data-start=&quot;1826&quot; data-end=&quot;1864&quot;&gt;Deploy your CMS 13 preview environment&lt;/li&gt;
&lt;li data-start=&quot;1868&quot; data-end=&quot;1898&quot;&gt;Run the RSS scheduled job once&lt;/li&gt;
&lt;li data-start=&quot;1902&quot; data-end=&quot;1927&quot;&gt;Validate your feed output&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-start=&quot;1929&quot; data-end=&quot;1939&quot;&gt;That&amp;rsquo;s it.&amp;nbsp;If you are preparing your CMS 13 migration roadmap, this beta gives you a safe way to validate RSS generation ahead of the official release.&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>            <guid>https://www.davidhome.net/blog/rss-feed-plugin-cms-13-ready/</guid>            <pubDate>Sat, 21 Feb 2026 19:39:23 GMT</pubDate>           <category>Blog post</category></item><item> <title>Multi Site NuGet v2 for Optimizely CMS 13 – Breaking Changes &amp; Migration</title>            <link>https://www.davidhome.net/blog/multi-site-plugin-on-cms-13-breaking-changes/</link>            <description>&lt;div&gt;&lt;div&gt;
&lt;div&gt;&lt;p&gt;The beta version 2 of DavidHome.Optimizely.MultiSite is now available on NuGet: &lt;a href=&quot;https://www.nuget.org/packages?q=DavidHome.Optimizely.MultiSite&quot;&gt;https://www.nuget.org/packages?q=DavidHome.Optimizely.MultiSite&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This is a major upgrade compatible with Optimizely CMS 13 preview.&amp;nbsp;Version 1 of this plugin will remain available and compatible with CMS 12. Both versions are planned to be supported until either Optimizely announces they no longer support the old version or that the disparity of feature between CMS 12 and 13 is making it complicated to maintain the plugin for both versions.&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;What Changed in v2&lt;/h2&gt;
&lt;p&gt;Starting with CMS 13, multi-site resolution is based on the Application identity (internal / normalized name) instead of the display name.&lt;/p&gt;
&lt;p&gt;This means folder resolution, asset handling, and UI grouping no longer rely on the display name of the site.&lt;/p&gt;
&lt;h2&gt;Breaking Change When Upgrading&lt;/h2&gt;
&lt;p&gt;If your CMS 12 website configuration included spaces in the site name, you must now use the normalized internal name under CMS 13.&lt;/p&gt;
&lt;p&gt;Example:&lt;br /&gt;&lt;strong&gt;Display name (CMS 12):&lt;/strong&gt; My Corporate Website&lt;br /&gt;&lt;strong&gt;Normalized Application ID (CMS 13):&lt;/strong&gt; mycorporatewebsite&lt;/p&gt;
&lt;p&gt;Folder structure must now follow the normalized name:&lt;br /&gt;&lt;strong&gt;Incorrect (old style): &lt;/strong&gt;wwwroot/My Corporate Website/&lt;br /&gt;&lt;strong&gt;Correct (CMS 13 style):&amp;nbsp;&lt;/strong&gt;wwwroot/mycorporatewebsite/&lt;/p&gt;
&lt;p&gt;If you upgrade to v2, every area of the plugin that relies on the site name must be updated accordingly.&lt;/p&gt;
&lt;p&gt;This includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Static asset folders&lt;/li&gt;
&lt;li&gt;Category mapping&lt;/li&gt;
&lt;li&gt;UI grouping&lt;/li&gt;
&lt;li&gt;Automation or deployment scripts&lt;/li&gt;
&lt;li&gt;Any name-based conventions&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Technical Prerequisites&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;.NET 10 target framework&lt;/li&gt;
&lt;li&gt;CMS dependency 13.0.0-preview2+&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;More Information&lt;/h2&gt;
&lt;p&gt;Initial announcement post: &lt;a href=&quot;https://www.davidhome.net/blog/optimizely-cms-multi-site-setup/&quot;&gt;https://www.davidhome.net/blog/optimizely-cms-multi-site-setup&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Source code and README: &lt;a href=&quot;https://github.com/ddprince17/davidhome-optimizely-multisite&quot;&gt;https://github.com/ddprince17/davidhome-optimizely-multisite&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Final Notes&lt;/h2&gt;
&lt;p&gt;This release intentionally aligns with CMS 13 architecture. If you are upgrading from version 1, review your website configuration names and folder structures carefully before deploying to production.&lt;/p&gt;
&lt;p&gt;Feedback and migration experiences are always welcome.&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>            <guid>https://www.davidhome.net/blog/multi-site-plugin-on-cms-13-breaking-changes/</guid>            <pubDate>Sat, 21 Feb 2026 18:39:37 GMT</pubDate>           <category>Blog post</category></item><item> <title>Managing robots.txt and Root Text Files in Optimizely CMS - Introducing Virtual Text</title>            <link>https://www.davidhome.net/blog/optimizely-plugin-virtual-text/</link>            <description>&lt;div&gt;&lt;div&gt;
&lt;div&gt;&lt;p data-start=&quot;427&quot; data-end=&quot;648&quot;&gt;In many Optimizely CMS projects, certain files must exist at the root of the site -&amp;nbsp;&lt;code data-start=&quot;511&quot; data-end=&quot;523&quot;&gt;robots.txt&lt;/code&gt;, &lt;code data-start=&quot;525&quot; data-end=&quot;534&quot;&gt;ads.txt&lt;/code&gt;, &lt;code data-start=&quot;536&quot; data-end=&quot;550&quot;&gt;security.txt&lt;/code&gt;, or other plain text endpoints required by search engines, security standards, or integrations.&lt;/p&gt;
&lt;p data-start=&quot;650&quot; data-end=&quot;831&quot;&gt;These files are often managed outside the CMS and require deployments for even small changes. That workflow is inconvenient for editors and adds unnecessary friction for operations.&lt;/p&gt;
&lt;p data-start=&quot;833&quot; data-end=&quot;1013&quot;&gt;To address this, I developed a small open-source plugin called &lt;strong data-start=&quot;896&quot; data-end=&quot;912&quot;&gt;Virtual Text&lt;/strong&gt;, which allows managing and rendering root-level text files directly from the Optimizely back office.&lt;/p&gt;
&lt;p data-start=&quot;1015&quot; data-end=&quot;1091&quot;&gt;Repository:&lt;br data-start=&quot;1026&quot; data-end=&quot;1029&quot; /&gt;&lt;a class=&quot;decorated-link&quot; href=&quot;https://github.com/ddprince17/davidhome-optimizely-virtualtext&quot; rel=&quot;noopener&quot; data-start=&quot;1029&quot; data-end=&quot;1091&quot;&gt;https://github.com/ddprince17/davidhome-optimizely-virtualtext&lt;/a&gt;&lt;/p&gt;
&lt;h2 data-start=&quot;1098&quot; data-end=&quot;1123&quot;&gt;What Virtual Text Does&lt;/h2&gt;
&lt;p data-start=&quot;1125&quot; data-end=&quot;1290&quot;&gt;Virtual Text extends Optimizely CMS with the ability to define and serve text files directly from the application pipeline rather than from physical files on disk.&lt;/p&gt;
&lt;p data-start=&quot;1292&quot; data-end=&quot;1325&quot;&gt;Editors can manage files such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li data-start=&quot;1329&quot; data-end=&quot;1341&quot;&gt;robots.txt&lt;/li&gt;
&lt;li data-start=&quot;1344&quot; data-end=&quot;1353&quot;&gt;ads.txt&lt;/li&gt;
&lt;li data-start=&quot;1356&quot; data-end=&quot;1382&quot;&gt;.well-known/security.txt&lt;/li&gt;
&lt;li data-start=&quot;1385&quot; data-end=&quot;1417&quot;&gt;Any other root-level text file&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-start=&quot;1419&quot; data-end=&quot;1506&quot;&gt;All content is editable through the CMS interface and served dynamically by middleware.&lt;/p&gt;
&lt;h2 data-start=&quot;1905&quot; data-end=&quot;1941&quot;&gt;Multi-Site and Hostname Awareness&lt;/h2&gt;
&lt;p data-start=&quot;1943&quot; data-end=&quot;2035&quot;&gt;The plugin was built with real Optimizely environments in mind, including multi-site setups.&lt;/p&gt;
&lt;p data-start=&quot;2037&quot; data-end=&quot;2059&quot;&gt;Virtual Text supports:&lt;/p&gt;
&lt;ul&gt;
&lt;li data-start=&quot;2063&quot; data-end=&quot;2087&quot;&gt;Per-site configuration&lt;/li&gt;
&lt;li data-start=&quot;2090&quot; data-end=&quot;2123&quot;&gt;Per-site hostname configuration&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-start=&quot;2125&quot; data-end=&quot;2251&quot;&gt;This allows different sites or hostnames within the same CMS instance to expose different text files or content when required.&lt;/p&gt;
&lt;h2 data-start=&quot;2258&quot; data-end=&quot;2285&quot;&gt;Storage and Architecture&lt;/h2&gt;
&lt;p data-start=&quot;2287&quot; data-end=&quot;2388&quot;&gt;The plugin separates file locations from file contents and supports Azure-backed storage providers.&lt;/p&gt;
&lt;p data-start=&quot;2390&quot; data-end=&quot;2417&quot;&gt;Available packages include:&lt;/p&gt;
&lt;p data-start=&quot;2419&quot; data-end=&quot;2424&quot;&gt;Core:&lt;/p&gt;
&lt;ul&gt;
&lt;li data-start=&quot;2427&quot; data-end=&quot;2452&quot;&gt;VirtualText core plugin&lt;/li&gt;
&lt;li data-start=&quot;2455&quot; data-end=&quot;2496&quot;&gt;Azure Table provider for file locations&lt;/li&gt;
&lt;li data-start=&quot;2499&quot; data-end=&quot;2538&quot;&gt;Azure Blob provider for file contents&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-start=&quot;2540&quot; data-end=&quot;2636&quot;&gt;This approach allows scaling storage independently and avoids coupling text files to local disk.&lt;/p&gt;
&lt;h2 data-start=&quot;2643&quot; data-end=&quot;2697&quot;&gt;RobotsTxt Extension for Non-Production Environments&lt;/h2&gt;
&lt;p data-start=&quot;2699&quot; data-end=&quot;2862&quot;&gt;The repository also includes an optional RobotsTxt extension designed to solve a common operational problem - preventing indexing of test and staging environments.&lt;/p&gt;
&lt;p data-start=&quot;2864&quot; data-end=&quot;2881&quot;&gt;Features include:&lt;/p&gt;
&lt;ul&gt;
&lt;li data-start=&quot;2885&quot; data-end=&quot;2922&quot;&gt;Environment-based indexing policies&lt;/li&gt;
&lt;li data-start=&quot;2925&quot; data-end=&quot;2991&quot;&gt;Automatic robots.txt manipulation in non-production environments&lt;/li&gt;
&lt;li data-start=&quot;2994&quot; data-end=&quot;3047&quot;&gt;Default directives such as disallowing all crawling&lt;/li&gt;
&lt;li data-start=&quot;3050&quot; data-end=&quot;3092&quot;&gt;Optional emission of indexing directives&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-start=&quot;3094&quot; data-end=&quot;3240&quot;&gt;This reduces the risk of staging or QA environments being indexed accidentally, which is still a surprisingly common issue in enterprise projects.&lt;/p&gt;
&lt;h2 data-start=&quot;3247&quot; data-end=&quot;3276&quot;&gt;Permissions and Governance&lt;/h2&gt;
&lt;p data-start=&quot;3278&quot; data-end=&quot;3348&quot;&gt;Virtual Text integrates with Optimizely permissions to control access:&lt;/p&gt;
&lt;ul&gt;
&lt;li data-start=&quot;3352&quot; data-end=&quot;3370&quot;&gt;View permissions&lt;/li&gt;
&lt;li data-start=&quot;3373&quot; data-end=&quot;3391&quot;&gt;Edit permissions&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-start=&quot;3393&quot; data-end=&quot;3522&quot;&gt;This allows delegating responsibility safely to operations or marketing teams without exposing broader CMS administration rights.&lt;/p&gt;
&lt;h2 data-start=&quot;3529&quot; data-end=&quot;3548&quot;&gt;Why I Built This&lt;/h2&gt;
&lt;p data-start=&quot;3550&quot; data-end=&quot;3615&quot;&gt;In multiple projects, I repeatedly encountered the same friction:&lt;/p&gt;
&lt;ul&gt;
&lt;li data-start=&quot;3619&quot; data-end=&quot;3668&quot;&gt;Editors needing to update robots.txt or ads.txt&lt;/li&gt;
&lt;li data-start=&quot;3671&quot; data-end=&quot;3724&quot;&gt;Developers being asked to deploy small file changes&lt;/li&gt;
&lt;li data-start=&quot;3727&quot; data-end=&quot;3771&quot;&gt;Risk of staging environments being indexed&lt;/li&gt;
&lt;li data-start=&quot;3774&quot; data-end=&quot;3834&quot;&gt;Multi-site environments requiring different configurations&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-start=&quot;3836&quot; data-end=&quot;3916&quot;&gt;Virtual Text was built to solve these problems in a reusable and consistent way.&lt;/p&gt;
&lt;h2 data-start=&quot;3923&quot; data-end=&quot;3941&quot;&gt;Getting Started&lt;/h2&gt;
&lt;p data-start=&quot;3943&quot; data-end=&quot;3975&quot;&gt;Installation is straightforward:&lt;/p&gt;
&lt;ul&gt;
&lt;li data-start=&quot;3980&quot; data-end=&quot;4004&quot;&gt;Add the NuGet packages&lt;/li&gt;
&lt;li data-start=&quot;4008&quot; data-end=&quot;4031&quot;&gt;Register the services&lt;/li&gt;
&lt;li data-start=&quot;4035&quot; data-end=&quot;4064&quot;&gt;Configure optional settings&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-start=&quot;4066&quot; data-end=&quot;4194&quot;&gt;Full instructions and examples are available in the repository:&lt;br data-start=&quot;4129&quot; data-end=&quot;4132&quot; /&gt;&lt;a class=&quot;decorated-link&quot; href=&quot;https://github.com/ddprince17/davidhome-optimizely-virtualtext&quot; rel=&quot;noopener&quot; data-start=&quot;4132&quot; data-end=&quot;4194&quot;&gt;https://github.com/ddprince17/davidhome-optimizely-virtualtext&lt;/a&gt;&lt;/p&gt;
&lt;p data-start=&quot;4419&quot; data-end=&quot;4467&quot;&gt;Feedback, issues, and contributions are always welcome!&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>            <guid>https://www.davidhome.net/blog/optimizely-plugin-virtual-text/</guid>            <pubDate>Wed, 18 Feb 2026 05:21:32 GMT</pubDate>           <category>Blog post</category></item><item> <title>Optimizely CMS RSS Feed Integration Library — Version 2 Release</title>            <link>https://www.davidhome.net/blog/optimizely-rss-feed-integration-v2/</link>            <description>&lt;div&gt;&lt;div&gt;
&lt;div&gt;&lt;h2 data-start=&quot;325&quot; data-end=&quot;392&quot;&gt;&lt;!--StartFragment--&gt;&lt;/h2&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;strong&gt;Optimizely CMS Easy RSS Feed Integration Library &amp;mdash; Now in v2&lt;/strong&gt;&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;A while ago I launched a NuGet-package called &lt;strong&gt;DavidHome.RssFeed&lt;/strong&gt; to make RSS feed generation from Optimizely CMS &lt;em&gt;really easy&lt;/em&gt;. Back then, I focused on a simple, opinionated design: container and item types marked by interfaces, Azure Blob Storage as the backend, and a scheduled generation job. (If you missed that, you can still read the &lt;a href=&quot;https://www.davidhome.net/blog/optimizely-cms-easy-rss-feed-integration-library/&quot;&gt;original post&lt;/a&gt;.)&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;Today, I&amp;rsquo;m excited to announce that the library has matured significantly &amp;mdash; welcome &lt;strong&gt;version 2.0.0&lt;/strong&gt;. This isn&amp;rsquo;t a small patch: it&amp;rsquo;s a major rewrite and refactor, and I think it will serve more complex scenarios much better, especially for multi-language, multi-site Optimizely projects.&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;Here&amp;rsquo;s what&amp;rsquo;s changed, why I made these changes, and how to upgrade.&lt;/p&gt;
&lt;div class=&quot;MsoNormal&quot; style=&quot;text-align: center;&quot; align=&quot;center&quot;&gt;&lt;hr align=&quot;center&quot; size=&quot;2&quot; width=&quot;100%&quot; /&gt;&lt;/div&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;strong&gt;What&amp;rsquo;s Changed Since v1&lt;/strong&gt;&lt;/p&gt;
&lt;ol style=&quot;margin-top: 0cm;&quot; start=&quot;1&quot; type=&quot;1&quot;&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l2 level1 lfo1; tab-stops: list 36.0pt;&quot;&gt;&lt;strong&gt;Improved Processor Pipeline&lt;/strong&gt;&lt;/li&gt;
&lt;ul style=&quot;margin-top: 0cm;&quot; type=&quot;circle&quot;&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l2 level2 lfo1; tab-stops: list 72.0pt;&quot;&gt;The processor architecture has been clarified and formalized. Rather than ad-hoc processors, there are well-defined builder and processor interfaces.&lt;/li&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l2 level2 lfo1; tab-stops: list 72.0pt;&quot;&gt;Better extension points: it&amp;rsquo;s now much easier to write custom processing logic (filtering, transformation, enrichment).&lt;/li&gt;
&lt;/ul&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l2 level1 lfo1; tab-stops: list 36.0pt;&quot;&gt;&lt;strong&gt;Multi-Host and Multi-Language Awareness&lt;/strong&gt;&lt;/li&gt;
&lt;ul style=&quot;margin-top: 0cm;&quot; type=&quot;circle&quot;&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l2 level2 lfo1; tab-stops: list 72.0pt;&quot;&gt;Feeds are now scoped per host (domain) and language. Each container type can generate distinct feeds depending on the hostname and language.&lt;/li&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l2 level2 lfo1; tab-stops: list 72.0pt;&quot;&gt;This is a big deal if your Optimizely setup supports multiple sites or localized content &amp;mdash; no more &amp;ldquo;one feed for everything&amp;rdquo; workaround.&lt;/li&gt;
&lt;/ul&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l2 level1 lfo1; tab-stops: list 36.0pt;&quot;&gt;&lt;strong&gt;Storage Provider Contracts&lt;/strong&gt;&lt;/li&gt;
&lt;ul style=&quot;margin-top: 0cm;&quot; type=&quot;circle&quot;&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l2 level2 lfo1; tab-stops: list 72.0pt;&quot;&gt;The storage abstraction (IRssFeedStorageProvider) is stronger and more flexible.&lt;/li&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l2 level2 lfo1; tab-stops: list 72.0pt;&quot;&gt;The Azure Blob provider now handles host- and language-based folder structures, e.g. /mydomain.com/en/MyFeed.xml.&lt;/li&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l2 level2 lfo1; tab-stops: list 72.0pt;&quot;&gt;This means the library can be extended more reliably if you want to implement a different storage backend.&lt;/li&gt;
&lt;/ul&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l2 level1 lfo1; tab-stops: list 36.0pt;&quot;&gt;&lt;strong&gt;Breaking API Changes&lt;/strong&gt;&lt;/li&gt;
&lt;ul style=&quot;margin-top: 0cm;&quot; type=&quot;circle&quot;&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l2 level2 lfo1; tab-stops: list 72.0pt;&quot;&gt;Many core interfaces have changed (builders, processor callbacks, storage signatures).&lt;/li&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l2 level2 lfo1; tab-stops: list 72.0pt;&quot;&gt;If you wrote custom processors in v1, they may need to be updated to match the new generic types and discovery model.&lt;/li&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l2 level2 lfo1; tab-stops: list 72.0pt;&quot;&gt;Registration style in Startup.cs is slightly different: you must pass assemblies for discovery, and you register things in a more modular way.&lt;/li&gt;
&lt;/ul&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l2 level1 lfo1; tab-stops: list 36.0pt;&quot;&gt;&lt;strong&gt;Better Error Handling &amp;amp; Logging&lt;/strong&gt;&lt;/li&gt;
&lt;ul style=&quot;margin-top: 0cm;&quot; type=&quot;circle&quot;&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l2 level2 lfo1; tab-stops: list 72.0pt;&quot;&gt;The new pipeline makes it easier to surface problems during feed generation, so malformed items or storage errors are more diagnosable.&lt;/li&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l2 level2 lfo1; tab-stops: list 72.0pt;&quot;&gt;This should make production issues less mysterious (especially in complex, multi-language sites).&lt;/li&gt;
&lt;/ul&gt;
&lt;/ol&gt;
&lt;div class=&quot;MsoNormal&quot; style=&quot;text-align: center;&quot; align=&quot;center&quot;&gt;&lt;hr align=&quot;center&quot; size=&quot;2&quot; width=&quot;100%&quot; /&gt;&lt;/div&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;strong&gt;Why These Changes Matter&lt;/strong&gt;&lt;/p&gt;
&lt;ul style=&quot;margin-top: 0cm;&quot; type=&quot;disc&quot;&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l1 level1 lfo2; tab-stops: list 36.0pt;&quot;&gt;&lt;strong&gt;Scalability&lt;/strong&gt;: As my own blog (and potentially other sites) grew, I needed more control and modularity, not a one-size-fits-all approach.&lt;/li&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l1 level1 lfo2; tab-stops: list 36.0pt;&quot;&gt;&lt;strong&gt;Maintainability&lt;/strong&gt;: The cleaner separation of concerns makes the code easier to test, extend, and maintain.&lt;/li&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l1 level1 lfo2; tab-stops: list 36.0pt;&quot;&gt;&lt;strong&gt;Real-world Use Cases&lt;/strong&gt;: Multi-host and multi-language support is no longer an afterthought &amp;mdash; it&#39;s built in.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;MsoNormal&quot; style=&quot;text-align: center;&quot; align=&quot;center&quot;&gt;&lt;hr align=&quot;center&quot; size=&quot;2&quot; width=&quot;100%&quot; /&gt;&lt;/div&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;strong&gt;How to Upgrade to v2&lt;/strong&gt;&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;If you&amp;rsquo;re already using v1, here&amp;rsquo;s a rough migration path:&lt;/p&gt;
&lt;ol style=&quot;margin-top: 0cm;&quot; start=&quot;1&quot; type=&quot;1&quot;&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l0 level1 lfo3; tab-stops: list 36.0pt;&quot;&gt;&lt;strong&gt;Update your NuGet references&lt;/strong&gt; to the v2 packages (core, Optimizely, storage)&lt;/li&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l0 level1 lfo3; tab-stops: list 36.0pt;&quot;&gt;&lt;strong&gt;Review your DI setup&lt;/strong&gt;: switch to the new registration style, pass assemblies to discovery&lt;/li&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l0 level1 lfo3; tab-stops: list 36.0pt;&quot;&gt;&lt;strong&gt;Refactor custom processors&lt;/strong&gt;: align them with the new builder/processor interfaces&lt;/li&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l0 level1 lfo3; tab-stops: list 36.0pt;&quot;&gt;&lt;strong&gt;Update feed models&lt;/strong&gt;: you need to change them with the new marker interfaces&lt;/li&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l0 level1 lfo3; tab-stops: list 36.0pt;&quot;&gt;&lt;strong&gt;Validate behavior&lt;/strong&gt;: regenerate your feeds, make sure routing works correctly, deploy in a test or staging environment&lt;/li&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l0 level1 lfo3; tab-stops: list 36.0pt;&quot;&gt;&lt;strong&gt;Monitor and tune&lt;/strong&gt;: watch for any error logs during feed generation, especially for multilingual or multi-host feeds&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;MsoNormal&quot; style=&quot;text-align: center;&quot; align=&quot;center&quot;&gt;&lt;hr align=&quot;center&quot; size=&quot;2&quot; width=&quot;100%&quot; /&gt;&lt;/div&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;strong&gt;Final Thoughts&lt;/strong&gt;&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;Version 2 of &lt;strong&gt;DavidHome.RssFeed&lt;/strong&gt; is a big leap: it&amp;rsquo;s not just an &amp;ldquo;improvement&amp;rdquo;, it&amp;rsquo;s a &lt;strong&gt;re-architecture&lt;/strong&gt;. I believe this makes the library much more powerful for serious Optimizely use cases &amp;mdash; but also more flexible than ever for hobby or solo projects like my blog.&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;If you&amp;rsquo;re using it today, now&amp;rsquo;s a good time to upgrade. If you&amp;rsquo;re just discovering it, hopefully v2 feels more robust and future-ready than the early days. You can find it on &lt;a href=&quot;https://www.nuget.org/packages?q=DavidHome.RssFeed&quot;&gt;NuGet.org&lt;/a&gt;. If you&#39;re interested, you can also view my &lt;a href=&quot;https://www.davidhome.net/nuget-packages/&quot;&gt;NuGet library&lt;/a&gt;.&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;Thanks to everyone who has been using, testing, or contributing &amp;mdash; I appreciate every bit of feedback. If you run into issues or want to help shape the future of this library, let me know or open an issue in the repo.&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;Happy coding!&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>            <guid>https://www.davidhome.net/blog/optimizely-rss-feed-integration-v2/</guid>            <pubDate>Sat, 15 Nov 2025 18:30:30 GMT</pubDate>           <category>Blog post</category></item><item> <title>Optimizely CMS platform bug in ErrorsController (EPiServer.CMS.Core 12.22.9 fix)</title>            <link>https://www.davidhome.net/blog/optimizely-cms-platform-bug-in-errorscontroller/</link>            <description>&lt;div&gt;&lt;div&gt;
&lt;div&gt;&lt;p data-start=&quot;225&quot; data-end=&quot;536&quot;&gt;While checking&amp;nbsp;&lt;strong data-start=&quot;240&quot; data-end=&quot;264&quot;&gt;Application Insights&lt;/strong&gt; earlier this year, I stumbled upon a strange exception in my Optimizely site. At first, I thought it might be a misconfigured redirect or some leftover test code &amp;mdash; but after digging in, it turned out to be an&amp;nbsp;&lt;strong data-start=&quot;470&quot; data-end=&quot;493&quot;&gt;actual platform bug&lt;/strong&gt;, one I ended up reporting to Optimizely.&lt;/p&gt;
&lt;p data-start=&quot;538&quot; data-end=&quot;700&quot;&gt;It&amp;rsquo;s a subtle one. If you&amp;rsquo;re using the &lt;strong data-start=&quot;577&quot; data-end=&quot;623&quot;&gt;custom error pages provided out of the box&lt;/strong&gt;, you could be impacted &amp;mdash; unless you&amp;rsquo;re already running the latest version.&lt;/p&gt;
&lt;hr data-start=&quot;702&quot; data-end=&quot;705&quot; /&gt;
&lt;h3 data-start=&quot;707&quot; data-end=&quot;724&quot;&gt;The Symptom&lt;/h3&gt;
&lt;p data-start=&quot;726&quot; data-end=&quot;916&quot;&gt;My site doesn&amp;rsquo;t generate a lot of exceptions, so this one stood out right away. A few errors were being thrown, all with the same root cause, and all triggered by &lt;strong data-start=&quot;889&quot; data-end=&quot;897&quot;&gt;bots&lt;/strong&gt;, not real users.&lt;/p&gt;
&lt;p data-start=&quot;918&quot; data-end=&quot;966&quot;&gt;Here&amp;rsquo;s what showed up in Application Insights:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;System.UriFormatException:
   at System.Uri.CreateThis (System.Private.Uri undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=b03f5f7f11d50a3a undefined)
   at System.Uri..ctor (System.Private.Uri undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=b03f5f7f11d50a3a undefined)
   at System.UriBuilder..ctor (System.Private.Uri undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=b03f5f7f11d50a3a undefined)
   at EPiServer.UrlBuilder.Init (EPiServer undefined, Version=12.22.6.0 undefined, Culture=neutral undefined, PublicKeyToken=8fe83dea738b45b7 undefined)
   at EPiServer.Core.Routing.Pipeline.Internal.SimpleAddressResolverPipelineStep.ResolveSimpleAddress (EPiServer undefined, Version=12.22.6.0 undefined, Culture=neutral undefined, PublicKeyToken=8fe83dea738b45b7 undefined)
   at EPiServer.Core.Routing.Pipeline.Internal.SimpleAddressResolverPipelineStep.Resolve (EPiServer undefined, Version=12.22.6.0 undefined, Culture=neutral undefined, PublicKeyToken=8fe83dea738b45b7 undefined)
   at EPiServer.Core.Routing.Internal.DefaultContentUrlResolver.Resolve (EPiServer undefined, Version=12.22.6.0 undefined, Culture=neutral undefined, PublicKeyToken=8fe83dea738b45b7 undefined)
   at EPiServer.Cms.Shell.UI.Controllers.Internal.ErrorsController.ResolveUICulture (EPiServer.Cms.Shell.UI undefined, Version=12.32.5.0 undefined, Culture=neutral undefined, PublicKeyToken=8fe83dea738b45b7 undefined)
   at EPiServer.Cms.Shell.UI.Controllers.Internal.ErrorsController.Error500 (EPiServer.Cms.Shell.UI undefined, Version=12.32.5.0 undefined, Culture=neutral undefined, PublicKeyToken=8fe83dea738b45b7 undefined)
   at lambda_method58068 .lambda_method58068 (Anonymously Hosted, Version=0.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=null undefined)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor+SyncActionResultExecutor.Execute (Microsoft.AspNetCore.Mvc.Core undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=adb9793829ddae60 undefined)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker+&amp;lt;&amp;lt;InvokeActionMethodAsync&amp;gt;g__Logged|12_1&amp;gt;d.MoveNext (Microsoft.AspNetCore.Mvc.Core undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=adb9793829ddae60 undefined)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker+&amp;lt;&amp;lt;InvokeNextActionFilterAsync&amp;gt;g__Awaited|10_0&amp;gt;d.MoveNext (Microsoft.AspNetCore.Mvc.Core undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=adb9793829ddae60 undefined)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow (Microsoft.AspNetCore.Mvc.Core undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=adb9793829ddae60 undefined)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next (Microsoft.AspNetCore.Mvc.Core undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=adb9793829ddae60 undefined)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync (Microsoft.AspNetCore.Mvc.Core undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=adb9793829ddae60 undefined)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker+&amp;lt;&amp;lt;InvokeNextResourceFilter&amp;gt;g__Awaited|25_0&amp;gt;d.MoveNext (Microsoft.AspNetCore.Mvc.Core undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=adb9793829ddae60 undefined)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow (Microsoft.AspNetCore.Mvc.Core undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=adb9793829ddae60 undefined)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Next (Microsoft.AspNetCore.Mvc.Core undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=adb9793829ddae60 undefined)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeFilterPipelineAsync (Microsoft.AspNetCore.Mvc.Core undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=adb9793829ddae60 undefined)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker+&amp;lt;&amp;lt;InvokeAsync&amp;gt;g__Logged|17_1&amp;gt;d.MoveNext (Microsoft.AspNetCore.Mvc.Core undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=adb9793829ddae60 undefined)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker+&amp;lt;&amp;lt;InvokeAsync&amp;gt;g__Logged|17_1&amp;gt;d.MoveNext (Microsoft.AspNetCore.Mvc.Core undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=adb9793829ddae60 undefined)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at Stott.Optimizely.RobotsHandler.Environments.RobotsHeaderMiddleware+&amp;lt;Invoke&amp;gt;d__2.MoveNext (Stott.Optimizely.RobotsHandler undefined, Version=4.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=null undefined)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware+&amp;lt;Invoke&amp;gt;d__11.MoveNext (Microsoft.AspNetCore.Authorization.Policy undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=adb9793829ddae60 undefined)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware+&amp;lt;Invoke&amp;gt;d__6.MoveNext (Microsoft.AspNetCore.Authentication undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=adb9793829ddae60 undefined)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at Geta.NotFoundHandler.Infrastructure.Initialization.NotFoundHandlerMiddleware+&amp;lt;InvokeAsync&amp;gt;d__2.MoveNext (Geta.NotFoundHandler undefined, Version=5.0.13.0 undefined, Culture=neutral undefined, PublicKeyToken=null undefined)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at EPiServer.Cms.Shell.UI.Internal.RegisterAdminUserMiddleware+&amp;lt;InvokeAsync&amp;gt;d__5.MoveNext (EPiServer.Cms.Shell.UI undefined, Version=12.32.5.0 undefined, Culture=neutral undefined, PublicKeyToken=8fe83dea738b45b7 undefined)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddlewareImpl+&amp;lt;HandleException&amp;gt;d__11.MoveNext (Microsoft.AspNetCore.Diagnostics undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=adb9793829ddae60 undefined)&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary&quot;&gt;
&lt;div class=&quot;sticky top-9&quot;&gt;
&lt;div class=&quot;absolute end-0 bottom-0 flex h-9 items-center pe-2&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr data-start=&quot;1260&quot; data-end=&quot;1263&quot; /&gt;
&lt;h3 data-start=&quot;1265&quot; data-end=&quot;1285&quot;&gt;The Root Cause&lt;/h3&gt;
&lt;p data-start=&quot;1287&quot; data-end=&quot;1473&quot;&gt;After a bit of debugging, I found that certain requests contained &lt;strong data-start=&quot;1353&quot; data-end=&quot;1386&quot;&gt;invalid characters in the URL&lt;/strong&gt;, which caused Optimizely&amp;rsquo;s &lt;code data-start=&quot;1414&quot; data-end=&quot;1432&quot;&gt;ErrorsController&lt;/code&gt; to fail while trying to build a &lt;code data-start=&quot;1465&quot; data-end=&quot;1470&quot;&gt;Uri&lt;/code&gt;.&lt;/p&gt;
&lt;p data-start=&quot;1475&quot; data-end=&quot;1533&quot;&gt;Here&amp;rsquo;s an example of a request that triggered the issue:&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;https://www.davidhome.net/h%20ttps:%20oast.me&lt;/code&gt;&lt;/pre&gt;
&lt;p data-start=&quot;1590&quot; data-end=&quot;1837&quot;&gt;That tiny bit of malformed input was enough to cause a &lt;code data-start=&quot;1645&quot; data-end=&quot;1672&quot;&gt;System.UriFormatException&lt;/code&gt; deep inside the platform. Essentially, the controller attempts to construct a &lt;code data-start=&quot;1751&quot; data-end=&quot;1756&quot;&gt;Uri&lt;/code&gt; from the incoming request &amp;mdash; but when the value isn&amp;rsquo;t valid, everything breaks.&lt;/p&gt;
&lt;hr data-start=&quot;1839&quot; data-end=&quot;1842&quot; /&gt;
&lt;h3 data-start=&quot;1844&quot; data-end=&quot;1857&quot;&gt;The Fix&lt;/h3&gt;
&lt;p data-start=&quot;1859&quot; data-end=&quot;2057&quot;&gt;The issue has already been fixed in &lt;strong data-start=&quot;1895&quot; data-end=&quot;1925&quot;&gt;EPiServer.CMS.Core 12.22.9&lt;/strong&gt;.&lt;br data-start=&quot;1926&quot; data-end=&quot;1929&quot; /&gt;If you&amp;rsquo;re on an earlier version and you&amp;rsquo;re seeing similar exceptions, simply &lt;strong data-start=&quot;2006&quot; data-end=&quot;2029&quot;&gt;update your package&lt;/strong&gt; and you&amp;rsquo;ll be good to go.&lt;/p&gt;
&lt;hr data-start=&quot;2059&quot; data-end=&quot;2062&quot; /&gt;
&lt;h3 data-start=&quot;2064&quot; data-end=&quot;2084&quot;&gt;Final Thoughts&lt;/h3&gt;
&lt;p data-start=&quot;2086&quot; data-end=&quot;2243&quot;&gt;This isn&amp;rsquo;t something an end user would normally trigger, but it&amp;rsquo;s still worth cleaning up to keep your telemetry free of noise and your logs easy to trust.&lt;/p&gt;
&lt;p data-start=&quot;2245&quot; data-end=&quot;2408&quot;&gt;Small bugs like this remind me why it&amp;rsquo;s always worth checking Application Insights from time to time &amp;mdash; even when you think nothing&amp;rsquo;s happening behind the scenes.&lt;/p&gt;
&lt;p data-start=&quot;2410&quot; data-end=&quot;2432&quot;&gt;Happy programming &#128075;&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>            <guid>https://www.davidhome.net/blog/optimizely-cms-platform-bug-in-errorscontroller/</guid>            <pubDate>Sun, 09 Nov 2025 18:52:25 GMT</pubDate>           <category>Blog post</category></item><item> <title>Avoid Using OnStatusChanged in Optimizely CMS – It Can Impact Database Performance</title>            <link>https://www.davidhome.net/blog/avoid-using-onstatuschanged-it-bothers-the-database/</link>            <description>&lt;div&gt;&lt;div&gt;
&lt;div&gt;&lt;h3 data-start=&quot;455&quot; data-end=&quot;529&quot;&gt;Beware of Overusing &lt;code data-start=&quot;479&quot; data-end=&quot;496&quot;&gt;OnStatusChanged&lt;/code&gt; in Optimizely CMS Scheduled Jobs&lt;/h3&gt;
&lt;p data-start=&quot;531&quot; data-end=&quot;750&quot;&gt;Optimizely CMS allows you to create scheduled jobs &amp;mdash; a powerful feature often used to automate repetitive tasks such as product imports. Sometimes, we even use them for one-off data migrations and delete them afterward.&lt;/p&gt;
&lt;p data-start=&quot;752&quot; data-end=&quot;978&quot;&gt;This post assumes you&amp;rsquo;re already familiar with the basics, but if not, you can catch up here:&amp;nbsp;&lt;a class=&quot;decorated-link&quot; href=&quot;https://docs.developers.optimizely.com/content-management-system/docs/scheduled-jobs&quot; target=&quot;_new&quot; rel=&quot;noopener&quot; data-start=&quot;851&quot; data-end=&quot;978&quot;&gt;Optimizely Scheduled Jobs Documentation&lt;/a&gt;&lt;/p&gt;
&lt;p data-start=&quot;980&quot; data-end=&quot;1330&quot;&gt;Most developers are aware of the &lt;code data-start=&quot;1013&quot; data-end=&quot;1030&quot;&gt;OnStatusChanged&lt;/code&gt; method. It&amp;rsquo;s handy for updating the administrative interface with progress messages while a scheduled job runs. For example, you might use it to display periodic updates like &amp;ldquo;Importing products&amp;hellip;&amp;rdquo; or &amp;ldquo;Processing batch 2 of 5&amp;hellip;&amp;rdquo; so that anyone monitoring the job can see what&amp;rsquo;s happening in real time.&lt;/p&gt;
&lt;p data-start=&quot;1332&quot; data-end=&quot;1363&quot;&gt;There&amp;rsquo;s a small hiccup, though.&lt;/p&gt;
&lt;hr data-start=&quot;1365&quot; data-end=&quot;1368&quot; /&gt;
&lt;h4 data-start=&quot;1370&quot; data-end=&quot;1386&quot;&gt;The Problem&lt;/h4&gt;
&lt;p data-start=&quot;1388&quot; data-end=&quot;1619&quot;&gt;While investigating a performance issue for one of our clients, we noticed the database was under heavy load during certain scheduled jobs. After some digging, we realized that our frequent use of &lt;code data-start=&quot;1585&quot; data-end=&quot;1602&quot;&gt;OnStatusChanged&lt;/code&gt; was the culprit.&lt;/p&gt;
&lt;p data-start=&quot;1621&quot; data-end=&quot;1737&quot;&gt;Even worse, some jobs began failing with an unexpected error &amp;mdash; one that had nothing to do with the job logic itself:&lt;/p&gt;
&lt;div class=&quot;contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary&quot;&gt;
&lt;div class=&quot;overflow-y-auto p-4&quot; dir=&quot;ltr&quot;&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;An unhandled error occured while running the job &#39;MyJob&#39;.
Microsoft.Data.SqlClient.SqlException (0x80131904): The server failed to resume the transaction. Desc:3e00000334.
The transaction active in this session has been committed or aborted by another session.
   at Microsoft.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
   at Microsoft.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
   at Microsoft.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj, SqlCommand command, Boolean callerHasConnectionLock, Boolean asyncClose)
   at Microsoft.Data.SqlClient.TdsParser.TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj, Boolean&amp;amp; dataReady)
   at Microsoft.Data.SqlClient.TdsParser.Run(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj)
   at Microsoft.Data.SqlClient.TdsParser.TdsExecuteTransactionManagerRequest(Byte[] buffer, TransactionManagerRequestType request, String transactionName, TransactionManagerIsolationLevel isoLevel, Int32 timeout, SqlInternalTransaction transaction, TdsParserStateObject stateObj, Boolean isDelegateControlRequest)
   at Microsoft.Data.SqlClient.SqlInternalConnectionTds.ExecuteTransaction2005(TransactionRequest transactionRequest, String transactionName, IsolationLevel iso, SqlInternalTransaction internalTransaction, Boolean isDelegateControlRequest)
   at Microsoft.Data.SqlClient.SqlInternalConnectionTds.ExecuteTransaction(TransactionRequest transactionRequest, String name, IsolationLevel iso, SqlInternalTransaction internalTransaction, Boolean isDelegateControlRequest)
   at Microsoft.Data.SqlClient.SqlInternalConnection.BeginSqlTransaction(IsolationLevel iso, String transactionName, Boolean shouldReconnect)
   at Microsoft.Data.SqlClient.SqlConnection.BeginTransaction(IsolationLevel iso, String transactionName)
   at Microsoft.Data.SqlClient.SqlConnection.BeginTransaction(IsolationLevel iso)
   at Microsoft.Data.SqlClient.SqlConnection.BeginDbTransaction(IsolationLevel isolationLevel)
   at EPiServer.Data.Providers.Internal.ConnectionContext.BeginTransaction()
   at EPiServer.Data.Internal.DefaultConnectionContextHandler.CreateConnectionScope(Boolean requireTransaction, Action completeAction)
   at EPiServer.Data.Internal.ConnectionScopeResolver.GetConnectionScope(Boolean requireTransaction)
   at EPiServer.Data.Providers.Internal.SqlDatabaseExecutor.GetConnection(Boolean requireTransaction)
   at EPiServer.Data.Providers.Internal.SqlDatabaseExecutor.&amp;lt;&amp;gt;c__DisplayClass28_0`1.&amp;lt;ExecuteTransaction&amp;gt;b__0()
   at EPiServer.Data.Providers.SqlTransientErrorsRetryPolicy.Execute[TResult](Func`1 method)
   at EPiServer.Data.Providers.Internal.SqlDatabaseExecutor.ExecuteTransaction[TResult](Func`1 action)
   at EPiServer.Data.Providers.Internal.SqlDatabaseExecutor.ExecuteTransaction(Action action)
   at EPiServer.DataAccess.Internal.SchedulerDB.UpdateCurrentStatusMessage(Guid id, String statusMessage)
   at EPiServer.Scheduler.Internal.DefaultScheduledJobExecutor.JobInstance_StatusChanged(Object sender, JobStatusChangedEventArgs e)
   at EPiServer.Scheduler.ScheduledJobBase.OnStatusChanged(String statusMessage)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr data-start=&quot;2337&quot; data-end=&quot;2340&quot; /&gt;
&lt;h4 data-start=&quot;2342&quot; data-end=&quot;2362&quot;&gt;What&amp;rsquo;s Going On&lt;/h4&gt;
&lt;p data-start=&quot;2364&quot; data-end=&quot;2674&quot;&gt;Digging through the stack trace reveals that each call to &lt;code data-start=&quot;2422&quot; data-end=&quot;2439&quot;&gt;OnStatusChanged&lt;/code&gt; triggers a database write. Every single status update results in a new SQL transaction. If your job updates its status too frequently &amp;mdash; say, inside a loop &amp;mdash; you can easily overwhelm the database with hundreds or thousands of writes.&lt;/p&gt;
&lt;p data-start=&quot;2676&quot; data-end=&quot;2778&quot;&gt;In our case, this not only degraded performance but also caused transaction errors like the one above.&lt;/p&gt;
&lt;hr data-start=&quot;2780&quot; data-end=&quot;2783&quot; /&gt;
&lt;h4 data-start=&quot;2785&quot; data-end=&quot;2797&quot;&gt;The Fix&lt;/h4&gt;
&lt;p data-start=&quot;2799&quot; data-end=&quot;2978&quot;&gt;Once we understood what was happening, we simply removed (or drastically reduced) our calls to &lt;code data-start=&quot;2894&quot; data-end=&quot;2911&quot;&gt;OnStatusChanged&lt;/code&gt;. That immediately stabilized the system and reduced database load.&lt;/p&gt;
&lt;p data-start=&quot;2980&quot; data-end=&quot;3305&quot;&gt;If your scheduled jobs make frequent calls to &lt;code data-start=&quot;3026&quot; data-end=&quot;3043&quot;&gt;OnStatusChanged&lt;/code&gt;, we strongly suggest you review and limit them. Consider logging detailed progress elsewhere (e.g., a file, Application Insights, or custom monitoring) and only update the UI when it truly matters &amp;mdash; such as at the start, at major milestones, or upon completion.&lt;/p&gt;
&lt;hr data-start=&quot;3307&quot; data-end=&quot;3310&quot; /&gt;
&lt;h4 data-start=&quot;3312&quot; data-end=&quot;3325&quot;&gt;Takeaway&lt;/h4&gt;
&lt;p data-start=&quot;3327&quot; data-end=&quot;3476&quot;&gt;&lt;code data-start=&quot;3327&quot; data-end=&quot;3344&quot;&gt;OnStatusChanged&lt;/code&gt; is a useful feature, but it&amp;rsquo;s not free. Every update hits the database. Use it sparingly to keep your jobs reliable and performant.&lt;/p&gt;
&lt;p data-start=&quot;3478&quot; data-end=&quot;3533&quot;&gt;Hope this helps someone avoid the same headache we had!&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>            <guid>https://www.davidhome.net/blog/avoid-using-onstatuschanged-it-bothers-the-database/</guid>            <pubDate>Sun, 09 Nov 2025 18:19:02 GMT</pubDate>           <category>Blog post</category></item><item> <title>Disable Optimizely CMS Default Error Handling in ASP.NET Core</title>            <link>https://www.davidhome.net/blog/how-to-override-optimizely-cms-custom-error-handling-in-asp.net-core</link>            <description>&lt;div&gt;&lt;div&gt;
&lt;div&gt;&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;When building a .NET web application, it&#39;s common to use the combination of &lt;code&gt;UseExceptionHandler&lt;/code&gt; and &lt;code&gt;UseStatusCodePagesWithReExecute&lt;/code&gt; in your &lt;code&gt;Startup.cs&lt;/code&gt; to serve user-friendly error pages:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;app.UseExceptionHandler(&quot;/errorhandler/500&quot;);
app.UseStatusCodePagesWithReExecute(&quot;/errorhandler/{0}&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This works seamlessly&amp;mdash;even for Optimizely CMS solutions. However, we recently uncovered a caveat when working on a project involving custom API endpoints. It turns out Optimizely has its own hidden behavior regarding error handling, and it can silently interfere with your carefully configured pipeline.&lt;/p&gt;
&lt;pre&gt;&amp;nbsp;&lt;/pre&gt;
&lt;h3&gt;The Problem: Custom Errors for an API Segment&lt;/h3&gt;
&lt;p&gt;We had an API route that &lt;em&gt;intentionally&lt;/em&gt; returns raw HTTP status codes (e.g., 401 Unauthorized) depending on business logic. The expected behavior was to return the actual HTTP response code, not a friendly HTML error page.&lt;/p&gt;
&lt;p&gt;However, once &lt;code&gt;UseStatusCodePagesWithReExecute&lt;/code&gt; is enabled, even API endpoints like &lt;code&gt;/my/custom/api&lt;/code&gt; get intercepted, and you&amp;rsquo;ll end up with an HTML error response when your API returns a 401.&lt;/p&gt;
&lt;p&gt;So, we added conditional logic:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;app.UseWhen(
    context =&amp;gt; !context.Request.Path.StartsWithSegments(&quot;/my/custom/api&quot;),
    appBuilder =&amp;gt;
    {
        appBuilder.UseExceptionHandler(&quot;/errorhandler/500&quot;);
        appBuilder.UseStatusCodePagesWithReExecute(&quot;/errorhandler/{0}&quot;);
    });&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This works&amp;mdash;&lt;strong&gt;until you go to production&lt;/strong&gt;.&lt;/p&gt;
&lt;h3&gt;The Surprise: Optimizely Hooks in Its Own Middleware&lt;/h3&gt;
&lt;p&gt;To our surprise, our API was still returning Optimizely&#39;s custom error page in production. After some digging, we discovered that &lt;strong&gt;Optimizely CMS automatically registers its own exception and status code handling&lt;/strong&gt;, regardless of what you configure.&lt;/p&gt;
&lt;p&gt;This happens when you call &lt;code&gt;.AddCms()&lt;/code&gt;&amp;mdash;specifically, &lt;code&gt;.AddCmsUI()&lt;/code&gt; internally registers the following service:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services.AddStartupFilter&amp;lt;ErrorsStartupFilter&amp;gt;();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This filter looks like this:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public Action&amp;lt;IApplicationBuilder&amp;gt; Configure(Action&amp;lt;IApplicationBuilder&amp;gt; nextAction)
{
    return app =&amp;gt;
    {
        if (app.ApplicationServices.GetRequiredService&amp;lt;IWebHostEnvironment&amp;gt;().IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseStatusCodePagesWithReExecute(&quot;/Util/Errors/Error{0}&quot;);
            app.UseExceptionHandler(&quot;/Util/Errors/Error500&quot;);
        }

        nextAction(app);
    };
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So, Optimizely&amp;nbsp;&lt;em&gt;always&lt;/em&gt; wires in its own error handlers&amp;mdash;after yours.&lt;/p&gt;
&lt;h3&gt;Our Solution: Remove the Startup Filter&lt;/h3&gt;
&lt;p&gt;There&amp;rsquo;s no public configuration to disable this behavior. The class is &lt;code&gt;internal&lt;/code&gt;, so you can&amp;rsquo;t override or configure it. We had to remove it from the service collection ourselves:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;var errorStartupFilterServiceDescriptor = services
    .FirstOrDefault(descriptor =&amp;gt; descriptor.ImplementationType?.Name == &quot;ErrorsStartupFilter&quot;);

if (errorStartupFilterServiceDescriptor != null)
{
    services.Remove(errorStartupFilterServiceDescriptor);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By doing this early in &lt;code&gt;ConfigureServices&lt;/code&gt;, you prevent Optimizely&amp;rsquo;s error handlers from being injected, giving you full control over the pipeline again.&lt;/p&gt;
&lt;h3&gt;Final Thoughts&lt;/h3&gt;
&lt;p&gt;This is one of those &lt;em&gt;&quot;framework magic vs. developer control&quot;&lt;/em&gt; situations. It&amp;rsquo;s easy to miss because everything seems to work locally&amp;mdash;until production exposes the conflict.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Hopefully, this saves someone a few hours of debugging!&lt;/strong&gt;&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>            <guid>https://www.davidhome.net/blog/how-to-override-optimizely-cms-custom-error-handling-in-asp.net-core</guid>            <pubDate>Fri, 25 Jul 2025 06:19:24 GMT</pubDate>           <category>Blog post</category></item><item> <title>UrlResolver Bug in Optimizely DXP: Troubleshooting Inconsistent URLs in Scaled-Out Apps</title>            <link>https://www.davidhome.net/blog/optimizely-cms-content-not-the-same-between-different-nodes/</link>            <description>&lt;div&gt;&lt;div&gt;
&lt;div&gt;&lt;p data-start=&quot;356&quot; data-end=&quot;588&quot;&gt;A while back, our team encountered a puzzling production bug: URLs generated by the &lt;code data-start=&quot;440&quot; data-end=&quot;453&quot;&gt;UrlResolver&lt;/code&gt; would &lt;strong data-start=&quot;460&quot; data-end=&quot;479&quot;&gt;randomly differ&lt;/strong&gt; depending on who accessed them. The bug has since &lt;a href=&quot;https://world.optimizely.com/documentation/Release-Notes/ReleaseNote/?releaseNoteId=CMS-26486&quot;&gt;been fixed&lt;/a&gt;, and a patch is now available for installation.&lt;/p&gt;
&lt;p data-start=&quot;593&quot; data-end=&quot;840&quot;&gt;After some initial investigation and discussion with the team, we confirmed this was &lt;strong data-start=&quot;678&quot; data-end=&quot;709&quot;&gt;one of those elusive issues&lt;/strong&gt;&amp;mdash;reproducible only in certain browsers or for specific users. We couldn&amp;rsquo;t reproduce it locally, nor in our integration environment.&lt;/p&gt;
&lt;p data-start=&quot;845&quot; data-end=&quot;866&quot;&gt;So what was going on?&lt;/p&gt;
&lt;p data-start=&quot;871&quot; data-end=&quot;1152&quot;&gt;If you&#39;re familiar with Optimizely DXP, you know it runs on &lt;a href=&quot;https://azure.microsoft.com/en-us/products/app-service&quot;&gt;Azure App Services&lt;/a&gt;, with your app &lt;strong data-start=&quot;965&quot; data-end=&quot;1005&quot;&gt;scaled out across multiple instances&lt;/strong&gt;. For those new to the concept: scaling out means your application code runs on several servers in parallel to handle high web traffic efficiently.&lt;/p&gt;
&lt;p data-start=&quot;1157&quot; data-end=&quot;1260&quot;&gt;But there&#39;s a caveat&amp;mdash;when running code across multiple nodes,&amp;nbsp;&lt;strong data-start=&quot;1219&quot; data-end=&quot;1259&quot;&gt;synchronizing state becomes critical&lt;/strong&gt;.&lt;/p&gt;
&lt;p data-start=&quot;1265&quot; data-end=&quot;1497&quot;&gt;Optimizely CMS handles this using Azure Service Bus to propagate key events and updates across all nodes. Whether you&#39;re publishing content or stopping a scheduled job, those actions are broadcast so that all instances stay in sync.&lt;/p&gt;
&lt;p data-start=&quot;1502&quot; data-end=&quot;1809&quot;&gt;In our case, however, the problem was a &lt;strong data-start=&quot;1542&quot; data-end=&quot;1570&quot;&gt;cache invalidation issue&lt;/strong&gt; across nodes. One node properly refreshed its cache and generated the updated URL, while others continued using stale data. This led to inconsistent URL generation depending on which server a user hit&amp;mdash;hence the randomness in user reports.&lt;/p&gt;
&lt;p data-start=&quot;1502&quot; data-end=&quot;1809&quot;&gt;Here&#39;s a visual to illustrate the scaled-out app structure with multiple nodes, one Azure Service Bus, and a single point of entry.&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;                +----------------------+
                |      Load Balancer   |
                +----------+-----------+
                           |
      +--------------------+--------------------+
      |                    |                    |
+-----v-----+        +-----v-----+        +-----v-----+
|  Node A   |        |  Node B   |        |  Node C   |
| (Updated) |        | (Stale)   |        | (Stale)   |
+-----------+        +-----------+        +-----------+
      \                    |                    /
       \                   |                   /
        \         +--------v--------+         /
         +--------&amp;gt; Azure Service   &amp;lt;--------+
                  |      Bus        |
                  +-----------------+
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-start=&quot;1814&quot; data-end=&quot;2109&quot;&gt;You can test this behavior yourself (in production or preproduction) if scale-out is enabled. Just open your browser&amp;rsquo;s developer tools, inspect your cookies, and delete &lt;code data-start=&quot;1983&quot; data-end=&quot;1996&quot;&gt;ARRAffinity&lt;/code&gt; and &lt;code data-start=&quot;2001&quot; data-end=&quot;2022&quot;&gt;ARRAffinitySameSite&lt;/code&gt;. Reload the page. If a different GUID appears, you&amp;rsquo;ve been routed to a different node.&lt;/p&gt;
&lt;p data-start=&quot;2114&quot; data-end=&quot;2347&quot;&gt;The takeaway? &lt;strong data-start=&quot;2128&quot; data-end=&quot;2244&quot;&gt;If a bug appears non-deterministically across users or browsers, consider your application&amp;rsquo;s distributed nature.&lt;/strong&gt; Multi-node environments can introduce quirks that don&#39;t show up locally or in single-instance testing.&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>            <guid>https://www.davidhome.net/blog/optimizely-cms-content-not-the-same-between-different-nodes/</guid>            <pubDate>Wed, 18 Jun 2025 00:34:05 GMT</pubDate>           <category>Blog post</category></item><item> <title>Automating Optimizely DXP Deployments with GitLab CI/CD</title>            <link>https://www.davidhome.net/blog/optimizely-dxp-gitlab-pipeline/</link>            <description>&lt;div&gt;&lt;div&gt;
&lt;div&gt;&lt;h3&gt;&lt;strong&gt;Introduction&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;This blog post will explore the GitLab CI/CD pipeline configuration for deploying to Optimizely DXP. I wanted to be able to automate as much as possible every deployment steps to DXP, including the build stages, so that almost none of the operations were manual. The end result looks as followed:&amp;nbsp;&lt;a href=&quot;https://github.com/ddprince17/gitlab-cicd-dxp-template&quot;&gt;GitLab CI/CD DXP Template repository&lt;/a&gt;.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;I must say, this is clearly easier that I initially thought. In general, a full run, starting from the build to the latest stage where it deploys to production can take approximatively 30 minutes.&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;PowerShell and GitLab CI&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;GitLab uses bash as its default shell when you configure a runner. In my own setup, I have decided to use the &lt;a href=&quot;https://hub.docker.com/r/gitlab/gitlab-runner&quot;&gt;docker image&lt;/a&gt; and configure two different runners in the same docker instance (yes you can do this). One for running bash based scripts and the other that needs to run PowerShell based scripts. Consequently, the pipeline heavily utilizes PowerShell (&lt;code&gt;pwsh&lt;/code&gt; tag) for deployment tasks as they are used in my configuration to stipulate to use my PowerShell based runner instead of the default one. Optimizely DXP&#39;s APIs and deployment commands are PowerShell-based, making it a prerequisite for certain stages. You&#39;ll find PowerShell scripts handling package uploads and triggering deployments.&lt;/p&gt;
&lt;p&gt;Here&#39;s an example where the tag &lt;code&gt;pwsh&lt;/code&gt; has been specified:&amp;nbsp;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;send-package-dxp:
  stage: send-package
  image: mcr.microsoft.com/powershell:lts
  tags:
    - pwsh
  script:
    - Install-Module -Name EpiCloud -Force
    - Connect-EpiCloud -ClientKey $DXP_CLIENT_KEY -ClientSecret $DXP_CLIENT_SECRET -ProjectId $DXP_PROJECT_ID
    - $packageLocation = Get-EpiDeploymentPackageLocation
    - $foundPackageLocations = Get-ChildItem -Path $ARTIFACTS_LOCATION -Filter &quot;*.nupkg&quot; | Sort-Object -Property Name -Descending
    - $resolvedPackagePath = $foundPackageLocations | Select-Object -First 1
    - &quot;Write-Host \&quot;The following package will be deployed: $resolvedPackagePath\&quot;&quot;
    - Add-EpiDeploymentPackage -SasUrl $packageLocation -Path $resolvedPackagePath.FullName&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Without the &lt;code&gt;pwsh&lt;/code&gt; tag, these PowerShell commands would fail to execute properly in GitLab CI/CD, as the default shell does not support them.&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;Pipeline highlights&lt;/strong&gt;&lt;/h3&gt;
&lt;ul data-spread=&quot;false&quot;&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Handling Failed Deployments&lt;/strong&gt;: The pipeline includes rollback mechanisms to reset or complete failed deployments.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Manual Triggers for Preproduction and Production&lt;/strong&gt;: To add a safety net, the preproduction and production deployment stages are set to manual, allowing for final verification before releasing.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here&#39;s a visual example:&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.davidhome.net/contentassets/f1f5bc62e3bf4917a5eb0c117f14fb39/image.png&quot;&gt;&lt;img src=&quot;https://www.davidhome.net/contentassets/f1f5bc62e3bf4917a5eb0c117f14fb39/image.png?1740336480023#1740336546073&quot; alt=&quot;example 1&quot; width=&quot;300&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;And of the job dependencies:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.davidhome.net/contentassets/f1f5bc62e3bf4917a5eb0c117f14fb39/screenshot-2025-02-23-134850.png&quot;&gt;&lt;img src=&quot;https://www.davidhome.net/contentassets/f1f5bc62e3bf4917a5eb0c117f14fb39/screenshot-2025-02-23-134850.png&quot; alt=&quot;example 2&quot; width=&quot;300&quot; height=&quot;71&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;Final Thoughts&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;As a reminder, you can refer to the complete files available in the repository:&amp;nbsp;&lt;a href=&quot;https://github.com/ddprince17/gitlab-cicd-dxp-template&quot;&gt;GitLab CI/CD DXP Template&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;By using a well-defined CI/CD pipeline for Optimizely DXP, you can:&amp;nbsp;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Reduce manual deployment work.&lt;/li&gt;
&lt;li&gt;Ensure consistency across environments.&lt;/li&gt;
&lt;li&gt;Minimize deployment risks.&lt;/li&gt;
&lt;li&gt;Enable easy rollbacks if needed.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This GitLab CI template brings structure to your DXP deployments and makes life easier for your development and operations teams.&lt;/p&gt;
&lt;p&gt;Happy deploying! &#128640;&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>            <guid>https://www.davidhome.net/blog/optimizely-dxp-gitlab-pipeline/</guid>            <pubDate>Sun, 23 Feb 2025 18:56:37 GMT</pubDate>           <category>Blog post</category></item><item> <title>Enhancing Optimizely CMS Multi-Site Architecture with Structured Isolation</title>            <link>https://www.davidhome.net/blog/optimizely-cms-multi-site-setup/</link>            <description>&lt;div&gt;&lt;div&gt;
&lt;div&gt;&lt;p&gt;The main challenge of building an Optimizely CMS website is to think about its multi site capabilities up front. Making adjustment after the fact can be a difficult task and often requires a lot of refactoring.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;In this blog post, I&#39;ll talk about a bit on how I have found a way to easily isolate a single website, structurally speaking, under a C# solution.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Of course, this implies a couple of modifications and is far from perfect. But I thought sharing my discovery to the community could help a lot.&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;Goal&lt;/h2&gt;
&lt;p&gt;Of course building the architecture of&amp;nbsp;a solution to something that will easily accommodate&amp;nbsp;future development is always a question of determining whether it&#39;s worth the upfront efforts and cost that it entails. If your client is sure to have more than a single site in your Optimizely CMS project, well then the question of correctly structuring your solution up front is the best you can do to reduce the development complexity with a bigger and bigger code base that does a lot in a single place.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;This is something that we&#39;ve come across&amp;nbsp;quite often when clients were requesting us to make a new site under the same umbrella. What to do with the existing code base and avoid any regression while implementing the new website code base?&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Well then, here&#39;s my solution to it: routing based on the site name configured under Optimizely CMS. This essentially allow you to create a completely separated C# library project and isolate every business logic into that single bucket for that single CMS site.&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;In-dept details&lt;/h2&gt;
&lt;p&gt;Obviously there is not only &lt;em&gt;routing&lt;/em&gt; magic down there, but all in all, the goal was to modify every core elements of ASP.NET Core and Optimizely CMS that drives where and how certain elements are rendered.&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;Optimizely Content Types&lt;/h3&gt;
&lt;p&gt;By default, there is no controller for your content, you have to create one for your pages and your blocks. You are not forced to create a controller for your blocks, this is optional and also &lt;a href=&quot;https://docs.developers.optimizely.com/content-management-system/docs/content-templates#block-components-and-views&quot;&gt;recommended by Optimizely&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Avoiding to use a controller just to change the location of the partial view requires an adjustment&amp;nbsp;on the ASP.NET razor view engine. This is something that you can personalize by creating a class that implements a &lt;code&gt;IViewLocationExpander&lt;/code&gt;. That location expander needs to do a couple of things for us.&lt;/p&gt;
&lt;p&gt;First making sure the order of the locations are to the most precise to the less precised one. In the logic then goes like that:&amp;nbsp;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Look for a view that is inside a folder with the name of the site and the name of the content type.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;Look for a view that is inside a folder with the name of the site.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;Look for a view that is inside a folder with the name of the content type currently being rendered.&lt;/li&gt;
&lt;li&gt;Similarly to the previous steps, but inside the &quot;Shared&quot; folder. The expander will now look inside a &quot;Shared&quot; location for the site.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By the way, more information on the implementation will be shared later on. But you get the idea. This will essentially ask the view engine to look for locations that is only applicable for the site being requested. To give an example, this blog has been designed with that in mind. Here&#39;s a visual example of how the view structure looks:&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.davidhome.net/contentassets/8ed44ddf74c8406794985b4df2808242/image.png&quot; alt=&quot;Razor view structure&quot; width=&quot;300&quot; height=&quot;640&quot; /&gt;&lt;/p&gt;
&lt;p&gt;As you can see, every views are inside a folder with the name of the site. None of them are outside of it. This means two things:&amp;nbsp;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If I want to share the same content types between sites, it&#39;s entirely possible. They can:&amp;nbsp;
&lt;ul&gt;
&lt;li&gt;Use the same shared view.&lt;/li&gt;
&lt;li&gt;Have distinctive views for specific sites.&lt;/li&gt;
&lt;li&gt;Work in &lt;em&gt;hybrid&lt;/em&gt; mode, meaning that you could have both to work together.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Have a very specific &lt;code&gt;_ViewImports&lt;/code&gt; and &lt;code&gt;_ViewStart&lt;/code&gt; file for a site.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With this approach, the only thing needed to be done is to create a folder with the name of the site that will be created under Optimizely CMS and voila, you&#39;re good to go. The project you see in my previous screenshot doesn&#39;t contain a &lt;code&gt;Startup.cs&lt;/code&gt;. This is something only the starting assembly project have. This here is&amp;nbsp;&lt;em&gt;only&lt;/em&gt; a C# library project that is completely separated from the rest.&amp;nbsp;&lt;/p&gt;
&lt;h4&gt;But how does it know under which site and content type the request is being made?&amp;nbsp;&lt;/h4&gt;
&lt;p&gt;This is a good question. The short answer rely then again on an alteration to do on the ASP.NET Core routing engine.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Optimizely CMS uses what is known as the endpoint middleware. This is the reason why under your &lt;code&gt;Startup.cs&lt;/code&gt; file, you have to do something similar as that:&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;        app.UseEndpoints(endpoints =&amp;gt;
        {
            endpoints.MapContent();
        });&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;MapContent()&lt;/code&gt;&amp;nbsp;is essentially the method that hooks the entire CMS routing system on Microsoft&#39;s. Of course this is an oversimplification, but you get the picture. When the endpoint middleware is called, the matcher policies are called, and eventually the one from Optimizely is too. This consequently is used to load the correct page controller of the current http request.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;So, knowing a matcher policy is checking if the content from the CMS can be loaded, I&#39;ve hooked myself on that same system to add two variables in the route values:&amp;nbsp;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;One for knowing on which site this request is being made.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;To other to know which content type is loaded.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;code&gt;MultiSiteMatcher&lt;/code&gt; is adding those routing values only after the &lt;code&gt;ContentMatcherPolicy&lt;/code&gt; from Optimizely is running, making sure it is able to grab the correct data while adding them in the routing values.&lt;/p&gt;
&lt;h2&gt;Static assets&lt;/h2&gt;
&lt;p&gt;The other challenge with multi sites are the static assets. The classic setup with Optimizely CMS is to create a folder for a specific site under &lt;code&gt;wwwroot&lt;/code&gt; and then store all your assets in it for that site. All in all, you might want different scripts, styles, fonts, etc. and you don&#39;t necessarily want to share them with other sites.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;With this setup though, two things are happening:&amp;nbsp;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The browser load assets in a subfolder instead of&amp;nbsp;&lt;em&gt;looking&lt;/em&gt; like it&#39;s being loaded from the root of the website.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;You could technically load other assets that are not designed or meant for the site in the page.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Of course these problems are mainly cosmetic, but I felt like I needed to close the loop with this whole multi site thing correctly.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;There is a way to customize that and still keep a good separation between sites. By implementing a new &lt;code&gt;IFileProvider&lt;/code&gt;. Essentially there it&#39;s quite simple, this file provider is trying to find a file directly from a subpath of the requested site and if it cannot be found, fallback to the out of the box file provider.&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;NuGet package &amp;amp; Sources&lt;/h2&gt;
&lt;p&gt;All these concepts were implemented during the development of my blog. I have recently separated this logic into a NuGet package if you are interested.&amp;nbsp;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;NuGet: https://www.nuget.org/packages?q=DavidHome.Optimizely.MultiSite&amp;nbsp;&lt;/li&gt;
&lt;li&gt;Sources: &lt;a href=&quot;https://git.davidhome.net/web/davidhome-optimizely-multisite&quot;&gt;https://git.davidhome.net/web/davidhome-optimizely-multisite&lt;/a&gt;&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Please let me know what you think of! Of course if you&#39;d like to participate and contribute to the project, please let me know via LinkedIn!&amp;nbsp;&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>            <guid>https://www.davidhome.net/blog/optimizely-cms-multi-site-setup/</guid>            <pubDate>Sun, 09 Feb 2025 20:47:34 GMT</pubDate>           <category>Blog post</category></item><item> <title>Optimizely CMS easy RSS feed integration library</title>            <link>https://www.davidhome.net/blog/optimizely-cms-easy-rss-feed-integration-library/</link>            <description>&lt;div&gt;&lt;div&gt;
&lt;div&gt;&lt;p&gt;As I&#39;ve mentioned in my&amp;nbsp;&lt;a href=&quot;https://www.davidhome.net/blog/my-blog-is-now-running-using-optimizely-cms/&quot;&gt;previous blog post&lt;/a&gt;, while I was developing the Optimizely version of my blog, I tried to look for a library that could accommodate&amp;nbsp;my RSS feeds needs with the CMS. I&#39;ve found &lt;a href=&quot;https://www.hiddenfoundry.com/thoughts/rss-feed-nuget-package-for-optimizely-cms/&quot;&gt;one&lt;/a&gt; made by another fellow OMVP peer, but unfortunately it wasn&#39;t quite covering my own requirements and I thought why not developing it from scratch altogether.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Some might thing it&#39;s overkill, but it doesn&#39;t matter, I had fun designing &amp;amp; building it. &#128516;&lt;/p&gt;
&lt;h3&gt;Introducing DavidHome.RssFeed&lt;/h3&gt;
&lt;p&gt;This NuGet package integrates seamlessly into your Optimizely project and uses Azure Blob Storage for hosting your feeds. Whether you&#39;re building a single feed or managing multiple content streams, this tool is ready to meet your needs, and even better, its&amp;nbsp;extensible!&lt;/p&gt;
&lt;p&gt;I know there is probably stuff that has been totally overlooked, but any contribution will be greatly welcomed. If you&#39;d like to, simply contact me via LinkedIn and I&#39;ll give you the required access to my git repository.&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;Getting Started&lt;/h3&gt;
&lt;p&gt;First, install the required NuGet packages:&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;dotnet add package DavidHome.RssFeed.Optimizely
dotnet add package DavidHome.RssFeed.Storage.AzureBlob&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then configure the plugin in your &lt;code&gt;Startup.cs&lt;/code&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Configure Azure Blob Storage (inside &lt;code&gt;ConfigureServices&lt;/code&gt;)&lt;br /&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;services.AddDavidHomeRssFeed(configuration)
    .AddOptimizelyFeedIntegration(configuration)
    .AddDefaultOptimizelyProcessors()
    .AddContentPageFeed&amp;lt;MyContainer, MyItem&amp;gt;()
    .AddAzureBlobStorage(_configuration.GetSection(&quot;ConnectionStrings&quot;).GetSection(&quot;EPiServerAzureBlobs&quot;));&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Initialize Blob Storage (inside &lt;code&gt;Configure&lt;/code&gt;)&lt;/strong&gt;&lt;br /&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;app.UseAzureBlobRssFeed();&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Heads up:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Azure Blob Storage is &lt;strong&gt;mandatory&lt;/strong&gt; for this plugin. That said, if you&#39;re&amp;nbsp;feeling adventurous, you can fork the project and add your own storage implementation or create your own NuGet package to extend the functionality.&lt;/li&gt;
&lt;li&gt;You need to update &quot;Microsoft.Extensions.Azure&quot; to the latest version, or the further you can, so that support to use the direct IConfiguration section having the connection string information works as demonstrated there.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Fine-Tuning Your Feeds&lt;/h3&gt;
&lt;p&gt;The plugin follows a configuration-less design, meaning you&#39;re&amp;nbsp;good to go with just the defaults. But for those who want to tweak things, you can use &lt;code&gt;appsettings.json&lt;/code&gt; to configure your feeds:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;{
  &quot;DavidHome&quot;: {
    &quot;RssFeed&quot;: {
      &quot;ContentMaxLength&quot;: 25000000,
      &quot;MyCustomContainerPageTypeName&quot;: {
        &quot;ContentMaxLength&quot;: 15000000,
        &quot;ContentAreaPropertyName&quot;: &quot;MainContentArea&quot;,
        &quot;FeedTitlePropertyName&quot;: &quot;HeadTitle&quot;
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Key Optimizely-Specific Options&lt;/h3&gt;
&lt;p&gt;For &lt;a href=&quot;https://git.davidhome.net/web/davidhome-rssfeed#configuration&quot;&gt;Optimizely specific options&lt;/a&gt;, the following are available:&amp;nbsp;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;FeedRelativeUrl: Allows customization of the relative URL of your feed(s) based on the location of your container page(s) in your CMS tree.&lt;/li&gt;
&lt;li&gt;ContentAreaPropertyName: When provided, it will automatically extract the content of the content area as HTML and use it as the description field in your syndication item.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;FeedTitlePropertyName: This optional property is useful if you want to change the title of the link meta tag in your DOM head tag that can be generated when calling &lt;code&gt;@Html.SyndicationLink()&lt;/code&gt;&amp;nbsp;in your layout page.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can personalize every feed, and yes, you can have multiple feeds targeting different sections of your CMS tree!&lt;/p&gt;
&lt;h3&gt;Setting Up Your Content Types&lt;/h3&gt;
&lt;p&gt;To integrate with Optimizely&amp;rsquo;s data structure, you&#39;ll&amp;nbsp;need to apply these marker interfaces to your content types:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;IRssFeedContainer&amp;lt;TFeedItem&amp;gt;&lt;/code&gt; for container types.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;IRssFeedItem&amp;lt;TFeedContainer&amp;gt;&lt;/code&gt; for feed items.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Pro Tip:&lt;/strong&gt; Add the &lt;code&gt;IgnoreAttribute&lt;/code&gt; to unsupported properties.&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;Azure Blob Storage&lt;/h3&gt;
&lt;p&gt;Azure Blob Storage powers the backend for your RSS feeds. It stores generated feeds over there so that when it&#39;s being requested, it doesn&#39;t go back to the database and try to build it again on the fly. This allows a fast and seamless delivery of the file and avoids to use your precious CPU/database computing resources. A scheduled job is automatically running in the background to generate and store the file representing the syndication feed of your content. It&#39;s scheduled by default to run each hours, but can be easily customized under the&amp;nbsp;Optimizely CMS administrative interface.&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;Room for Innovation&lt;/h3&gt;
&lt;p&gt;The library is built with extensibility in mind. While Azure Blob Storage is the default (and required for now), you&#39;re welcome to dive into the source code and add your own flair. Whether that&#39;s&amp;nbsp;another storage provider or custom processors, the door is wide open for contributions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ready to get started?&lt;/strong&gt; &lt;a href=&quot;https://git.davidhome.net/web/davidhome-rssfeed&quot; target=&quot;_new&quot; rel=&quot;noopener&quot;&gt;Check out the source code here&lt;/a&gt; and give it a try.&amp;nbsp;&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>            <guid>https://www.davidhome.net/blog/optimizely-cms-easy-rss-feed-integration-library/</guid>            <pubDate>Sat, 25 Jan 2025 01:17:04 GMT</pubDate>           <category>Blog post</category></item><item> <title>My blog is now running using Optimizely CMS!</title>            <link>https://www.davidhome.net/blog/my-blog-is-now-running-using-optimizely-cms/</link>            <description>&lt;div&gt;&lt;div&gt;
&lt;div&gt;&lt;p&gt;It&#39;s official! You are currently reading this post on my shiny new Optimizely CMS website.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;In the past weeks, I have been quite busy crunching every items to transfer my originally developed website from Orchard CMS to Optimizely CMS. It was quite the experience and also, a lot of new opportunities are now on the horizon.&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;Now what?&lt;/h2&gt;
&lt;p&gt;During this experience, I was able to take my time to tackle certain architectural challenges that we&#39;re often facing when creating a new Optimizely CMS website. So in the upcoming days/weeks, I will be able to talk about these, but here&#39;s a sneak&amp;nbsp;peek on the topics I&#39;d like to talk about:&amp;nbsp;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What&#39;s the right project structure, or at least, best&amp;nbsp;&lt;em&gt;suggested&lt;/em&gt;&amp;nbsp;structure,&amp;nbsp;when building the project with a multi site setup in mind.&lt;/li&gt;
&lt;li&gt;Creation &amp;amp; announcement of a new NuGet feed that will ease the multi site setup for developers.&lt;/li&gt;
&lt;li&gt;For those interested, I have also developed a custom RSS feed integration for Optimizely CMS that I plan on publishing. You can see it in action on this blog:&amp;nbsp;&lt;a href=&quot;https://www.davidhome.net/rss/&quot;&gt;https://www.davidhome.net/rss/&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;GitLab pipelines. I have fully automated builds &amp;amp; deployments of the website using this technology. I&#39;m normally accustomed to use Azure DevOps, but for this personal project, it was a great opportunity to learn something different using my own GitLab instance.&lt;/li&gt;
&lt;li&gt;Maybe an open topic on the mediator pattern? I&#39;ve used the library MediatR for my blog.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Stay tuned &amp;amp; happy coding!&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>            <guid>https://www.davidhome.net/blog/my-blog-is-now-running-using-optimizely-cms/</guid>            <pubDate>Sun, 12 Jan 2025 21:36:56 GMT</pubDate>           <category>Blog post</category></item><item> <title>Optimizely Content Graph - Sync Computed Getter Properties like with Search &amp; Navigation</title>            <link>https://www.davidhome.net/blog/optimizely-content-graph-sync-computed-getter-properties-like-with-search-navigation</link>            <description>&lt;p&gt;Recently, I have been re-writing my blog using Optimizely CMS. During this process, I wanted to try new released technologies offered by Optimizely. Today, I&#39;m going to talk a bit about my experience with Content Graph.&lt;/p&gt;
&lt;p&gt;Of course, I thought about using Search &amp;amp; Navigation, but I think it was beneficial that I learned something that will eventually, to my opinion, replace it entirely.&lt;/p&gt;
&lt;p&gt;With Search &amp;amp; Navigation, the majority of Optimizely developers are well aware that you can synchronize not only properties meant for the content type itself, but also the ones that are computed, meaning only a getter has been used. The following example demonstrate that (see FirstPublishedDate):&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/Screenshot%202024-10-18%20153816.png&quot;&gt;&lt;img src=&quot;/media/Screenshot%202024-10-18%20153816.png?width=160&quot; alt=&quot;Figure 1&quot; title=&quot;Figure 2&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If you use Search &amp;amp; Navigation, this property is getting indexed and can be used on any query. It also means you can order your result with that computed field and do a bunch of cool tricks, such as including different ways to represent a certain element from the content type in a fashion consumable by the Search &amp;amp; Navigation C# client. Complex types aren’t really its force, so we often add getter only properties to return a more simplified, but useful, data format.&lt;/p&gt;
&lt;p&gt;So, under Content Graph, I was under the impression that I could do the same thing, but alas, no, you can&#39;t for the moment. But Optimizely did confirmed to myself via a support request they have plans to add something that would allow developers to customize and add the ability to extend the data synchronization of content types. In the meantime, I have concocted a very... but very hackish workaround that allow you to synchronize getter properties. Before moving forward with the solution, a little explanation is in order to understand a bit more what you can/can&#39;t do:&lt;/p&gt;
&lt;p&gt;The schedule job &quot;Optimizely Graph content synchronization job&quot; is the main responsible to do the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Scan for all existing content types and synchronize them to Content Graph. It&#39;s using their definitions to know the structure your content should have under its own engine.&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;By the way, you can also use their endpoints to synchronize content types completely unrelated to the CMS: &lt;a href=&quot;https://docs.developers.optimizely.com/platform-optimizely/v1.4.0-optimizely-graph/docs/synchronize-content-types&quot;&gt;https://docs.developers.optimizely.com/platform-optimizely/v1.4.0-optimizely-graph/docs/synchronize-content-types&lt;/a&gt;. Very useful in scenarios where you need content from different external sources, but all available in the same place. Super powerful and allow the leverage of the engine to do extremely useful searches.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Then scans for all existing content in the CMS and synchronize all of its data under Content Graph. &lt;strong&gt;This is the step that interests us&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;While digging for a solution to extend my content type and allow the synchronization of getter only properties, I have realized the following must be respected:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Your property &lt;strong&gt;cannot&lt;/strong&gt; be a getter only, you still have to put a setter. It&#39;s because otherwise the property is not considered a &lt;em&gt;real&lt;/em&gt; CMS property and thus is getting ignored during the indexation. A &lt;em&gt;real&lt;/em&gt; property will appear under &quot;CMS -&amp;gt; Settings -&amp;gt; Content Type&quot; menu.&lt;/li&gt;
&lt;li&gt;So I simply added a setter using &lt;code&gt;this.SetPropertyValue()&lt;/code&gt; to get over this limitation.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once you get your property synchronized to Optimizely database comes the part where the indexation job of Content Graph is still ignoring it. While reading the decompiled code of Optimizely, I learned that the current business logic is &lt;strong&gt;only&lt;/strong&gt; relying on the data stored within the CMS database, which means that in the end &lt;strong&gt;all&lt;/strong&gt; defined elements in the code is entirely ignored, since the job is only directly pulling the data inside the internal content type tables.&lt;/p&gt;
&lt;p&gt;So, well, this is where I come with the idea to build a dynamic type. I had to create a dynamic assembly which was allowed to interact with &quot;internal&quot; types of the &quot;Optimizely.ContentGraph.Cms.NetCore&quot; assembly. In the end, I had to create IL code so that my desired behavior could be added to the synchronization job. To keep that brief, if you want the solution, here&#39;s my &lt;a href=&quot;https://github.com/ddprince17/optimizely-content-graph&quot;&gt;GitHub repository&lt;/a&gt; with a small example. The dynamic type is inheriting from &quot;ContentGraphContentConverter&quot; and is replacing the original type under the IoC container. Under the &quot;Convert&quot; method, instead of directly returning the model, I&#39;m calling the extension AddReadonlyProperties, which then allow me to customize the return value.&lt;/p&gt;
&lt;p&gt;Happy coding!&lt;/p&gt;
</description>            <guid>https://www.davidhome.net/blog/optimizely-content-graph-sync-computed-getter-properties-like-with-search-navigation</guid>            <pubDate>Fri, 18 Oct 2024 21:13:50 GMT</pubDate>           <category>Blog post</category></item><item> <title>Optimizely Content Graph - Sync Computed Getter Properties like with Search &amp; Navigation</title>            <link>https://www.davidhome.net/blog/optimizely-content-graph-sync-computed-getter-properties-like-with-search-navigation/</link>            <description>&lt;div&gt;&lt;div&gt;
&lt;div&gt;&lt;p&gt;Recently, I have been re-writing my blog using Optimizely CMS. During this process, I wanted to try new released technologies offered by Optimizely. Today, I&#39;m going to talk a bit about my experience with Content Graph.&lt;/p&gt;
&lt;p&gt;Of course, I thought about using Search &amp;amp; Navigation, but I think it was beneficial that I learned something that will eventually, to my opinion, replace it entirely.&lt;/p&gt;
&lt;p&gt;With Search &amp;amp; Navigation, the majority of Optimizely developers are well aware that you can synchronize not only properties meant for the content type itself, but also the ones that are computed, meaning only a getter has been used. The following example demonstrate that (see FirstPublishedDate):&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.davidhome.net/contentassets/22ee54e089604fc4b4e70b23b51da9c7/screenshot202024-10-18201538161.png&quot;&gt;&lt;img src=&quot;https://www.davidhome.net/contentassets/22ee54e089604fc4b4e70b23b51da9c7/screenshot202024-10-18201538161.png&quot; alt=&quot;Figure 1&quot; width=&quot;160&quot; height=&quot;67&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If you use Search &amp;amp; Navigation, this property is getting indexed and can be used on any query. It also means you can order your result with that computed field and do a bunch of cool tricks, such as including different ways to represent a certain element from the content type in a fashion consumable by the Search &amp;amp; Navigation C# client. Complex types aren&amp;rsquo;t really its force, so we often add getter only properties to return a more simplified, but useful, data format.&lt;/p&gt;
&lt;p&gt;So, under Content Graph, I was under the impression that I could do the same thing, but alas, no, you can&#39;t for the moment. But Optimizely did confirmed to myself via a support request they have plans to add something that would allow developers to customize and add the ability to extend the data synchronization of content types. In the meantime, I have concocted a very... but very hackish workaround that allow you to synchronize getter properties. Before moving forward with the solution, a little explanation is in order to understand a bit more what you can/can&#39;t do:&lt;/p&gt;
&lt;p&gt;The schedule job &quot;Optimizely Graph content synchronization job&quot; is the main responsible to do the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Scan for all existing content types and synchronize them to Content Graph. It&#39;s using their definitions to know the structure your content should have under its own engine.&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;By the way, you can also use their endpoints to synchronize content types completely unrelated to the CMS:&amp;nbsp;&lt;a href=&quot;https://docs.developers.optimizely.com/platform-optimizely/v1.4.0-optimizely-graph/docs/synchronize-content-types&quot;&gt;https://docs.developers.optimizely.com/platform-optimizely/v1.4.0-optimizely-graph/docs/synchronize-content-types&lt;/a&gt;. Very useful in scenarios where you need content from different external sources, but all available in the same place. Super powerful and allow the leverage of the engine to do extremely useful searches.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Then scans for all existing content in the CMS and synchronize all of its data under Content Graph.&amp;nbsp;&lt;strong&gt;This is the step that interests us&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;While digging for a solution to extend my content type and allow the synchronization of getter only properties, I have realized the following must be respected:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Your property&amp;nbsp;&lt;strong&gt;cannot&lt;/strong&gt;&amp;nbsp;be a getter only, you still have to put a setter. It&#39;s because otherwise the property is not considered a&amp;nbsp;&lt;em&gt;real&lt;/em&gt;&amp;nbsp;CMS property and thus is getting ignored during the indexation. A&amp;nbsp;&lt;em&gt;real&lt;/em&gt;&amp;nbsp;property will appear under &quot;CMS -&amp;gt; Settings -&amp;gt; Content Type&quot; menu.&lt;/li&gt;
&lt;li&gt;So I simply added a setter using&amp;nbsp;&lt;code&gt;this.SetPropertyValue()&lt;/code&gt;&amp;nbsp;to get over this limitation.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once you get your property synchronized to Optimizely database comes the part where the indexation job of Content Graph is still ignoring it. While reading the decompiled code of Optimizely, I learned that the current business logic is&amp;nbsp;&lt;strong&gt;only&lt;/strong&gt;&amp;nbsp;relying on the data stored within the CMS database, which means that in the end&amp;nbsp;&lt;strong&gt;all&lt;/strong&gt;&amp;nbsp;defined elements in the code is entirely ignored, since the job is only directly pulling the data inside the internal content type tables.&lt;/p&gt;
&lt;p&gt;So, well, this is where I come with the idea to build a dynamic type. I had to create a dynamic assembly which was allowed to interact with &quot;internal&quot; types of the &quot;Optimizely.ContentGraph.Cms.NetCore&quot; assembly. In the end, I had to create IL code so that my desired behavior could be added to the synchronization job. To keep that brief, if you want the solution, here&#39;s my&amp;nbsp;&lt;a href=&quot;https://github.com/ddprince17/optimizely-content-graph&quot;&gt;GitHub repository&lt;/a&gt;&amp;nbsp;with a small example. The dynamic type is inheriting from &quot;ContentGraphContentConverter&quot; and is replacing the original type under the IoC container. Under the &quot;Convert&quot; method, instead of directly returning the model, I&#39;m calling the extension AddReadonlyProperties, which then allow me to customize the return value.&lt;/p&gt;
&lt;p&gt;Happy coding!&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>            <guid>https://www.davidhome.net/blog/optimizely-content-graph-sync-computed-getter-properties-like-with-search-navigation/</guid>            <pubDate>Fri, 18 Oct 2024 04:00:00 GMT</pubDate>           <category>Blog post</category></item><item> <title>GetNextSegment with empty Remaining causing fuzzes</title>            <link>https://www.davidhome.net/blog/getnextsegment-with-empty-remaining-causing-fuzzes</link>            <description>&lt;p&gt;Optimizely CMS offers you to create partial routers. This concept allows you display content differently depending on the routed content in the URL. Of course, a more in-dept explanation can be found at the following URL: &lt;a href=&quot;https://docs.developers.optimizely.com/content-management-system/docs/example-of-news-partial-routing&quot;&gt;https://docs.developers.optimizely.com/content-management-system/docs/example-of-news-partial-routing&lt;/a&gt;. All in all, this concept also existed in the previous version of the CMS, but it differed a bit than it is currently.&lt;/p&gt;
&lt;p&gt;See, when we migrated a customer from .NET Framework to .NET Core, AKA CMS 11 to 12, we encountered a memory leak that we couldn&#39;t really put our hand on at first sight. The solution heavily used those partial routers. Our issue also often occurred on DXP and not on our development machine, which was even weirder. Until we remembered that the application is automatically &lt;a href=&quot;https://docs.developers.optimizely.com/digital-experience-platform/docs/warming-up-sites&quot;&gt;warmed up&lt;/a&gt; when running under DXP. This led us to find the source of the problem, the partial routers, but more specifically on the part when we get the next value of the URL segment.&lt;/p&gt;
&lt;p&gt;As explained in the &lt;a href=&quot;https://docs.developers.optimizely.com/content-management-system/docs/example-of-news-partial-routing&quot;&gt;CMS 12 documentation&lt;/a&gt;, you can use from an &lt;code&gt;UrlResolverContext&lt;/code&gt; the method &lt;code&gt;GetNextSegment&lt;/code&gt; to get that information. It returns a &lt;code&gt;Segment&lt;/code&gt;, which contains what&#39;s &quot;next&quot; and the &quot;remaining path&quot;. Our code, since it was originally developed for CMS 11, used at the time the old API, a method &quot;GetNextValue&quot;, probably from a similar service which I don&#39;t reminder the name and was looping around it until Optimizely code returned an empty segment. It all makes sense right? Here&#39;s the decompiled code from CMS 11:
&lt;img src=&quot;/media/image001.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;As you can see, CMS 11 was simply returning an empty &quot;next&quot; and &quot;remaining&quot; &lt;code&gt;SegmentPair&lt;/code&gt; if the remaining path was empty. Our code was relying on this information, well, specifically the property &quot;Remaining&quot; of the segment pair. I&#39;m sure at this point you&#39;re probably realizing why we were having memory leaks. Indeed, under CMS 12, this piece of code changed and caused infinite loops in ours, concatenating string &lt;em&gt;to infinity and beyond&lt;/em&gt;. The change itself is very inexplicable I would say, even myself don&#39;t understand why Optimizely decided to make it that way, it feels more like an unwanted error than anything else. Here&#39;s a snapshot of the decompiled code, this will speak for itself:
&lt;img src=&quot;/media/Screenshot 2024-07-07 232757.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;It, hum, well, reassign the remaining path if it&#39;s empty, but also have &lt;strong&gt;the same check just after&lt;/strong&gt;, a condition, which at this point, will never be met, that simply does &lt;strong&gt;what we would expect&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;We informed Optimizely of this quirk, though unfortunately didn&#39;t ended up being fixed. Our code now has different checks and also a failsafe to avoid looping indefinitely. I hope that with this post can save you time diagnosing similar problems on your end.&lt;/p&gt;
</description>            <guid>https://www.davidhome.net/blog/getnextsegment-with-empty-remaining-causing-fuzzes</guid>            <pubDate>Mon, 08 Jul 2024 03:50:26 GMT</pubDate>           <category>Blog post</category></item><item> <title>GetNextSegment with empty Remaining causing fuzzes</title>            <link>https://www.davidhome.net/blog/getnextsegment-with-empty-remaining-causing-fuzzes/</link>            <description>&lt;div&gt;&lt;div&gt;
&lt;div&gt;&lt;p&gt;Optimizely CMS offers you to create partial routers. This concept allows you display content differently depending on the routed content in the URL. Of course, a more in-dept explanation can be found at the following URL:&amp;nbsp;&lt;a href=&quot;https://docs.developers.optimizely.com/content-management-system/docs/example-of-news-partial-routing&quot;&gt;https://docs.developers.optimizely.com/content-management-system/docs/example-of-news-partial-routing&lt;/a&gt;. All in all, this concept also existed in the previous version of the CMS, but it differed a bit than it is currently.&lt;/p&gt;
&lt;p&gt;See, when we migrated a customer from .NET Framework to .NET Core, AKA CMS 11 to 12, we encountered a memory leak that we couldn&#39;t really put our hand on at first sight. The solution heavily used those partial routers. Our issue also often occurred on DXP and not on our development machine, which was even weirder. Until we remembered that the application is automatically&amp;nbsp;&lt;a href=&quot;https://docs.developers.optimizely.com/digital-experience-platform/docs/warming-up-sites&quot;&gt;warmed up&lt;/a&gt;&amp;nbsp;when running under DXP. This led us to find the source of the problem, the partial routers, but more specifically on the part when we get the next value of the URL segment.&lt;/p&gt;
&lt;p&gt;As explained in the&amp;nbsp;&lt;a href=&quot;https://docs.developers.optimizely.com/content-management-system/docs/example-of-news-partial-routing&quot;&gt;CMS 12 documentation&lt;/a&gt;, you can use from an&amp;nbsp;&lt;code&gt;UrlResolverContext&lt;/code&gt;&amp;nbsp;the method&amp;nbsp;&lt;code&gt;GetNextSegment&lt;/code&gt;&amp;nbsp;to get that information. It returns a&amp;nbsp;&lt;code&gt;Segment&lt;/code&gt;, which contains what&#39;s &quot;next&quot; and the &quot;remaining path&quot;. Our code, since it was originally developed for CMS 11, used at the time the old API, a method &quot;GetNextValue&quot;, probably from a similar service which I don&#39;t reminder the name and was looping around it until Optimizely code returned an empty segment. It all makes sense right? Here&#39;s the decompiled code from CMS 11:&amp;nbsp;&lt;img src=&quot;https://www.davidhome.net/contentassets/d4ddd2d2f5c440eca0ded2148cedf170/image0011.png&quot; alt=&quot;Image 1&quot; width=&quot;1278&quot; height=&quot;760&quot; /&gt;&lt;/p&gt;
&lt;p&gt;As you can see, CMS 11 was simply returning an empty &quot;next&quot; and &quot;remaining&quot;&amp;nbsp;&lt;code&gt;SegmentPair&lt;/code&gt;&amp;nbsp;if the remaining path was empty. Our code was relying on this information, well, specifically the property &quot;Remaining&quot; of the segment pair. I&#39;m sure at this point you&#39;re probably realizing why we were having memory leaks. Indeed, under CMS 12, this piece of code changed and caused infinite loops in ours, concatenating string&amp;nbsp;&lt;em&gt;to infinity and beyond&lt;/em&gt;. The change itself is very inexplicable I would say, even myself don&#39;t understand why Optimizely decided to make it that way, it feels more like an unwanted error than anything else. Here&#39;s a snapshot of the decompiled code, this will speak for itself:&amp;nbsp;&lt;img src=&quot;https://www.davidhome.net/contentassets/d4ddd2d2f5c440eca0ded2148cedf170/screenshot202024-07-07202327571.png&quot; alt=&quot;Image 2&quot; width=&quot;1252&quot; height=&quot;956&quot; /&gt;&lt;/p&gt;
&lt;p&gt;It, hum, well, reassign the remaining path if it&#39;s empty, but also have&amp;nbsp;&lt;strong&gt;the same check just after&lt;/strong&gt;, a condition, which at this point, will never be met, that simply does&amp;nbsp;&lt;strong&gt;what we would expect&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;We informed Optimizely of this quirk, though unfortunately didn&#39;t ended up being fixed. Our code now has different checks and also a failsafe to avoid looping indefinitely. I hope that with this post can save you time diagnosing similar problems on your end.&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>            <guid>https://www.davidhome.net/blog/getnextsegment-with-empty-remaining-causing-fuzzes/</guid>            <pubDate>Sun, 07 Jul 2024 04:00:00 GMT</pubDate>           <category>Blog post</category></item><item> <title>How NDepend can quickly help you find code quality issues and resolve them</title>            <link>https://www.davidhome.net/blog/how-ndepend-can-quickly-help-you-find-code-quality-issues-and-resolve-them/</link>            <description>&lt;div&gt;&lt;div&gt;
&lt;div&gt;&lt;p&gt;NDepend has been around&amp;nbsp;&lt;a href=&quot;https://web.archive.org/web/20060518122151/https://www.ndepend.com/&quot;&gt;for quite a long time&lt;/a&gt;. If you are unaware of what it does, you should be checking it. It&#39;s an analysis tools which helps you find inconsistencies and discrepancies in your C# source code. I&#39;ve been trying it out recently and helped me found certain issue which I wasn&#39;t aware of.&lt;/p&gt;
&lt;p&gt;To be clear, there is indeed other available solution and it&#39;s up to your team to decide which fits best with you. In this case, NDepend runs independently from any IDE, but you can install an integration plugin inside Visual Studio. This can give the leverage/advantage to avoid loading any IDE to obtain a report. Unfortunately, there is no plugin/extension support under JetBrains Rider, my personal IDE of choice, like there is under Visual Studio. I hope their team can implement one, that would be greatly valuable.&lt;/p&gt;
&lt;h3&gt;Dashboard&lt;/h3&gt;
&lt;p&gt;Once a solution analysis is completed, you will be greeted with the dashboard tab:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.davidhome.net/contentassets/35299a47b8d442dfb348d60119617eb6/screenshot202024-01-22201216421.png&quot;&gt;&lt;img title=&quot;Figure 1&quot; src=&quot;https://www.davidhome.net/contentassets/35299a47b8d442dfb348d60119617eb6/screenshot202024-01-22201216421.png&quot; alt=&quot;Figure 1&quot; width=&quot;160&quot; height=&quot;87&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Here lies a general overview of the quality of the whole solution that you have analyzed. This gives a great quick shot of the elements you could get your hand onto if your goal is to refactor the code to make it more maintainable. You can personalize the rules and change them to your likings. For example, in my previous screenshot, there is one critical rule that flags I&#39;m using a type within another type which have both different namespaces.&amp;nbsp;&lt;a href=&quot;https://www.ndepend.com/default-rules/NDepend-Rules-Explorer.html?ruleid=ND1400#!&quot;&gt;The debt explanation is interesting&lt;/a&gt;. Of course, you can always decide to ignore it, but it gives an interesting point of view regarding the fact that some developers are using folders within a project to &quot;organize&quot; classes, which can lead to this kind of rule alert. Of course, everything is to take with a grain of salt, as I agree with someone from the comments of&amp;nbsp;&lt;a href=&quot;https://stackoverflow.com/questions/59519084/how-to-avoid-namespaces-dependency-cycles-between-my-entities&quot;&gt;this Stack Overflow thread&lt;/a&gt;, these kind of alerts can be opinion based. An interesting thing that NDepend&#39;s team could be adding inside their analysis program is rule categorization; Opiniated ones versus the others by example. It could help to quickly address undiscussable items and leave any development team to discuss the other opiniated points and see whether they need to be addressed.&lt;/p&gt;
&lt;h3&gt;Dependency Graph&lt;/h3&gt;
&lt;p&gt;I think the most interesting feature of NDepend is the dependency graph. It&#39;s so incredibly useful to quickly identify areas of your code that aren&#39;t supposed to inherit another project within your solution. This can otherwise, to my opinion, often lead to solutions having classes/elements heavily coupled/entangled together. Based on the same project that I have used to create my first screenshot, here is an example of the graph:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.davidhome.net/contentassets/35299a47b8d442dfb348d60119617eb6/screenshot202024-01-23200946081.png&quot;&gt;&lt;img title=&quot;Figure 2&quot; src=&quot;https://www.davidhome.net/contentassets/35299a47b8d442dfb348d60119617eb6/screenshot202024-01-23200946081.png&quot; alt=&quot;Figure 2&quot; width=&quot;160&quot; height=&quot;82&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;As you can realize in the previous screenshot, there is a design pattern here that I often follow in projects:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The website is the main &quot;referrer&quot; of projects, meaning that nothing should be referencing the website.
&lt;ul&gt;
&lt;li&gt;The exception are the unit tests. You can also realize the only additional reference of the test projects are the &quot;Models&quot; projects. Nothing else.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;The &quot;Bootstrappers&quot; projects serve only the purpose to add inside the IoC container the necessary services that are consumable by all &quot;Contracts&quot; projects.&lt;/li&gt;
&lt;li&gt;Each consumable &quot;Contracts&quot; projects comes in pair with a minimum of an additional &quot;concrete&quot; implementation project. These projects, as previously mentioned, only contain the implementation your contracts, which will then be consumed by the website.&lt;/li&gt;
&lt;li&gt;Almost everything consumes the &quot;Models&quot;. This is again, by design. Models are the basically &quot;the end of the road&quot;, it should generally refer nothing else.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;I will be doing another blog post about the design pattern I personally follow in projects.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;NDepend quickly identified a discrepancy in my project: There is a direct reference from the website to the database library and this is not normal. Double clicking on the arrow gives me the following:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.davidhome.net/contentassets/35299a47b8d442dfb348d60119617eb6/screenshot202024-01-23201034241.png&quot;&gt;&lt;img title=&quot;Figure 3&quot; src=&quot;https://www.davidhome.net/contentassets/35299a47b8d442dfb348d60119617eb6/screenshot202024-01-23201034241.png&quot; alt=&quot;Figure 3&quot; width=&quot;160&quot; height=&quot;81&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;With that, I can easily remove the three direct references from my code and refactor to use my repository pattern implementation. It&#39;s one of many example that can clearly help you and your team to identify elements such as this one.&lt;/p&gt;
&lt;h3&gt;Metrics&lt;/h3&gt;
&lt;p&gt;The &quot;Metrics&quot; tab is again another great example of the tool usefulness; From there you can view &quot;heated&quot; areas of your code which has strong cyclomatic complexity. Here in my example, the following method is considered a bit more complex:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.davidhome.net/contentassets/35299a47b8d442dfb348d60119617eb6/screenshot202024-01-23201114511.png&quot;&gt;&lt;img title=&quot;Figure 4&quot; src=&quot;https://www.davidhome.net/contentassets/35299a47b8d442dfb348d60119617eb6/screenshot202024-01-23201114511.png&quot; alt=&quot;Figure 4&quot; width=&quot;160&quot; height=&quot;62&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Double clicking it opens the file in your preferred IDE and points your cursor directly to the method. If you&#39;re interesting to have different insights, you can also change the metric data type to something else:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.davidhome.net/contentassets/35299a47b8d442dfb348d60119617eb6/screenshot202024-01-23201107411.png&quot;&gt;&lt;img title=&quot;Figure 5&quot; src=&quot;https://www.davidhome.net/contentassets/35299a47b8d442dfb348d60119617eb6/screenshot202024-01-23201107411.png&quot; alt=&quot;Figure 5&quot; width=&quot;160&quot; height=&quot;205&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Closure&lt;/h3&gt;
&lt;p&gt;There is also a CI/CD integration that you can install. Obviously, the goal is to obtain those analysis directly from your build pipelines so that you can act preventively on any code quality depreciation. If you&#39;re interested, you can also view certain sample reports at the following URL:&amp;nbsp;&lt;a href=&quot;https://www.ndepend.com/sample-reports/&quot;&gt;https://www.ndepend.com/sample-reports/&lt;/a&gt;. For Optimizely solutions, CD/CI can come super handy because such projects can quickly become a burden maintainability wise.&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>            <guid>https://www.davidhome.net/blog/how-ndepend-can-quickly-help-you-find-code-quality-issues-and-resolve-them/</guid>            <pubDate>Fri, 26 Jan 2024 05:00:00 GMT</pubDate>           <category>Blog post</category></item><item> <title>How NDepend can quickly help you find code quality issues and resolve them</title>            <link>https://www.davidhome.net/blog/how-ndepend-can-quickly-help-you-find-code-quality-issues-and-resolve-them</link>            <description>&lt;p&gt;NDepend has been around &lt;a href=&quot;https://web.archive.org/web/20060518122151/https://www.ndepend.com/&quot;&gt;for quite a long time&lt;/a&gt;. If you are unaware of what it does, you should be checking it. It&#39;s an analysis tools which helps you find inconsistencies and discrepancies in your C# source code. I&#39;ve been trying it out recently and helped me found certain issue which I wasn&#39;t aware of.&lt;/p&gt;
&lt;p&gt;To be clear, there is indeed other available solution and it&#39;s up to your team to decide which fits best with you. In this case, NDepend runs independently from any IDE, but you can install an integration plugin inside Visual Studio. This can give the leverage/advantage to avoid loading any IDE to obtain a report. Unfortunately, there is no plugin/extension support under JetBrains Rider, my personal IDE of choice, like there is under Visual Studio. I hope their team can implement one, that would be greatly valuable.&lt;/p&gt;
&lt;h3&gt;Dashboard&lt;/h3&gt;
&lt;p&gt;Once a solution analysis is completed, you will be greeted with the dashboard tab:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/Screenshot%202024-01-22%20121642.png&quot;&gt;&lt;img src=&quot;/media/Screenshot%202024-01-22%20121642.png?width=160&quot; alt=&quot;Figure 1&quot; title=&quot;Figure 1&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Here lies a general overview of the quality of the whole solution that you have analyzed. This gives a great quick shot of the elements you could get your hand onto if your goal is to refactor the code to make it more maintainable. You can personalize the rules and change them to your likings. For example, in my previous screenshot, there is one critical rule that flags I&#39;m using a type within another type which have both different namespaces. &lt;a href=&quot;https://www.ndepend.com/default-rules/NDepend-Rules-Explorer.html?ruleid=ND1400#!&quot;&gt;The debt explanation is interesting&lt;/a&gt;. Of course, you can always decide to ignore it, but it gives an interesting point of view regarding the fact that some developers are using folders within a project to &quot;organize&quot; classes, which can lead to this kind of rule alert. Of course, everything is to take with a grain of salt, as I agree with someone from the comments of &lt;a href=&quot;https://stackoverflow.com/questions/59519084/how-to-avoid-namespaces-dependency-cycles-between-my-entities&quot;&gt;this Stack Overflow thread&lt;/a&gt;, these kind of alerts can be opinion based. An interesting thing that NDepend&#39;s team could be adding inside their analysis program is rule categorization; Opiniated ones versus the others by example. It could help to quickly address undiscussable items and leave any development team to discuss the other opiniated points and see whether they need to be addressed.&lt;/p&gt;
&lt;h3&gt;Dependency Graph&lt;/h3&gt;
&lt;p&gt;I think the most interesting feature of NDepend is the dependency graph. It&#39;s so incredibly useful to quickly identify areas of your code that aren&#39;t supposed to inherit another project within your solution. This can otherwise, to my opinion, often lead to solutions having classes/elements heavily coupled/entangled together. Based on the same project that I have used to create my first screenshot, here is an example of the graph:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/Screenshot%202024-01-23%20094608.png&quot;&gt;&lt;img src=&quot;/media/Screenshot%202024-01-23%20094608.png?width=160&quot; alt=&quot;Figure 2&quot; title=&quot;Figure 2&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;As you can realize in the previous screenshot, there is a design pattern here that I often follow in projects:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The website is the main &quot;referrer&quot; of projects, meaning that nothing should be referencing the website.
&lt;ul&gt;
&lt;li&gt;The exception are the unit tests. You can also realize the only additional reference of the test projects are the &quot;Models&quot; projects. Nothing else.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;The &quot;Bootstrappers&quot; projects serve only the purpose to add inside the IoC container the necessary services that are consumable by all &quot;Contracts&quot; projects.&lt;/li&gt;
&lt;li&gt;Each consumable &quot;Contracts&quot; projects comes in pair with a minimum of an additional &quot;concrete&quot; implementation project. These projects, as previously mentioned, only contain the implementation your contracts, which will then be consumed by the website.&lt;/li&gt;
&lt;li&gt;Almost everything consumes the &quot;Models&quot;. This is again, by design. Models are the basically &quot;the end of the road&quot;, it should generally refer nothing else.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;I will be doing another blog post about the design pattern I personally follow in projects.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;NDepend quickly identified a discrepancy in my project: There is a direct reference from the website to the database library and this is not normal. Double clicking on the arrow gives me the following:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/Screenshot%202024-01-23%20103424.png&quot;&gt;&lt;img src=&quot;/media/Screenshot%202024-01-23%20103424.png?width=160&quot; alt=&quot;Figure 3&quot; title=&quot;Figure 3&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;With that, I can easily remove the three direct references from my code and refactor to use my repository pattern implementation. It&#39;s one of many example that can clearly help you and your team to identify elements such as this one.&lt;/p&gt;
&lt;h3&gt;Metrics&lt;/h3&gt;
&lt;p&gt;The &quot;Metrics&quot; tab is again another great example of the tool usefulness; From there you can view &quot;heated&quot; areas of your code which has strong cyclomatic complexity.
Here in my example, the following method is considered a bit more complex:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/Screenshot%202024-01-23%20111451.png&quot;&gt;&lt;img src=&quot;/media/Screenshot%202024-01-23%20111451.png?width=160&quot; alt=&quot;Figure 4&quot; title=&quot;Figure 4&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Double clicking it opens the file in your preferred IDE and points your cursor directly to the method. If you&#39;re interesting to have different insights, you can also change the metric data type to something else:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/Screenshot%202024-01-23%20110741.png&quot;&gt;&lt;img src=&quot;/media/Screenshot%202024-01-23%20110741.png?width=160&quot; alt=&quot;Figure 5&quot; title=&quot;Figure 5&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Closure&lt;/h3&gt;
&lt;p&gt;There is also a CI/CD integration that you can install. Obviously, the goal is to obtain those analysis directly from your build pipelines so that you can act preventively on any code quality depreciation. Within Optimizely solutions, this can come super handy because such projects can quickly become a burden maintainability wise.&lt;/p&gt;
</description>            <guid>https://www.davidhome.net/blog/how-ndepend-can-quickly-help-you-find-code-quality-issues-and-resolve-them</guid>            <pubDate>Tue, 23 Jan 2024 16:34:37 GMT</pubDate>           <category>Blog post</category></item><item> <title>Optimizely Content Cloud CMS Apps (Add-ons) - Tips &amp; Tricks</title>            <link>https://www.davidhome.net/blog/optimizely-content-cloud-cms-apps-add-ons-tips-tricks/</link>            <description>&lt;div&gt;&lt;div&gt;
&lt;div&gt;&lt;p&gt;Developing an Optimizely CMS Add-on can be a bit tricky. There is a lot of advantages of doing it, but building it right can be a bit tedious. You have probably already seen the following&amp;nbsp;&lt;a href=&quot;https://docs.developers.optimizely.com/content-management-system/docs/developing-add-ons&quot;&gt;documentation page&lt;/a&gt;&amp;nbsp;about the subject itself, but it feels like a lot of details are missing to make it right. You will also quickly realize there is a lot of historical elements which makes the process a bit more complicated/confusing. In this blog, we will unravel everything so that you can successfully build yours!&lt;/p&gt;
&lt;p&gt;I will be assuming that your project is currently using .NET 6.0, but the process is the same if the target framework changes. You can also add conditional dependencies based on the target framework, which then the command&amp;nbsp;&lt;code&gt;dotnet pack&lt;/code&gt;&amp;nbsp;will automatically handle when bundling your library. I will also assume that you are already mastering how packing your library to .nupkg file(s) works.&lt;/p&gt;
&lt;h2&gt;1. Adjustements inside the project file&lt;/h2&gt;
&lt;p&gt;First of all, create a new solution with a library project using your preferred IDE, then edit the .csproj file. Your file should look like the following in the end:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml hljs&quot; data-highlighted=&quot;yes&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Project&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;Sdk&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;Microsoft.NET.Sdk.Razor&quot;&lt;/span&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;PropertyGroup&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;TargetFramework&lt;/span&gt;&amp;gt;&lt;/span&gt;net6.0&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;TargetFramework&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Nullable&lt;/span&gt;&amp;gt;&lt;/span&gt;enable&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;Nullable&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;ImplicitUsings&lt;/span&gt;&amp;gt;&lt;/span&gt;enable&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;ImplicitUsings&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;AddRazorSupportForMvc&lt;/span&gt;&amp;gt;&lt;/span&gt;true&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;AddRazorSupportForMvc&lt;/span&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;PropertyGroup&lt;/span&gt;&amp;gt;&lt;/span&gt;

  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;ItemGroup&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;FrameworkReference&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;Include&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;Microsoft.AspNetCore.App&quot;&lt;/span&gt; /&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;ItemGroup&lt;/span&gt;&amp;gt;&lt;/span&gt;
  
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;ItemGroup&lt;/span&gt;&amp;gt;&lt;/span&gt;
		&lt;span class=&quot;hljs-comment&quot;&gt;&amp;lt;!--    These are package example. Install those you need. --&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;PackageReference&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;Include&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;EPiServer.CMS.AspNetCore.Mvc&quot;&lt;/span&gt; /&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;PackageReference&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;Include&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;EPiServer.CMS.Core&quot;&lt;/span&gt; /&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;PackageReference&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;Include&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;EPiServer.CMS.UI.Core&quot;&lt;/span&gt; /&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;ItemGroup&lt;/span&gt;&amp;gt;&lt;/span&gt;

  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;ItemGroup&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Content&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;Remove&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;module.config&quot;&lt;/span&gt; /&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;None&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;Include&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;module.config&quot;&lt;/span&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;CopyToOutputDirectory&lt;/span&gt;&amp;gt;&lt;/span&gt;Never&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;CopyToOutputDirectory&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;None&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Content&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;Remove&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;packages.lock.json&quot;&lt;/span&gt; /&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;None&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;Include&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;packages.lock.json&quot;&lt;/span&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;CopyToOutputDirectory&lt;/span&gt;&amp;gt;&lt;/span&gt;Never&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;CopyToOutputDirectory&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;None&lt;/span&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;ItemGroup&lt;/span&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;Project&lt;/span&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you can also realize, I&#39;m using&amp;nbsp;&lt;a href=&quot;https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management&quot;&gt;CPM&lt;/a&gt;. This will greatly simplify the dependency management along the way.&lt;/p&gt;
&lt;p&gt;Couple of things to highlight here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Super important to change the SDK attribute on the &quot;Project&quot; node to &quot;Microsoft.NET.Sdk.Razor&quot;.&lt;/li&gt;
&lt;li&gt;Inside the first PropertyGroup node, add AddRazorSupportForMvc and set it &quot;true&quot;.&lt;/li&gt;
&lt;li&gt;Make sure to add a FrameworkReference node which will be including the Microsoft.AspNetCore.App reference. Necessary for having the ASP.NET Core web references, which normally the SDK Microsoft.NET.Sdk.Web includes by default.&lt;/li&gt;
&lt;li&gt;The last, but not least, make sure to remove &amp;amp; ignore any files you want to exclude from your .nupkg file. In our example, and you will probably need it too, we want to prevent both &quot;module.config&quot; and &quot;packages.lock.json&quot; file to be inside the file package.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2. Custom MSBuild instructions&lt;/h2&gt;
&lt;p&gt;There is a couple of adjustments to make so that MSBuild is helping up ease the bundling:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Assuming your projects are hierarchically structured in the following directory pattern: [root]/src/projectname/projectname.csproj, create a Directory.Build.props file in the &quot;src&quot; folder with the following content:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml hljs&quot; data-highlighted=&quot;yes&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Project&lt;/span&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;PropertyGroup&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;NoWarn&lt;/span&gt;&amp;gt;&lt;/span&gt;NU1507&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;NoWarn&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;RestorePackagesWithLockFile&lt;/span&gt;&amp;gt;&lt;/span&gt;True&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;RestorePackagesWithLockFile&lt;/span&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;PropertyGroup&lt;/span&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;Project&lt;/span&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;Assuming the same structure, create under the &quot;src&quot; folder the file Directory.Packages.props with the following content:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml hljs&quot; data-highlighted=&quot;yes&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Project&lt;/span&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;PropertyGroup&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;ManagePackageVersionsCentrally&lt;/span&gt;&amp;gt;&lt;/span&gt;true&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;ManagePackageVersionsCentrally&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;CentralPackageTransitivePinningEnabled&lt;/span&gt;&amp;gt;&lt;/span&gt;true&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;CentralPackageTransitivePinningEnabled&lt;/span&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;PropertyGroup&lt;/span&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;ItemGroup&lt;/span&gt;&amp;gt;&lt;/span&gt;
		&lt;span class=&quot;hljs-comment&quot;&gt;&amp;lt;!--    These are package example. Install those you need. --&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;PackageVersion&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;Include&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;EPiServer.CMS.AspNetCore.Mvc&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;Version&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;[12.4.0, 13.0.0)&quot;&lt;/span&gt; /&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;PackageVersion&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;Include&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;EPiServer.CMS.Core&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;Version&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;[12.4.0, 13.0.0)&quot;&lt;/span&gt; /&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;PackageVersion&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;Include&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;EPiServer.CMS.UI.Core&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;Version&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;[12.4.0, 13.0.0)&quot;&lt;/span&gt; /&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;ItemGroup&lt;/span&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;Project&lt;/span&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;Inside the project folder, where the .csproj file resides, create a new file entitled Directory.Build.props with the following content:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml hljs&quot; data-highlighted=&quot;yes&quot;&gt;&lt;span class=&quot;hljs-meta&quot;&gt;&amp;lt;?xml version=&lt;span class=&quot;hljs-string&quot;&gt;&quot;1.0&quot;&lt;/span&gt; encoding=&lt;span class=&quot;hljs-string&quot;&gt;&quot;utf-8&quot;&lt;/span&gt; ?&amp;gt;&lt;/span&gt;
&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Project&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;xmlns&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;http://schemas.microsoft.com/developer/msbuild/2003&quot;&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;ItemGroup&lt;/span&gt;&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;ClientResources&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;Include&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;$(ProjectDir)ClientResources\**\*&quot;&lt;/span&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;ItemGroup&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;PropertyGroup&lt;/span&gt;&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;TmpOutDir&lt;/span&gt;&amp;gt;&lt;/span&gt;$([System.IO.Path]::Combine($(ProjectDir), &#39;tmp&#39;))&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;TmpOutDir&lt;/span&gt;&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;NoWarn&lt;/span&gt;&amp;gt;&lt;/span&gt;NU1507&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;NoWarn&lt;/span&gt;&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;RestorePackagesWithLockFile&lt;/span&gt;&amp;gt;&lt;/span&gt;True&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;RestorePackagesWithLockFile&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;PropertyGroup&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;ItemGroup&lt;/span&gt;&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Content&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;Include&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;$(MSBuildProjectName).zip&quot;&lt;/span&gt;&amp;gt;&lt;/span&gt;
            &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Pack&lt;/span&gt;&amp;gt;&lt;/span&gt;true&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;Pack&lt;/span&gt;&amp;gt;&lt;/span&gt;
            &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;PackagePath&lt;/span&gt;&amp;gt;&lt;/span&gt;contentFiles\any\any\modules\_protected\$(MSBuildProjectName)&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;PackagePath&lt;/span&gt;&amp;gt;&lt;/span&gt;
            &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;BuildAction&lt;/span&gt;&amp;gt;&lt;/span&gt;None&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;BuildAction&lt;/span&gt;&amp;gt;&lt;/span&gt;
            &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;PackageCopyToOutput&lt;/span&gt;&amp;gt;&lt;/span&gt;true&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;PackageCopyToOutput&lt;/span&gt;&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;Content&lt;/span&gt;&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Content&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;Include&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;msbuild\CopyZipFiles.targets&quot;&lt;/span&gt; &amp;gt;&lt;/span&gt;
            &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Pack&lt;/span&gt;&amp;gt;&lt;/span&gt;true&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;Pack&lt;/span&gt;&amp;gt;&lt;/span&gt;
            &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;PackagePath&lt;/span&gt;&amp;gt;&lt;/span&gt;build\$(MSBuildProjectName).targets&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;PackagePath&lt;/span&gt;&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;Content&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;ItemGroup&lt;/span&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;Project&lt;/span&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;Still in the project folder, create the file Directory.Build.targets and add the following content:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml hljs&quot; data-highlighted=&quot;yes&quot;&gt;&lt;span class=&quot;hljs-meta&quot;&gt;&amp;lt;?xml version=&lt;span class=&quot;hljs-string&quot;&gt;&quot;1.0&quot;&lt;/span&gt; encoding=&lt;span class=&quot;hljs-string&quot;&gt;&quot;utf-8&quot;&lt;/span&gt; ?&amp;gt;&lt;/span&gt;
&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Project&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;xmlns&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;http://schemas.microsoft.com/developer/msbuild/2003&quot;&lt;/span&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Target&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;Name&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;CreateCmsAddOnZip&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;BeforeTargets&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;Build&quot;&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Copy&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;SourceFiles&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;$(ProjectDir)module.config&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;DestinationFolder&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;$(TmpOutDir)\content&quot;&lt;/span&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Copy&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;SourceFiles&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;@(ClientResources)&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;DestinationFiles&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;@(ClientResources -&amp;gt; &#39;$(TmpOutDir)\content\$(PackageVersion)\ClientResources\%(RecursiveDir)%(Filename)%(Extension)&#39;)&quot;&lt;/span&gt;/&amp;gt;&lt;/span&gt;

    &lt;span class=&quot;hljs-comment&quot;&gt;&amp;lt;!-- Update the module config with the version information --&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;XmlPoke&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;XmlInputPath&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;$(TmpOutDir)\content\module.config&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;Query&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;/module/@clientResourceRelativePath&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;Value&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;$(PackageVersion)&quot;&lt;/span&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;Target&lt;/span&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Target&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;Name&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;ZipClientResources&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;BeforeTargets&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;Build&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;AfterTargets&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;CreateCmsAddOnZip&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;DependsOnTargets&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;CreateCmsAddOnZip&quot;&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;ZipDirectory&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;SourceDirectory&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;$(TmpOutDir)\content&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;DestinationFile&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;$(ProjectDir)$(MSBuildProjectName).zip&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;Overwrite&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;true&quot;&lt;/span&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;Target&lt;/span&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Target&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;Name&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;CleanupTmpOutDir&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;BeforeTargets&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;Build&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;AfterTargets&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;ZipClientResources&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;DependsOnTargets&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;ZipClientResources&quot;&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;RemoveDir&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;Directories&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;$(TmpOutDir)&quot;&lt;/span&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;Target&lt;/span&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;Project&lt;/span&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;5&quot;&gt;
&lt;li&gt;You will also have to add the file CopyZipFiles.targets to a new &quot;msbuild&quot; folder under the root of the project directory:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml hljs&quot; data-highlighted=&quot;yes&quot;&gt;&lt;span class=&quot;hljs-meta&quot;&gt;&amp;lt;?xml version=&lt;span class=&quot;hljs-string&quot;&gt;&quot;1.0&quot;&lt;/span&gt; encoding=&lt;span class=&quot;hljs-string&quot;&gt;&quot;utf-8&quot;&lt;/span&gt;?&amp;gt;&lt;/span&gt;
&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Project&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;xmlns&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;http://schemas.microsoft.com/developer/msbuild/2003&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;ToolsVersion&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;4.0&quot;&lt;/span&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;ItemGroup&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;CmsAddOnZips&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;Include&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;$(MSBuildThisFileDirectory)..\contentFiles\any\any\modules\_protected\**\*.zip&quot;&lt;/span&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;ItemGroup&lt;/span&gt;&amp;gt;&lt;/span&gt;

  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Target&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;Name&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;CopyCmsAddOnZip&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;BeforeTargets&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;Build&quot;&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Copy&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;SourceFiles&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;@(CmsAddOnZips)&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;DestinationFolder&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;$(MSBuildProjectDirectory)\modules\_protected\%(RecursiveDir)&quot;&lt;/span&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;Target&lt;/span&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;Project&lt;/span&gt;&amp;gt;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To summarize, these customizations will do the following to your project each time you will be compiling:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Adds CPM, as previously mentioned.&lt;/li&gt;
&lt;li&gt;Adds NuGet lock files, super useful for your DevOps pipeline.&lt;/li&gt;
&lt;li&gt;Automatically compile views.&lt;/li&gt;
&lt;li&gt;Automatically generates module the zip file with all necessary elements in it that will be packaged within your .nupkg file.
&lt;ul&gt;
&lt;li&gt;This file contains the recommended structure &amp;amp; content that you can find in the&amp;nbsp;&lt;a href=&quot;https://docs.developers.optimizely.com/content-management-system/docs/developing-add-ons&quot;&gt;Optimizely CMS addon documentation&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;3. Minimum required configuration&lt;/h2&gt;
&lt;p&gt;See the &quot;module.config&quot; file like the definition of your addon. Without it, Optimizely will use the default values from the class ShellModuleManifest, which unfortunately omit a very important detail; The assembly&amp;rsquo;s name of your addon. Without it, Optimizely won&#39;t be able to load yours at boot. Create it where the .csproj file resides with the following content:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml hljs&quot; data-highlighted=&quot;yes&quot;&gt;&lt;span class=&quot;hljs-meta&quot;&gt;&amp;lt;?xml version=&lt;span class=&quot;hljs-string&quot;&gt;&quot;1.0&quot;&lt;/span&gt; encoding=&lt;span class=&quot;hljs-string&quot;&gt;&quot;utf-8&quot;&lt;/span&gt;?&amp;gt;&lt;/span&gt;
&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;loadFromBin&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;false&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;name&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;Your.Assembly.Name&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;viewEngine&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;Razor&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;clientResourceRelativePath&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;$version$&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;tags&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;EPiServerModulePackage&quot;&lt;/span&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;assemblies&lt;/span&gt;&amp;gt;&lt;/span&gt;
		&lt;span class=&quot;hljs-comment&quot;&gt;&amp;lt;!-- Change the assembly name with yours --&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;add&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;assembly&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;Your.Assembly.Name&quot;&lt;/span&gt; /&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;assemblies&lt;/span&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;clientModule&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;moduleDependencies&lt;/span&gt;&amp;gt;&lt;/span&gt;
			&lt;span class=&quot;hljs-comment&quot;&gt;&amp;lt;!-- Adjust accordingly --&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;add&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;dependency&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;CMS&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;type&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;RunAfter&quot;&lt;/span&gt; /&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;moduleDependencies&lt;/span&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;clientModule&lt;/span&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;module&lt;/span&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add an extension method on the interface &quot;IServiceCollection&quot; and add at least the following piece of code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cs hljs language-csharp&quot; data-highlighted=&quot;yes&quot;&gt;&lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;static&lt;/span&gt; IServiceCollection &lt;span class=&quot;hljs-title&quot;&gt;AddMyAddon&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;this&lt;/span&gt; IServiceCollection services&lt;/span&gt;)&lt;/span&gt;
    {
        &lt;span class=&quot;hljs-comment&quot;&gt;// Add services here.&lt;/span&gt;
        &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; services
                &lt;span class=&quot;hljs-comment&quot;&gt;// Super required, otherwise your addon won&#39;t load when the site loads.&lt;/span&gt;
            .Configure&amp;lt;ProtectedModuleOptions&amp;gt;(
            pm =&amp;gt;
            {
                &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt; (!pm.Items.Any(i =&amp;gt; i.Name.Equals(ModuleName, StringComparison.OrdinalIgnoreCase)))
                {
                    pm.Items.Add(&lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; ModuleDetails { Name = ModuleName });
                }
            });
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. Controllers &amp;amp; Views&lt;/h2&gt;
&lt;p&gt;Probably the most undocumented/unclear part for Optimizely CMS addons. The instructions around views doesn&#39;t exactly explain how they can be used or how the routing works within your library. If you&#39;re looking at existing addons, e.g.,&amp;nbsp;&lt;a href=&quot;https://github.com/Geta/geta-notfoundhandler&quot;&gt;Geta.NotFoundHandler&lt;/a&gt;, the majority of developers are handling it slightly differently.&amp;nbsp;&lt;a href=&quot;https://docs.developers.optimizely.com/content-management-system/docs/migrating-add-ons-to-net-5&quot;&gt;This page&lt;/a&gt;&amp;nbsp;explains how to structure your files in a manner that the module will automatically include them for you, but I was unable to make it work. Maybe because it should be exclusively a&amp;nbsp;&lt;a href=&quot;https://learn.microsoft.com/en-us/aspnet/core/razor-pages/?view=aspnetcore-8.0&amp;amp;tabs=visual-studio&quot;&gt;razor page&lt;/a&gt;&amp;nbsp;and not a simple razor view dependent on a Controller. Fortunately, you can make it work with a controller, but with a little additional tweaking:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create a new&amp;nbsp;&lt;a href=&quot;https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/routing?view=aspnetcore-8.0#custom-route-attributes-using-iroutetemplateprovider&quot;&gt;Route Attribute&lt;/a&gt;. We will use it to customize the routes to our controllers. Here&#39;s an example:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-cs hljs language-csharp&quot; data-highlighted=&quot;yes&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;using&lt;/span&gt; EPiServer.Shell;
&lt;span class=&quot;hljs-keyword&quot;&gt;using&lt;/span&gt; Microsoft.AspNetCore.Mvc.Routing;

&lt;span class=&quot;hljs-keyword&quot;&gt;namespace&lt;/span&gt; &lt;span class=&quot;hljs-title&quot;&gt;Playground.Mvc&lt;/span&gt;;

[&lt;span class=&quot;hljs-meta&quot;&gt;AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)&lt;/span&gt;]
&lt;span class=&quot;hljs-keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;hljs-title&quot;&gt;ModuleRoute&lt;/span&gt; : &lt;span class=&quot;hljs-title&quot;&gt;Attribute&lt;/span&gt;, &lt;span class=&quot;hljs-title&quot;&gt;IRouteTemplateProvider&lt;/span&gt;
{
    &lt;span class=&quot;hljs-keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;readonly&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;string&lt;/span&gt; _controllerName;
    &lt;span class=&quot;hljs-keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;readonly&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;string&lt;/span&gt; _actionName;

    &lt;span class=&quot;hljs-keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;string&lt;/span&gt; Template =&amp;gt; Paths.ToResource(&lt;span class=&quot;hljs-keyword&quot;&gt;typeof&lt;/span&gt;(ModuleRoute), &lt;span class=&quot;hljs-string&quot;&gt;$&quot;&lt;span class=&quot;hljs-subst&quot;&gt;{_controllerName}&lt;/span&gt;/&lt;span class=&quot;hljs-subst&quot;&gt;{_actionName}&lt;/span&gt;&quot;&lt;/span&gt;);
    &lt;span class=&quot;hljs-keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;int&lt;/span&gt;? Order { &lt;span class=&quot;hljs-keyword&quot;&gt;get&lt;/span&gt;; &lt;span class=&quot;hljs-keyword&quot;&gt;set&lt;/span&gt;; } = &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;;
    &lt;span class=&quot;hljs-keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;string&lt;/span&gt; Name { &lt;span class=&quot;hljs-keyword&quot;&gt;get&lt;/span&gt;; &lt;span class=&quot;hljs-keyword&quot;&gt;set&lt;/span&gt;; }

    &lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;hljs-title&quot;&gt;ModuleRoute&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;span class=&quot;hljs-built_in&quot;&gt;string&lt;/span&gt; controllerName, &lt;span class=&quot;hljs-built_in&quot;&gt;string&lt;/span&gt; actionName&lt;/span&gt;)&lt;/span&gt;
    {
        _controllerName = controllerName;
        _actionName = actionName;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;Decorate your controller actions with your newly created route attribute. e.g.:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-cs hljs language-csharp&quot; data-highlighted=&quot;yes&quot;&gt;    [&lt;span class=&quot;hljs-meta&quot;&gt;HttpGet&lt;/span&gt;]
    [&lt;span class=&quot;hljs-meta&quot;&gt;ModuleRoute(&lt;span class=&quot;hljs-string&quot;&gt;&quot;Default&quot;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&quot;Index&quot;&lt;/span&gt;)&lt;/span&gt;]
    &lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;public&lt;/span&gt; IActionResult &lt;span class=&quot;hljs-title&quot;&gt;Index&lt;/span&gt;()&lt;/span&gt;
    {
        &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; View();
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;Create a new menu provider class including all paths to your actions in your addons. e.g.:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-cs hljs language-csharp&quot; data-highlighted=&quot;yes&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;using&lt;/span&gt; EPiServer.Framework.Localization;
&lt;span class=&quot;hljs-keyword&quot;&gt;using&lt;/span&gt; EPiServer.Shell;
&lt;span class=&quot;hljs-keyword&quot;&gt;using&lt;/span&gt; EPiServer.Shell.Navigation;
&lt;span class=&quot;hljs-keyword&quot;&gt;using&lt;/span&gt; Playground.Controllers;

&lt;span class=&quot;hljs-keyword&quot;&gt;namespace&lt;/span&gt; &lt;span class=&quot;hljs-title&quot;&gt;Playground.Optimizely&lt;/span&gt;;

[&lt;span class=&quot;hljs-meta&quot;&gt;MenuProvider&lt;/span&gt;]
&lt;span class=&quot;hljs-keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;hljs-title&quot;&gt;AddonMenuProvider&lt;/span&gt; : &lt;span class=&quot;hljs-title&quot;&gt;IMenuProvider&lt;/span&gt;
{
    &lt;span class=&quot;hljs-keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;readonly&lt;/span&gt; LocalizationService _localizationService;

    &lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;hljs-title&quot;&gt;AddonMenuProvider&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;LocalizationService localizationService&lt;/span&gt;)&lt;/span&gt;
    {
        _localizationService = localizationService;
    }

    &lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;public&lt;/span&gt; IEnumerable&amp;lt;MenuItem&amp;gt; &lt;span class=&quot;hljs-title&quot;&gt;GetMenuItems&lt;/span&gt;()&lt;/span&gt;
    {
        &lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;yield&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title&quot;&gt;UrlMenuItem&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;_localizationService.GetString(&lt;span class=&quot;hljs-string&quot;&gt;&quot;/myaddon/gadget/title&quot;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&quot;My Addon&quot;&lt;/span&gt;&lt;/span&gt;), &quot;/&lt;span class=&quot;hljs-keyword&quot;&gt;global&lt;/span&gt;/cms/myaddon&quot;,
            Paths.&lt;span class=&quot;hljs-title&quot;&gt;ToResource&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;GetType(&lt;/span&gt;), $&quot;Default/&lt;/span&gt;{&lt;span class=&quot;hljs-keyword&quot;&gt;nameof&lt;/span&gt;(DefaultController.Index)}&lt;span class=&quot;hljs-string&quot;&gt;&quot;))
        {
            SortIndex = 0,
            Alignment = 0,
            IsAvailable = _ =&amp;gt; true
        };

        yield return new UrlMenuItem(_localizationService.GetString(&quot;&lt;/span&gt;/myaddon/index/menu&lt;span class=&quot;hljs-string&quot;&gt;&quot;, &quot;&lt;/span&gt;Home&lt;span class=&quot;hljs-string&quot;&gt;&quot;), &quot;&lt;/span&gt;/&lt;span class=&quot;hljs-keyword&quot;&gt;global&lt;/span&gt;/cms/myaddon/index&lt;span class=&quot;hljs-string&quot;&gt;&quot;,
            Paths.ToResource(GetType(), $&quot;&lt;/span&gt;Default/{&lt;span class=&quot;hljs-keyword&quot;&gt;nameof&lt;/span&gt;(DefaultController.Index)}&lt;span class=&quot;hljs-string&quot;&gt;&quot;))
        {
            SortIndex = 10,
            Alignment = 0,
            IsAvailable = _ =&amp;gt; true
        };

        yield return new UrlMenuItem(_localizationService.GetString(&quot;&lt;/span&gt;/myaddon/secondaction/menu&lt;span class=&quot;hljs-string&quot;&gt;&quot;, &quot;&lt;/span&gt;Second Action&lt;span class=&quot;hljs-string&quot;&gt;&quot;), &quot;&lt;/span&gt;/&lt;span class=&quot;hljs-keyword&quot;&gt;global&lt;/span&gt;/cms/myaddon/secondaction&lt;span class=&quot;hljs-string&quot;&gt;&quot;,
            Paths.ToResource(GetType(), $&quot;&lt;/span&gt;Default/{&lt;span class=&quot;hljs-keyword&quot;&gt;nameof&lt;/span&gt;(DefaultController.SecondAction)}&lt;span class=&quot;hljs-string&quot;&gt;&quot;))
        {
            SortIndex = 20,
            Alignment = 0,
            IsAvailable = _ =&amp;gt; true
        };
    }
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By doing so, you are generating routes which will point directly on your addon controller. Say by example you have the &quot;DefaultController&quot; class with the &quot;Index&quot; action, well then, the action under your browser should look as the following: ~/EPiServer/MyAddon/Default/Index. This also convenientely allow you to use razor helpers such as Html.BeginForm, since the MVC routing system knows these belongs to your addon with a custom template.&lt;/p&gt;
&lt;p&gt;It is very important to respect the actions defined within your IMenuProvider implementation, otherwise the custom routing attribute won&#39;t be working just right. As you can see, a certain minimum level of structure has been established in this file, which makes actions from the controller to work correctly. The third parameter of the constructor of UrlMenuItem is exactly where the magic happens. The recommendation is indeed to keep using the helper, Paths.ToResource and add the additional segment manually. This consequently creates the same path as previously described in the last paragraph and will match it with the available controller action. A menu item is essentially an element that will appear under the Backoffice, when navigating under ~/EPiServer.&lt;/p&gt;
&lt;p&gt;I hope this will be super helpful to people reading this blog! You can view a starter code example over there:&amp;nbsp;&lt;a href=&quot;https://github.com/ddprince17/Optimizely-CMS-Addon-Playground&quot;&gt;https://github.com/ddprince17/Optimizely-CMS-Addon-Playground&lt;/a&gt;.&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>            <guid>https://www.davidhome.net/blog/optimizely-content-cloud-cms-apps-add-ons-tips-tricks/</guid>            <pubDate>Mon, 22 Jan 2024 05:00:00 GMT</pubDate>           <category>Blog post</category></item></channel>
</rss>