<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom"><title type="text">Blog posts by Mark Stott</title><link href="http://world.optimizely.com" /><updated>2025-04-09T08:23:00.0000000Z</updated><id>https://world.optimizely.com/blogs/mark-stott/</id> <generator uri="http://world.optimizely.com" version="2.0">Optimizely World</generator> <entry><title>Announcing Stott Security Version 3.0</title><link href="https://world.optimizely.com/blogs/mark-stott/dates/2025/4/announcing-stott-security-version-3.0" /><id>&lt;p&gt;I&#39;m proud to announce the release of version 3 of Stott Security for Optimizely PAAS CMS. This release has been developed over several months owing to a significant new feature among other quality of life changes.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;To see the full release notes you can head over to the discussion page on &lt;a title=&quot;Github - Stott Security v3.0.0&quot; href=&quot;https://github.com/GeekInTheNorth/Stott.Security.Optimizely/discussions/278&quot;&gt;GitHub.&lt;/a&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;h2&gt;Permissions-Policy Support&lt;/h2&gt;
&lt;p&gt;The &lt;strong&gt;Permissions-Policy&lt;/strong&gt; HTTP header (formerly &lt;strong&gt;Feature-Policy&lt;/strong&gt;) allows developers to control which web features and APIs can be used in the browser, and by which origin. This header can improve security and performance by restricting access to sensitive capabilities such as the camera, microphone, and geolocation.&lt;/p&gt;
&lt;p&gt;Currently, support for the &lt;strong&gt;Permissions-Policy&lt;/strong&gt; header is mixed. It is well-supported by Chromium-based browsers like Chrome and Edge, but not yet implemented in Firefox or Safari. This naturally raises the question: why invest time in implementing a feature that isn&amp;rsquo;t universally supported?&lt;/p&gt;
&lt;p&gt;According to the latest &lt;a title=&quot;browser market share data&quot; href=&quot;https://gs.statcounter.com/browser-market-share&quot;&gt;browser market share data&lt;/a&gt;, over 70% of users are on browsers that support this header. Additionally, unsupported browsers simply ignore the header, without causing any issues. While I&#39;ve yet to see a penetration test flag the absence of this header as a vulnerability, I&amp;rsquo;m increasingly seeing clients request its inclusion as part of their CMS security requirements.&lt;/p&gt;
&lt;p&gt;To support this, I&amp;rsquo;ve added a new &lt;strong&gt;Permissions Policy&lt;/strong&gt; screen to Stott Security. This interface allows administrators to enable or disable the &lt;strong&gt;Permissions-Policy&lt;/strong&gt; header globally with a single toggle. It also includes a filter bar for quickly narrowing directives by source (URL) or current configuration (e.g., *Disabled*, *Allow None*, *Allow All Sites*, etc.).&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/83b55fc2cfae462eb112c8cdcaaf50e2.aspx?1744186114798&quot; alt=&quot;The Permission Policy listing for Stott Security&quot; width=&quot;3420&quot; height=&quot;1276&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Clicking the &lt;strong&gt;Edit&lt;/strong&gt; button for a directive opens a modal where the configuration for that specific directive can be adjusted.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/3fc3a110a51d401bad8d7fc98a2734b6.aspx?1744186158433&quot; alt=&quot;The Permission Policy modal for a single directive within Stott Security&quot; width=&quot;2280&quot; height=&quot;842&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Available options include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Disabled (Omitted this directive from the policy)&lt;/li&gt;
&lt;li&gt;Allow None&lt;/li&gt;
&lt;li&gt;Allow All Sites&lt;/li&gt;
&lt;li&gt;Allow Just This Website&lt;/li&gt;
&lt;li&gt;Allow This Website and Specific Third Party Websites&lt;/li&gt;
&lt;li&gt;Allow Specific Third Party Websites&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For configurations involving third-party origins, administrators can specify one or more sources. Input is strictly validated to require only protocol and domain. Wildcards are supported, but only as the first segment after the protocol (e.g. &lt;em&gt;https://*.example.com&lt;/em&gt;).&lt;/p&gt;
&lt;p&gt;All changes to the Permissions Policy are fully audited. Additionally, import/export functionality has been extended to include this new feature.&lt;/p&gt;
&lt;h2&gt;Small Features&lt;/h2&gt;
&lt;h3&gt;.NET 9 Support&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;If you&#39;re building an Optimizely PaaS solution targeting &lt;strong&gt;.NET 9&lt;/strong&gt;, you can now integrate Stott Security into your project, regardless of whether your solution is headed, headless, or hybrid.&lt;/div&gt;
&lt;br /&gt;
&lt;div&gt;Stott Security leverages Entity Framework for database access and operations such as migrations. Since each major release of Entity Framework often introduces breaking changes aligned with specific .NET versions, the Stott Security package has been updated to include build targets for &lt;strong&gt;.NET 6&lt;/strong&gt;, &lt;strong&gt;.NET 8&lt;/strong&gt;, and &lt;strong&gt;.NET 9&lt;/strong&gt;, each using the appropriate version of Entity Framework under the hood.&lt;/div&gt;
&lt;h3&gt;Import Settings Tool&lt;/h3&gt;
&lt;p&gt;In previous versions, the import tool required that CSP, CORS, and Response Headers configurations all be present in the import file. To support backwards compatibility with export files created in version 2.x, validation in version 3.x has been relaxed. Now, settings are only applied if they contain a non-null value. This allows for partial imports; if your import file includes only the CSP configuration, then only the CSP settings will be updated, leaving all other settings unchanged.&lt;/p&gt;
&lt;h3&gt;X-XSS-Protection Header Warning&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;The &lt;strong&gt;X-XSS-Protection&lt;/strong&gt; header was originally introduced to instruct browsers to enable their built-in XSS filters, aiming to protect users from cross-site scripting attacks. However, the feature introduced its own set of issues. When configured with &lt;code&gt;X-XSS-Protection: 1; mode=block&lt;/code&gt;, malicious actors could exploit it to trigger denial-of-service conditions by injecting scripts that caused legitimate content to be blocked. Alternatively, when simply enabled without blocking (&lt;code&gt;X-XSS-Protection: 1&lt;/code&gt;), the header became susceptible to &lt;strong&gt;XS-Search&lt;/strong&gt; attacks this is where an attacker submits crafted XSS payloads to probe for differences in application behavior, potentially exposing sensitive data in applications that would otherwise be considered secure.&lt;/div&gt;
&lt;br /&gt;
&lt;div&gt;Although Stott Security continues to support the &lt;strong&gt;X-XSS-Protection&lt;/strong&gt; header, it should &lt;strong&gt;only&lt;/strong&gt; be set to &lt;strong&gt;disabled&lt;/strong&gt; or &lt;strong&gt;omitted entirely&lt;/strong&gt;. To help guide users, explanatory notes have been added to the UI outlining current best practices.&lt;/div&gt;
&lt;br /&gt;
&lt;div&gt;It&amp;rsquo;s worth noting that Chromium-based browsers already ignore this header entirely, but some other browsers have yet to follow suit.&lt;/div&gt;
&lt;h3&gt;Content Security Policy Source Updates&lt;/h3&gt;
&lt;p&gt;Validation has been enhanced to support the &lt;strong&gt;&#39;inline-speculation-rules&#39;&lt;/strong&gt;&amp;nbsp;keyword within Content Security Policy (CSP) directives. Speculation rules enable the browser to preload potential navigation targets based on user behavior, improving perceived performance by initiating page loads slightly before user interaction (e.g., clicks).&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;CSP Edit Source&lt;/strong&gt; modal in Stott Security has also been updated to ensure that special keywords such as &lt;strong&gt;&#39;unsafe-inline&#39;&lt;/strong&gt;, &lt;strong&gt;&#39;unsafe-eval&#39;&lt;/strong&gt;, and &lt;strong&gt;&#39;inline-speculation-rules&#39;&lt;/strong&gt; can only be added to directives where they are valid. These are typically limited to &lt;strong&gt;script-src&lt;/strong&gt;, &lt;strong&gt;style-src&lt;/strong&gt;, and their more specific variants.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/743f539570f24afdaac612ad801523b0.aspx?1744186454614&quot; alt=&quot;Content Security Policy Edit Source modal for Stott Security&quot; width=&quot;2280&quot; height=&quot;850&quot; /&gt;&lt;/p&gt;
&lt;div&gt;
&lt;h3&gt;CMS Editor Gadget Removed&lt;/h3&gt;
&lt;p&gt;In version 2, I introduced a CMS Editor gadget that displayed the HTTP headers generated when rendering a specific page. This was intended to support the feature allowing users to extend the Content Security Policy (CSP) for individual content items by specifying additional sources.&lt;/p&gt;
&lt;p&gt;However, over time it became clear that this approach had several drawbacks:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The gadget was read-only and appeared automatically for all CMS administrators, even if the page-specific CSP feature wasn&#39;t in use. In most cases, this meant the gadget was visible but offered little to no value.&lt;/li&gt;
&lt;li&gt;Some developers experienced installation issues with the AddOn. Specifically, in projects where the &lt;code&gt;.csproj&lt;/code&gt; excluded the &lt;code&gt;modules\_protected&lt;/code&gt; folder; often done to avoid conflicts with other third-party AddOns that add files to the solution multiple times. This resulted in the Stott Security &lt;strong&gt;module.config&lt;/strong&gt; being be removed and not re-added. This in turn causes an error during the start up of the solution that could only be resovled by manually adding the into source control and project.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Given the minimal benefit provided by the gadget and the friction it caused in certain build pipelines, I&amp;rsquo;ve decided to remove it entirely in version 3.&lt;/p&gt;
&lt;div&gt;
&lt;h3&gt;Obsolete Code Removed&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;If your solution has been using Stott Security since version 1 then you may find that you are using some obsoleted code. &amp;nbsp;As part of packaging up version 3, the following items have been removed:&lt;/div&gt;
&lt;br /&gt;
&lt;div&gt;
&lt;ul&gt;
&lt;li&gt;SecurityServiceExtensions.AddCspManager(...)
&lt;ul&gt;
&lt;li&gt;Replaced by SecurityServiceExtensions.AddStottSecurity(...)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;SecurityServiceExtensions.UseCspManager()
&lt;ul&gt;
&lt;li&gt;Replaced by SecurityServiceExtensions.UseStottSecurity()&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;CspReportingViewComponent
&lt;ul&gt;
&lt;li&gt;Replaced by Report-Uri and Report-To within the CSP.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;h3&gt;Report To Endpoint Fixes&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;The &lt;strong&gt;report-uri&lt;/strong&gt; directive has been deprecated within the content security policy. &amp;nbsp;The specification for this endpoint was for it to receive a single CSP Report per request with a content type of &lt;code&gt;application/csp-report&lt;/code&gt;. &amp;nbsp;With the introduction of the &lt;strong&gt;report-to&lt;/strong&gt; directive, browsers are now meant to send a collection of CSP Reports in an array with a content type of &lt;code&gt;application/reports+json&lt;/code&gt;. &amp;nbsp;The intent being to reduce the number of requests being made to any reporting endpoint.&lt;/div&gt;
&lt;br /&gt;
&lt;div&gt;Some browsers appear to have simply started sending their &lt;strong&gt;report-uri&lt;/strong&gt; payload to the &lt;strong&gt;report-to&lt;/strong&gt; endpoint on a per report basis instead of matching the specification. &amp;nbsp;The result is a lot of bad requests being returned and a host of reports being lost. &amp;nbsp;I have updated the &lt;strong&gt;report-to&lt;/strong&gt; endpoint so that it can handle both payloads based on their respective content types.&lt;/div&gt;
&lt;h2&gt;Get It Now&lt;/h2&gt;
&lt;div&gt;Stott Security v3.0 is free to use and is available on all the usual nuget feeds:&lt;/div&gt;
&lt;div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://nuget.optimizely.com/?q=stott&amp;amp;s=Popular&amp;amp;r=10&amp;amp;f=All&quot;&gt;https://nuget.optimizely.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://api.nuget.optimizely.com/?q=stott&amp;amp;s=Popular&amp;amp;r=10&amp;amp;f=All&quot;&gt;https://api.nuget.optimizely.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.nuget.org&quot;&gt;https://www.nuget.org&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;You can find all of my content collated on &lt;a href=&quot;https://www.stott.pro/&quot;&gt;https://www.stott.pro/&lt;/a&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</id><updated>2025-04-09T08:23:00.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Opti ID with Secure Cookies And Third Party AddOns</title><link href="https://world.optimizely.com/blogs/mark-stott/dates/2024/10/secure-cookies-with-opti-id/" /><id>&lt;p&gt;Opti ID has revolutionised access to the Optimizely One suite and is now the preferred authentication method on all PAAS CMS websites that I build. &amp;nbsp;However there are a couple of gotchas that always need to be taken care, outlined below are the solutions to both.&lt;/p&gt;
&lt;h2&gt;Secure Cookies&lt;/h2&gt;
&lt;p&gt;Any penetration test that you perform on your website will always advise that your cookies are set to be Secure, HTTP Only with a SameSite mode of Strict. &amp;nbsp;Setting these is simple enough until you realise that the Opti ID cookies are third party and therefore for need a SameSite mode of None. &amp;nbsp;If you have used Microsoft Entra (Formerly Azure AD) as the authentication method for a CMS, then this problem will be very familiar to you.&lt;br /&gt;&lt;br /&gt;The challenge we have is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We don&#39;t want all cookies to have a SameSite mode of None&lt;/li&gt;
&lt;li&gt;Optimizely cookies are not in our direct control&lt;/li&gt;
&lt;li&gt;Writing verbose custom cookie code everywhere is messy&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In the following solution I have used the app.UseCookiePolicy(...) method to set all of the cookies to be Secure, Http Only with a default SameSite mode of None by default. &amp;nbsp;I then provide a handler for the OnAppendCookie event that will set cookies to use a SameSite mode of Strict provided they do not match one of the following cookie name patterns:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;oid-&lt;/strong&gt; : These are cookies which Opti ID uses for authentication&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;.AspNetCore &lt;/strong&gt;: These are cookies NET Core uses for Authentication&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public static IApplicationBuilder UseSecureCookies(this IApplicationBuilder app)
{
    // Set the default cookie policy
    app.UseCookiePolicy(new CookiePolicyOptions
    {
        HttpOnly = HttpOnlyPolicy.Always,
        Secure = CookieSecurePolicy.Always,
        MinimumSameSitePolicy = SameSiteMode.None,
        OnAppendCookie = context =&amp;gt;
        {
            if (!context.CookieName.StartsWith(&quot;oid-&quot;) &amp;amp;&amp;amp;
                !context.CookieName.StartsWith(&quot;.AspNetCore&quot;))
            {
                // Any cookie that is not an authentication cookie should have a SameSite mode of Strict
                context.CookieOptions.SameSite = SameSiteMode.Strict;
            }
        }
    });

    return app;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Configuring AddOns To Use Opti ID&lt;/h2&gt;
&lt;p&gt;In Optimizely CMS 11, dependency setup relied upon &lt;strong&gt;Initialisation Modules&lt;/strong&gt;. &amp;nbsp;In CMS 12 and .NET core, all of this is now handled in &lt;strong&gt;Startup.cs&lt;/strong&gt;. Some commonly used AddOns include a service extension that is intended to be consumed within a startup.cs. Some go as far as to provide a custom authorization policy so that you can customise access to the AddOn to specific roles rather than granting full admin access to the CMS for users who just need that functionality. Here are a few AddOns and the roles that you might want to use:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Stott Security - CmsAdmins, SecurityAdmins, DataAnalytics&lt;/li&gt;
&lt;li&gt;Stott Robots Handlers - CmsAdmins, SeoAdmins&lt;/li&gt;
&lt;li&gt;Geta NotFound Handler - CmsAdmins, SeoAdmins&lt;/li&gt;
&lt;li&gt;Geta Sitemaps - CmsAdmins, SeoAdmins&lt;/li&gt;
&lt;/ul&gt;
&lt;p class=&quot;code-line&quot;&gt;Here is the challenge: These modules will not allow you to access them when using Opti Id unless you specify the &lt;strong&gt;Opti ID SchemeName&lt;/strong&gt; in the authorizarion policy for each AddOn.&lt;/p&gt;
&lt;p class=&quot;code-line&quot;&gt;Take this configuration for the&amp;nbsp;&lt;strong&gt;Stott Security AddOn&lt;/strong&gt; as an example; this AddOn allows you to segment the data for the AddOn into a separate database and it allows you to define an authorization policy. &amp;nbsp;In this scenario it is a simple matter of making sure the Opti ID Scheme Name is added to the policy:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public static IServiceCollection AddSecurityAddOn(this IServiceCollection services)
{
    services.AddStottSecurity(
        options =&amp;gt;
        {
            options.ConnectionStringName = &quot;EPiServerDB&quot;;
        },
        authorization =&amp;gt;
        {
            authorization.AddPolicy(CspConstants.AuthorizationPolicy, policy =&amp;gt;
            {
                // Use the Opti ID scheme Name
                policy.AddAuthenticationSchemes(OptimizelyIdentityDefaults.SchemeName);
                policy.RequireRole(Roles.CmsAdmins, &quot;SecurityAdmins&quot;);
            });
        });

    return services;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Service Collection Extension Pattern&lt;/h2&gt;
&lt;p&gt;In both of these examples above, I am following the &lt;a title=&quot;Service Collection Extension Pattern&quot; href=&quot;https://dotnetfullstackdev.medium.com/service-collection-extension-pattern-in-net-core-with-item-services-6db8cf9dcfd6&quot;&gt;Service Collection Extension Pattern&lt;/a&gt;. &amp;nbsp;I highly recomment this pattern as it allows you to modularise your configuration code and to keep your startup.cs clean and easy to understand or rearrange. &amp;nbsp;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;You can find all of my content collated on &lt;a href=&quot;https://www.stott.pro/&quot;&gt;https://www.stott.pro/&lt;/a&gt;&lt;/p&gt;</id><updated>2024-12-09T13:07:46.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Creating an Optimizely Addon - Best Practices</title><link href="https://world.optimizely.com/blogs/mark-stott/dates/2024/8/creating-an-optimizely-addon---best-practices/" /><id>&lt;p&gt;In&amp;nbsp;&lt;a href=&quot;/link/38b1217b7fdc41238ed62f1f31907605.aspx&quot;&gt;Part One&lt;/a&gt;,&amp;nbsp;&lt;a href=&quot;/link/c29176d530e64067b29392113b435547.aspx&quot;&gt;Part Two&lt;/a&gt;&amp;nbsp;and&amp;nbsp;&lt;a href=&quot;/link/59fdc24a290e4471bd32fcbb87d6454d.aspx&quot;&gt;Part Three&lt;/a&gt;, I have outlined the steps required to create an AddOn for Optimizely CMS, from architecture to packaging at as a NuGet package. In this part I will be covering some best practices that will help you succeed as an AddOn developer. You can view examples from across this series within the this&amp;nbsp;&lt;a href=&quot;https://github.com/GeekInTheNorth/OptimizelyAddOnTemplate&quot;&gt;Optimizely AddOn Template&lt;/a&gt;&amp;nbsp;that I have been creating.&lt;/p&gt;
&lt;div class=&quot;markdown-heading&quot;&gt;
&lt;h2&gt;Unit Tests&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;As a solo developer managing multiple AddOns, my ability to release updates regularly relies heavily on having extensive unit tests. For instance,&amp;nbsp;&lt;a href=&quot;https://github.com/GeekInTheNorth/Stott.Security.Optimizely&quot;&gt;Stott Security&lt;/a&gt;&amp;nbsp;includes over 1,500 unit tests that run whenever a pull request is made to merge a feature branch into the develop branch. This level of coverage ensures that functionality remains consistent across releases.&lt;/p&gt;
&lt;p&gt;As well as writing unit tests for your business logic, you can also write additional unit tests that validate the security of your controllers. I would consider adding these tests to be an essential part of ensuring the security of your system as they ensure the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Controller actions explicitly allow only the intended HTTP methods, ensuring endpoints respond only to the correct verbs.&lt;/li&gt;
&lt;li&gt;Controller actions are secured with the Authorization attribute or marked with AllowAnonymous if security isn&amp;rsquo;t required. This enforces clear security requirements for each endpoint.&lt;/li&gt;
&lt;li&gt;Controller actions are defined with specific routes, preventing conflicts with other modules or the consuming application&amp;rsquo;s routing.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;[TestFixture]
public sealed class ControllerStandardsTests
{
    [Test]
    [TestCaseSource(typeof(ControllerStandardsTestCases), nameof(ControllerStandardsTestCases.PageControllerActionTestCases))]
    public void ControllersShouldHaveHttpMethodAttributes(string controllerName, string methodName, MethodInfo methodInfo)
    {
        // Act
        var hasHttpMethodAttribute = methodInfo.GetCustomAttributes(typeof(HttpMethodAttribute)).Any();

        // Assert
        // Controllers should only respond in intended Verbs and should respond with method not allowed on unintended verbs.
        // This will prevent posting of malicious payloads to methods not intended to retrieve of deal with these payloads.
        // First raised by a penetration test where an attempt to post files to a content end point returned a 200 instead of a 405
        Assert.That(hasHttpMethodAttribute, Is.True, $&quot;{controllerName}.{methodName} should be decorated with a http method attribute.&quot;);
    }

    [Test]
    [TestCaseSource(typeof(ControllerStandardsTestCases), nameof(ControllerStandardsTestCases.PageControllerActionTestCases))]
    public void ControllerMethodsShouldEitherHaveAuthorizeOrAllowAnonymousAttributes(string controllerName, string methodName, MethodInfo methodInfo)
    {
        // Act
        var hasAuthorizeAttribute = methodInfo.GetCustomAttributes(typeof(AuthorizeAttribute)).Any();
        var hasAllowAnonymousAttribute = methodInfo.GetCustomAttributes(typeof(AllowAnonymousAttribute)).Any();
        var controllerHasAuthorizeAttribute = methodInfo.DeclaringType?.GetCustomAttributes(typeof(AuthorizeAttribute)).Any() ?? false;

        var hasAttribute = hasAuthorizeAttribute || hasAllowAnonymousAttribute || controllerHasAuthorizeAttribute;

        // Assert
        // Controller actions should be protected with an Authorization attribute or an intentional AllowAnonymous attribute.
        // This will ensure your controllers are secure by default and that you have to explicitly allow anonymous access.
        Assert.That(hasAttribute, Is.True, $&quot;{controllerName}.{methodName} should be decorated directly or indirectly with an Authorize or AllowAnonymous attribute.&quot;);
    }

    [Test]
    [TestCaseSource(typeof(ControllerStandardsTestCases), nameof(ControllerStandardsTestCases.PageControllerActionTestCases))]
    public void ControllerMethodsShouldHaveRouteAttributes(string controllerName, string methodName, MethodInfo methodInfo)
    {
        // Act
        var hasRouteAttribute = methodInfo.GetCustomAttributes(typeof(RouteAttribute)).Any();
        var controllerHasRouteAttribute = methodInfo.DeclaringType?.GetCustomAttributes(typeof(RouteAttribute)).Any() ?? false;

        var hasAttribute = hasRouteAttribute || controllerHasRouteAttribute;

        // Assert
        // Controller actions should have a fixed route attribute so as to not have clashes with routes declared by other modules.
        Assert.That(hasAttribute, Is.True, $&quot;{controllerName}.{methodName} should be decorated directly with a Route attribute.&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These tests use a Test Case Source method which uses reflection to identify all controllers within your solution. This means as you add new controllers, you will not forget to secure them.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public static class ControllerStandardsTestCases
{
    public static IEnumerable&amp;lt;TestCaseData&amp;gt; PageControllerActionTestCases
    {
        get
        {
            var assembly = Assembly.GetAssembly(typeof(SettingsLandingPageController));

            if (assembly == null)
            {
                yield break;
            }

            var controllers = assembly.GetTypes()
                                      .Where(t =&amp;gt; (t.BaseType?.Name.StartsWith(&quot;Controller&quot;) ?? false)
                                               || (t.BaseType?.Name.StartsWith(&quot;BaseController&quot;) ?? false))
                                      .ToList();

            foreach (var controller in controllers)
            {
                var actions = controller.GetMethods()
                                        .Where(x =&amp;gt; x.DeclaringType == controller &amp;amp;&amp;amp; x.IsPublic)
                                        .ToList();

                foreach (var methodInfo in actions)
                {
                    if (methodInfo.ReturnType == typeof(IActionResult) || methodInfo.ReturnType == typeof(Task&amp;lt;IActionResult&amp;gt;))
                    {
                        yield return new TestCaseData(controller.Name, methodInfo.Name, methodInfo);
                    }
                }
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;markdown-heading&quot;&gt;
&lt;h2&gt;Test System&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;If you have followed this series to create an AddOn, you&#39;ll note that the sample site for developing the AddOn does not use nuget to consume the AddOn code and instead uses a project reference. This will allow you to develop efficiently, however it does mean that you are not testing your code in a production-like manner.&lt;/p&gt;
&lt;p&gt;Create a separate repository with it&#39;s own Optimizely CMS solution. Import your AddOn as a NuGet package directly into this solution. This will allow you to test your AddOn in the same way that another developer will be experiencing your AddOn for the first time. If you are able to generate a developer cloud license on&amp;nbsp;&lt;a href=&quot;https://license.episerver.com/&quot;&gt;EPiServer License Centre&lt;/a&gt;, then I would recommend you deploy this test system into an Azure WebApp running on Linux with .NET 6.0 or 8.0 so that you can validate your AddOn inside of a deployed environment.&lt;/p&gt;
&lt;p&gt;As part of my go live cycle I perform the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Create a beta version of my nuget package&lt;/li&gt;
&lt;li&gt;Upload the beta package to nuget.org&lt;/li&gt;
&lt;li&gt;Update my test system to use the beta package&lt;/li&gt;
&lt;li&gt;Deploy my test system to Azure&lt;/li&gt;
&lt;li&gt;Delist my beta version on nuget.org&lt;/li&gt;
&lt;li&gt;Test my AddOn in my test system&lt;/li&gt;
&lt;li&gt;Create a production version of my nuget package&lt;/li&gt;
&lt;li&gt;Upload the production package to nuget.optimizely.com&lt;/li&gt;
&lt;li&gt;Wait for the package to be approved by Optimizely&#39;s QA team&lt;/li&gt;
&lt;li&gt;Upload the production package to nuget.org&lt;/li&gt;
&lt;li&gt;Announce the release&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;markdown-heading&quot;&gt;
&lt;h2&gt;Performance and Caching&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Optimizely&#39;s default settings for Azure SQL Server typically support around 120 simultaneous database connections, suggesting the use of an S2 SQL Database (or equivelent) for production. This configuration, combined with efficient caching by the Optimizely CMS code allows smaller databases to perform effectively for websites with high traffic.&lt;/p&gt;
&lt;p&gt;If you&#39;re using Microsoft Entity Framework, be aware that each&amp;nbsp;&lt;code&gt;DbContext&lt;/code&gt;&amp;nbsp;instance opens a new database connection. Failing to manage these connections can lead to server instability due to connection limits. Therefore, it&#39;s advisable to follow Optimizely&#39;s approach by extensively using caching. To implement this, consider the following steps:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use a Custom Cache Wrapper that consumes Optimizely&#39;s&amp;nbsp;&lt;code&gt;ISynchronizedObjectInstanceCache&lt;/code&gt;&amp;nbsp;and uses it&#39;s own master key to allow you to purge your cache effectively.&lt;/li&gt;
&lt;li&gt;Inject your DbContext as a scoped object to limit the number of instances to 1 per request.&lt;/li&gt;
&lt;li&gt;Lazy Load dependencies that require a Db Context so that they are not instantiated if not consumed.&lt;/li&gt;
&lt;li&gt;Handle data loading in the following order:
&lt;ul&gt;
&lt;li&gt;Attempt to retrieve and return data from cache first.&lt;/li&gt;
&lt;li&gt;Attempt to retrieve and return data from the database second.
&lt;ul&gt;
&lt;li&gt;Push the data into a cache before returning it.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The following is an example of a cache wrapper that consumes the&amp;nbsp;&lt;code&gt;ISynchronizedObjectInstanceCache&lt;/code&gt;.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public sealed class CacheWrapper : ICacheWrapper
{
    private readonly ISynchronizedObjectInstanceCache _cache;

    private const string MasterKey = &quot;My-OptimizelyAddOn-MasterKey&quot;;

    public CacheWrapper(ISynchronizedObjectInstanceCache cache)
    {
        _cache = cache;
    }

    public void Add&amp;lt;T&amp;gt;(string cacheKey, T? objectToCache)
        where T : class
    {
        if (string.IsNullOrWhiteSpace(cacheKey) || objectToCache == null)
        {
            return;
        }

        try
        {
            var evictionPolicy = new CacheEvictionPolicy(
                TimeSpan.FromHours(12),
                CacheTimeoutType.Absolute,
                Enumerable.Empty&amp;lt;string&amp;gt;(),
                new[] { MasterKey });

            _cache.Insert(cacheKey, objectToCache, evictionPolicy);
        }
        catch (Exception exception)
        {
            // Add logging here
        }
    }

    public T? Get&amp;lt;T&amp;gt;(string cacheKey)
        where T : class
    {
        return _cache.TryGet&amp;lt;T&amp;gt;(cacheKey, ReadStrategy.Wait, out var cachedObject) ? cachedObject : default;
    }

    public void RemoveAll()
    {
        try
        {
            _cache.Remove(MasterKey);
        }
        catch (Exception exception)
        {
            // Add logging here
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In order to use Lazy Loaded dependencies, you first need to define the Lazy variant within your service extension method:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;services.AddScoped&amp;lt;IMyDataContext, MyDataContext&amp;gt;();
services.AddScoped&amp;lt;Lazy&amp;lt;IMyDataContext&amp;gt;&amp;gt;(provider =&amp;gt; new Lazy&amp;lt;IMyDataContext&amp;gt;(() =&amp;gt; provider.GetRequiredService&amp;lt;IMyDataContext&amp;gt;()));&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can then declare your dependencies as Lazy in your constructors and consume them as per the example below. In this scenario, the&amp;nbsp;&lt;code&gt;IMyDataContext&lt;/code&gt;&amp;nbsp;is instantiated once, but that is deferred until it is used by the&amp;nbsp;&lt;code&gt;GetData()&lt;/code&gt;&amp;nbsp;method:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;internal sealed class MyRepository : IMyRepository
{
  private readonly Lazy&amp;lt;IMyDataContext&amp;gt; _context;

  public MyRepository(Lazy&amp;lt;IMyDataContext&amp;gt; context)
  {
    _context = context;
  }

  public async Task&amp;lt;IList&amp;lt;string&amp;gt;&amp;gt; GetData()
  {
    return await _context.Value.MyData.ToListAsync();
  }
} &lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A service can then consume both the&amp;nbsp;&lt;code&gt;IMyRepository&lt;/code&gt;and&amp;nbsp;&lt;code&gt;ICacheWrapper&lt;/code&gt;&amp;nbsp;and make performant calls to retrieve data that has not changed. In the following example we attempt to retrieve the data from the cache first, then if it is null or empty we then attempt to load the data from the repository, push that data into cache before returning it. If the&amp;nbsp;&lt;code&gt;Delete&lt;/code&gt;&amp;nbsp;method is called within the service, we call&amp;nbsp;&lt;code&gt;RemoveAll()&lt;/code&gt;&amp;nbsp;on the cache wrapper to invalidate cache entries based on a master key:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;internal sealed class MyService : IMyService
{
  private readonly IMyRepository _repository;
  private readonly ICacheWrapper _cache;
  private const string CacheKey = &quot;Unique.Cache.Key&quot;;

  public MyService(IMyRepository repository, ICacheWrapper cache)
  {
    _repository = repository;
    _cache = cache;
  }

  public async Task&amp;lt;IList&amp;lt;string&amp;gt;&amp;gt; GetDate()
  {
    var data = _cache.Get&amp;lt;IList&amp;lt;string&amp;gt;&amp;gt;(CacheKey);
    if (data is not { Count: &amp;gt;0 })
    {
      data = await _repository.GetData();
      _cache.Add(CacheKey, data);
    }

    return data;
  }

  public async Task Delete(string data)
  {
    await _repository.Delete(data);

    _cache.RemoveAll();
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note how we don&#39;t need to lazy load the repository as the database context is already lazy loaded by the repository itself, however you may choose to lazy load the repository as it is not used if the cache is populated.&lt;/p&gt;
&lt;div class=&quot;markdown-heading&quot;&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Ensure all key business logic is covered by a unit test.&lt;/li&gt;
&lt;li&gt;Use unit tests to enforce standards such as:
&lt;ul&gt;
&lt;li&gt;Controller actions have correct HTTP method attributes.&lt;/li&gt;
&lt;li&gt;Controllers have secure actions with authorization or explicit anonymous access.&lt;/li&gt;
&lt;li&gt;Controllers have defined routes to avoid conflicts.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Test your AddOn in a separate production-like environment.&lt;/li&gt;
&lt;li&gt;Use caching to optimize database access.&lt;/li&gt;
&lt;li&gt;Implement lazy loading and scoped dependencies.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;You can find all of my content collated on &lt;a href=&quot;https://www.stott.pro/&quot;&gt;https://www.stott.pro/&lt;/a&gt;&lt;/p&gt;</id><updated>2024-10-24T12:32:06.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Creating an Optimizely Addon - Packaging for NuGet</title><link href="https://world.optimizely.com/blogs/mark-stott/dates/2024/8/creating-an-optimizely-addon---packaging-for-nuget/" /><id>&lt;p&gt;In&amp;nbsp;&lt;a href=&quot;/link/38b1217b7fdc41238ed62f1f31907605.aspx&quot;&gt;Part One&lt;/a&gt;&amp;nbsp;and&amp;nbsp;&lt;a href=&quot;/link/c29176d530e64067b29392113b435547.aspx&quot;&gt;Part Two&lt;/a&gt;&amp;nbsp;of this series; I covered topics from having a great idea, solution structure, extending the menus and adding gadgets to the editor interface. In this part I will be covering the challenges of creating and submitting your AddOn as a NuGet package into the Optimizely NuGet feed. You can view examples from across this series within the this&amp;nbsp;&lt;a href=&quot;https://github.com/GeekInTheNorth/OptimizelyAddOnTemplate&quot;&gt;Optimizely AddOn Template&lt;/a&gt;&amp;nbsp;that I have been creating.&lt;/p&gt;
&lt;div class=&quot;markdown-heading&quot;&gt;
&lt;h2&gt;Defining What to Package&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Each project within your solution must be created as a NuGet package if it is deemed to be the primary project or a dependency for the primary project. Consider the following solution as an example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MySolution.sln
&lt;ul&gt;
&lt;li&gt;MyAddOn.Admin.csproj&lt;/li&gt;
&lt;li&gt;MyAddOn.Core.csproj&lt;/li&gt;
&lt;li&gt;MyAddOn.Test.csproj&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In this scenario, the administrator interface for the AddOn is separated from its core shared functionality. This design enables a consuming site to incorporate MyAddOn.Admin into their Optimizely website project and to reference MyAddOn.Core within any project in their solution structure. Consequently, MyAddOn.Admin has a direct dependency on MyAddOn.Core. To publish MyAddOn.Admin as a NuGet package, MyAddOn.Core must also be published as a NuGet package. It should be noted that MyAddOn.Admin only requires MyAddOn.Core as a project dependency during development; this dependency will be converted into a package dependency during the packaging process.&lt;/p&gt;
&lt;div class=&quot;markdown-heading&quot;&gt;
&lt;h2&gt;Defining NuGet Properties&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;If you are using Visual Studio, right click on the project you want to package and select properties to show the project properties screen. Under the Package section you can define all of the properties for your NuGet package.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/82eb39f7e54044f2944d4c45f7385832.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;I would recommend you complete the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Package Id :&lt;/strong&gt; This will need to be globally unique name within nuget.org and nuget.optimizely.com. If you use the&amp;nbsp;&lt;code&gt;$(AssemblyName)&lt;/code&gt;&amp;nbsp;variable, then this will match the name of the project.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Title :&lt;/strong&gt; Visual Studio describes this as the name of the package used in UI displays such as Package Manager, but this largely does not get used.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Package Version : &lt;/strong&gt;This should be a semantic version number with three or four parts and an optional alpha or beta tag. For Example:
&lt;ul&gt;
&lt;li&gt;1.0.0&lt;/li&gt;
&lt;li&gt;1.0.0.0&lt;/li&gt;
&lt;li&gt;0.1.1-alpha&lt;/li&gt;
&lt;li&gt;0.2.2.0-beta&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Authors : &lt;/strong&gt;This should contain the names of the primary people who will own the AddOn / Repository.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Company :&lt;/strong&gt; This should contain the name of the business that is behind creating the Addon. If this is individually owned, then seting this to&amp;nbsp;&lt;code&gt;$(Authors)&lt;/code&gt;&amp;nbsp;will mirror the value from the Authors property.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Description :&lt;/strong&gt; This should be a short description about your Addon, this will be visible within the NuGet package feed and within the Plugin Manager screen within Optimizely CMS.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Copyright : &lt;/strong&gt;This should contain the name of the owner and the year. You get copyright protection automatically when creating software and you do not have to apply or pay a fee. There isn&amp;rsquo;t a register of copyright works in the UK. There are however organisations which will provide extra protection for a fee for validating your copyright. You can read more about copyright here:&amp;nbsp;&lt;a href=&quot;https://www.gov.uk/copyright&quot;&gt;How copyright protects your work&lt;/a&gt;. It is however worth you performing your own research into the matter within the country you live in.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Project Url : &lt;/strong&gt;This should point either to the repository for your Addon or an appropriate project page. Developers will use this to find out more about your Addon or to report issues that may need resolving.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Readme : &lt;/strong&gt;I have set this to the readme.md for my repositories, this will be visible to developers within the NuGet platform.
&lt;ul&gt;
&lt;li&gt;Do ensure assets such as images have absolute paths as this readme will be visible outside of the context of your repository and relative paths will result in images not being found.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Repository Url :&lt;/strong&gt; This should point to the repository for your Addon, assuming that your Addon is Open Source.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tags :&lt;/strong&gt; This is a delimited set of tags that make your package easier to find within the NuGet feeds.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;License File :&lt;/strong&gt; This should reference the license within your repository. Careful consideration should be given to the type of license for your AddOn. Certain licenses may require your users to make their code open source to utilize your package, so think carefully about the permissiveness or restrictiveness of your license. It is noteworthy that some highly popular AddOns employ an MIT or Apache license.
&lt;ul&gt;
&lt;li&gt;I am utilizing an MIT license due to its permissive nature and lack of warranty. While I do engage with my users and address any issues that are raised, my AddOns are free and are maintained in my free time.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Require License Acceptance :&lt;/strong&gt; If you tick this, the consumer will have to accept the license as they install the package. If you are using an MIT license, you may want to tick this to encourage the consumer to accept the warranty free nature of your AddOn.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you are using Visual Studio Code instead of Visual Studio, then you can edit the .csproj directly and add the package properties directly as XML values at the top of the csproj file. You can also add these properties into a .nuspec instead, when you package your project, the values from the .csproj and .nuspec are merged into a new .nuspec that is contained in the root of the compiled .nupkg file. I personnally prefer to put the NuGet properties directly into the .csproj.&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;&amp;lt;Project Sdk=&quot;Microsoft.NET.Sdk.Razor&quot;&amp;gt;
  &amp;lt;PropertyGroup&amp;gt;
    &amp;lt;TargetFrameworks&amp;gt;net6.0;net8.0&amp;lt;/TargetFrameworks&amp;gt;
    &amp;lt;AddRazorSupportForMvc&amp;gt;true&amp;lt;/AddRazorSupportForMvc&amp;gt;
    &amp;lt;Version&amp;gt;1.1.0.0&amp;lt;/Version&amp;gt;
    &amp;lt;RepositoryUrl&amp;gt;https://example.com/&amp;lt;/RepositoryUrl&amp;gt;
    &amp;lt;PackageProjectUrl&amp;gt;https://example.com/&amp;lt;/PackageProjectUrl&amp;gt;
    &amp;lt;PackageLicenseFile&amp;gt;LICENSE.txt&amp;lt;/PackageLicenseFile&amp;gt;
    &amp;lt;Authors&amp;gt;Your Name&amp;lt;/Authors&amp;gt;
    &amp;lt;Description&amp;gt;Your Package Summary&amp;lt;/Description&amp;gt;
    &amp;lt;Copyright&amp;gt;Your Name 2024&amp;lt;/Copyright&amp;gt;
    &amp;lt;PackageTags&amp;gt;TagOne TagTwo&amp;lt;/PackageTags&amp;gt;
    &amp;lt;PackageRequireLicenseAcceptance&amp;gt;true&amp;lt;/PackageRequireLicenseAcceptance&amp;gt;
    &amp;lt;RepositoryType&amp;gt;git&amp;lt;/RepositoryType&amp;gt;
    &amp;lt;PackageReadmeFile&amp;gt;README.md&amp;lt;/PackageReadmeFile&amp;gt;
    &amp;lt;AssemblyVersion&amp;gt;1.1.0.0&amp;lt;/AssemblyVersion&amp;gt;
    &amp;lt;GeneratePackageOnBuild&amp;gt;True&amp;lt;/GeneratePackageOnBuild&amp;gt;
    &amp;lt;PackageReleaseNotes&amp;gt;A short release summary.&amp;lt;/PackageReleaseNotes&amp;gt;
    &amp;lt;Nullable&amp;gt;enable&amp;lt;/Nullable&amp;gt;
    &amp;lt;Title&amp;gt;Package Name&amp;lt;/Title&amp;gt;
  &amp;lt;/PropertyGroup&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;markdown-heading&quot;&gt;
&lt;h2&gt;NuGet Package Structure&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;A NuGet Package is simply a zip file containing a structured set of files. If you rename a .nupkg to a .zip, you can extract it and explore it&#39;s structure. This will have a structure similar to the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;package
&lt;ul&gt;
&lt;li&gt;services
&lt;ul&gt;
&lt;li&gt;metadata
&lt;ul&gt;
&lt;li&gt;core-properties&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;build
&lt;ul&gt;
&lt;li&gt;project.name.targets&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;contentFiles
&lt;ul&gt;
&lt;li&gt;additional.file.txt&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;lib
&lt;ul&gt;
&lt;li&gt;net6.0
&lt;ul&gt;
&lt;li&gt;my.project.dll&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;net8.0
&lt;ul&gt;
&lt;li&gt;my.project.dll&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;my.project.nuspec&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;_rels&lt;/li&gt;
&lt;li&gt;[Content_Types].xml&lt;/li&gt;
&lt;li&gt;readme.md&lt;/li&gt;
&lt;li&gt;license.txt&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Folders such as build, contentFiles and the target folders under lib will vary depending on your code and deployable files. The readme.md and license.txt files referenced in your .csproj or .nuspec are copied to the root of the NuGet package.&lt;/p&gt;
&lt;div class=&quot;markdown-heading&quot;&gt;
&lt;h2&gt;Packaging for Multiple Frameworks&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;.NET Core is backwards compatible, meaning that if you build your package for .NET 6, it can be installed into .NET 6, 7, and 8. For most AddOns, compiling directly for .NET 6 ensures maximum compatibility.&lt;/p&gt;
&lt;p&gt;However, there may be instances where you need to compile your application in multiple framework versions. For example, if you are using Entity Framework and Migrations, there is a breaking change between .NET 6 and .NET 8. Fortunately, no code changes are required, but you will need to set your dependencies separately for .NET 6 and .NET 8. To accomplish this, you must make two modifications.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Change the&amp;nbsp;&lt;code&gt;TargetFramework&lt;/code&gt;&amp;nbsp;node in your .csproj to be&amp;nbsp;&lt;code&gt;TargetFrameworks&lt;/code&gt;&amp;nbsp;and separate your target frameworks with a semicolon. e.g.&amp;nbsp;&lt;code&gt;net6.0;net8.0&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Add a separate&amp;nbsp;&lt;code&gt;ItemGroup&lt;/code&gt;&amp;nbsp;per framework version to contain framework specific dependencies and add a condition to the ItemGroup to target the specific framework. e.g.&amp;nbsp;&lt;code&gt;Condition=&quot;&#39;$(TargetFramework)&#39; == &#39;net6.0&#39;&quot;&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;&amp;lt;Project Sdk=&quot;Microsoft.NET.Sdk.Razor&quot;&amp;gt;
  &amp;lt;PropertyGroup&amp;gt;
    &amp;lt;TargetFrameworks&amp;gt;net6.0;net8.0&amp;lt;/TargetFrameworks&amp;gt;
    &amp;lt;AddRazorSupportForMvc&amp;gt;true&amp;lt;/AddRazorSupportForMvc&amp;gt;
    &amp;lt;Nullable&amp;gt;enable&amp;lt;/Nullable&amp;gt;
  &amp;lt;/PropertyGroup&amp;gt;

  &amp;lt;ItemGroup Condition=&quot;&#39;$(TargetFramework)&#39; == &#39;net6.0&#39;&quot;&amp;gt;
    &amp;lt;PackageReference Include=&quot;Microsoft.EntityFrameworkCore.SqlServer&quot; Version=&quot;6.0.6&quot; /&amp;gt;
  &amp;lt;/ItemGroup&amp;gt;

  &amp;lt;ItemGroup Condition=&quot;&#39;$(TargetFramework)&#39; == &#39;net8.0&#39;&quot;&amp;gt;
    &amp;lt;PackageReference Include=&quot;Microsoft.EntityFrameworkCore.SqlServer&quot; Version=&quot;8.0.1&quot; /&amp;gt;
  &amp;lt;/ItemGroup&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will double the size of your NuGet package as it will contain separate folders for each target framework containing your code compiled for that framework.&lt;/p&gt;
&lt;div class=&quot;markdown-heading&quot;&gt;
&lt;h2&gt;Packaging Additional Files For The Protected Modules Folder&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;If your package contains an&amp;nbsp;&lt;code&gt;IFrameComponent&lt;/code&gt;&amp;nbsp;or other files needed to extend the Editor Interface. A&amp;nbsp;&lt;code&gt;module.config&lt;/code&gt;&amp;nbsp;file and those files will need to be deployed to the&amp;nbsp;&lt;code&gt;modules/_protected/my_project&lt;/code&gt;&amp;nbsp;folder within the target website.&lt;/p&gt;
&lt;p&gt;First you will need to tell the .csproj file that we want to copy these files into the&amp;nbsp;&lt;code&gt;contentFiles&lt;/code&gt;&amp;nbsp;folder of the NuGet package. This is as simple as setting the build output for those files to be&amp;nbsp;&lt;code&gt;None&lt;/code&gt;&amp;nbsp;and to set the&amp;nbsp;&lt;code&gt;PackagePath&lt;/code&gt;&amp;nbsp;to be inside of the&amp;nbsp;&lt;code&gt;contentFiles&lt;/code&gt;&amp;nbsp;folder.&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;&amp;lt;ItemGroup&amp;gt;
  &amp;lt;None Include=&quot;module.config&quot;&amp;gt;
    &amp;lt;Pack&amp;gt;true&amp;lt;/Pack&amp;gt;
    &amp;lt;PackagePath&amp;gt;contentFiles\module.config&amp;lt;/PackagePath&amp;gt;
  &amp;lt;/None&amp;gt;
&amp;lt;/ItemGroup&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You will then need to create a .targets file that instructs the NuGet package installer how to handle those files. The example below is taken straight from my own Addons where I am doing the same thing.&lt;/p&gt;
&lt;p&gt;The&amp;nbsp;&lt;code&gt;ItemGroup&lt;/code&gt;&amp;nbsp;tells the .targets file where the specific files are within the NuGet package structure. The&amp;nbsp;&lt;code&gt;$(MSBuildThisFileDirectory)&lt;/code&gt;&amp;nbsp;variable in this case is a reference to the directory the .targets file sits in. As this is in a build folder, I have used the&amp;nbsp;&lt;code&gt;$(MSBuildThisFileDirectory)&lt;/code&gt;&amp;nbsp;variable in combination with the relative path to my module.config file.&lt;/p&gt;
&lt;p&gt;The&amp;nbsp;&lt;code&gt;Target&lt;/code&gt;&amp;nbsp;node is then performing an action that is configured to execute on&amp;nbsp;&lt;code&gt;BeforeBuild&lt;/code&gt;. This then performs a&amp;nbsp;&lt;code&gt;Copy&lt;/code&gt;&amp;nbsp;action that will take my module.config file from the contentFiles folder in the nuget package to the&amp;nbsp;&lt;code&gt;modules\_protected\my_project&lt;/code&gt;&amp;nbsp;folder within the target website. This means that when you first install the package, the module.config file and folder will not exist within the protected modules folder. When you first build the solution they will be copied into this location.&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&amp;gt;
&amp;lt;Project xmlns=&quot;http://schemas.microsoft.com/developer/msbuild/2003&quot; ToolsVersion=&quot;4.0&quot;&amp;gt;
  &amp;lt;ItemGroup&amp;gt;
    &amp;lt;MyFiles Include=&quot;$(MSBuildThisFileDirectory)..\contentFiles\module.config&quot; /&amp;gt;
  &amp;lt;/ItemGroup&amp;gt;
  
  &amp;lt;Target Name=&quot;CopyFiles&quot; BeforeTargets=&quot;BeforeBuild&quot;&amp;gt;
        &amp;lt;Copy SourceFiles=&quot;@(MyFiles)&quot; DestinationFolder=&quot;$(MSBuildProjectDirectory)\modules\_protected\my_project\&quot; /&amp;gt;
    &amp;lt;/Target&amp;gt;
&amp;lt;/Project&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In order to make sure the .targets file can be executed, we also need to make sure that it is copied into the NuGet package file. This is as simple as editing your .csproj file and configuring the build output for the .targets file to be &lt;code&gt;None&lt;/code&gt;&amp;nbsp;and to set the&amp;nbsp;&lt;code&gt;PackagePath&lt;/code&gt;&amp;nbsp;to be inside of the&amp;nbsp;&lt;code&gt;build&lt;/code&gt;&amp;nbsp;folder.&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;&amp;lt;ItemGroup&amp;gt;
  &amp;lt;None Include=&quot;msbuild\copyfiles.targets&quot;&amp;gt;
    &amp;lt;Pack&amp;gt;true&amp;lt;/Pack&amp;gt;
    &amp;lt;PackagePath&amp;gt;build\$(MSBuildProjectName).targets&amp;lt;/PackagePath&amp;gt;
  &amp;lt;/None&amp;gt;
&amp;lt;/ItemGroup&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;markdown-heading&quot;&gt;
&lt;h2&gt;Submitting Your Package&lt;/h2&gt;
&lt;p&gt;Before submitting your AddOn to the Optimizely NuGet package feed, it is essential to ensure that your package installs successfully both in your local environment and within a CI/CD pipeline. To expedite this process, consider publishing your package as an alpha or beta build to &lt;a href=&quot;https://www.nuget.org/&quot;&gt;nuget.org&lt;/a&gt; first. After publishing, your package will be indexed and available for retrieval within a few minutes.&lt;/p&gt;
&lt;p&gt;To designate your package as an alpha or beta release, you should modify the&amp;nbsp;&lt;code&gt;version&lt;/code&gt;&amp;nbsp;property within your `.csproj` file to include a trailing &lt;code&gt;-alpha&lt;/code&gt;&amp;nbsp;or&amp;nbsp;&lt;code&gt;-beta&lt;/code&gt;. NuGet will automatically recognize this as a pre-release version and will generally filter these versions out by default. Developers can view these pre-release versions by selecting the option to display pre-release versions within their IDE&amp;rsquo;s NuGet package tool.&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;&amp;lt;Version&amp;gt;2.0.0.2-beta&amp;lt;/Version&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Upon publishing the alpha or beta version of your package to nuget.org and confirming its successful installation both locally and in a CI/CD pipeline, you will be prepared to submit the live version of your package to Optimizely.&lt;/p&gt;
&lt;p&gt;Ensure that you have an &lt;a href=&quot;/link/6c9478a8761c41d88dfc32e9ef56e714.aspx&quot;&gt;Optimizely World&lt;/a&gt; account. You can create a new account by visiting&amp;nbsp;&lt;a href=&quot;/link/6c9478a8761c41d88dfc32e9ef56e714.aspx&quot;&gt;Optimizely World&lt;/a&gt; and following the registration link located in the top right corner. This account will also provide access to the Optimizely NuGet feeds. Optimizely maintains two NuGet feeds:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://nuget.optimizely.com/&quot;&gt;https://nuget.optimizely.com&lt;/a&gt; (v2 NuGet feed)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://api.nuget.optimizely.com/&quot;&gt;https://api.nuget.optimizely.com&lt;/a&gt; (v3 NuGet feed)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Packages uploaded to the v2 NuGet feed are automatically synchronized to the v3 NuGet feed. Therefore, it is advisable to upload your packages to the v2 NuGet feed. Once Optimizely receives your package, it will undergo an approval process conducted by Optimizely&#39;s QA team. During this process, the QA team will verify that your AddOn functions correctly with the CMS. Including test guidance in the readme for your repository can be very beneficial for the QA team. This review process may take one or more business days, and there is currently no feedback mechanism to inform you of the status or outcome of the testing. You may periodically check the NuGet feed to determine if your package has been accepted. Given that Optimizely validates all packages uploaded to their NuGet feed, it is recommended to download AddOn updates directly from Optimizely and distribute your own package in this manner. Should you need to release a hotfix promptly, you may consider uploading it to nuget.org.&lt;/p&gt;
&lt;p&gt;It is advisable to upload your package to nuget.org at least once in addition to the Optimizely NuGet feed. This ensures that the package name is reserved on nuget.org, avoiding potential conflicts in package names across the main feeds that could affect your consumers.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Please note that as of the time of writing, there was an issue with packages uploaded directly to the v3 NuGet feed not being synchronized back to the v2 NuGet feed. Until this issue is resolved, the Upload link on the v3 NuGet feed redirects users to the v2 NuGet feed. Optimizely is actively working to resolve this issue.&lt;/em&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;markdown-heading&quot;&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Build your package for .NET 6 for maximum compatability
&lt;ul&gt;
&lt;li&gt;Build your package for both .NET 6 &amp;amp; 8 if you have compatability issues between both frameworks.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Use a Razor Class Library so you can package your UI and C# code together.&lt;/li&gt;
&lt;li&gt;Think very carefully about which license you will use for your package.&lt;/li&gt;
&lt;li&gt;Use a build targets file to put files into specific folders within a consuming application.&lt;/li&gt;
&lt;li&gt;Test your package installs and works as an alpha/beta on nuget.org before submitting to the Optimizely NuGet feed.&lt;/li&gt;
&lt;li&gt;Upload your package to&amp;nbsp;&lt;a href=&quot;https://nuget.optimizely.com/&quot;&gt;nuget.optimizely.com&lt;/a&gt;&amp;nbsp;when it is ready.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;You can find all of my content collated on &lt;a href=&quot;https://www.stott.pro/&quot;&gt;https://www.stott.pro/&lt;/a&gt;&lt;/p&gt;</id><updated>2024-09-16T08:17:11.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Creating an Optimizely CMS Addon - Adding an Editor Interface Gadget</title><link href="https://world.optimizely.com/blogs/mark-stott/dates/2024/8/creating-an-optimizely-cms-addon---adding-an-editor-interface-gadget/" /><id>&lt;p&gt;In&amp;nbsp;&lt;a href=&quot;/link/38b1217b7fdc41238ed62f1f31907605.aspx&quot;&gt;Part One&lt;/a&gt;&amp;nbsp;of this series, I covered getting started with creating your own AddOn for Optimizely CMS 12. This covered what I consider to be an ideal solution structure, best practices for your JavaScript and Styles, extending the menu interface and authentication. In Part Two, I will be covering adding an additional editor interface gadget. You can view examples from across this series within the this&amp;nbsp;&lt;a href=&quot;https://github.com/GeekInTheNorth/OptimizelyAddOnTemplate&quot;&gt;Optimizely AddOn Template&lt;/a&gt;&amp;nbsp;that I have been creating.&lt;/p&gt;
&lt;div class=&quot;markdown-heading&quot;&gt;
&lt;h2&gt;Adding a Gadget to the Editor Interface&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;You can easily turn a standard MVC Controller into a CMS Editor Gadget by decorating it with the&amp;nbsp;&lt;code&gt;[IFrameComponent]&lt;/code&gt;&amp;nbsp;attribute. The primary properties of the attributes are as follows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Title &lt;/strong&gt;: The name of the gadget, visible in the Gadget selector.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Description &lt;/strong&gt;: A brief description of the gadget, also visible in the Gadget selector. It is recommended to keep this to one sentence.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Categories &lt;/strong&gt;: The appropriate category for the gadget. For content-specific gadgets, this should be set to &quot;content&quot; or &quot;cms&quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Url &lt;/strong&gt;: The route corresponding to your controller action.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PlugInAreas &lt;/strong&gt;: The location within the system where the component should be made available.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ReloadOnContextChange &lt;/strong&gt;: Enables the UI to reload the gadget each time a different content item is selected within the CMS interface.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Optimizely will automatically identify these controllers and use the properties of the &lt;code&gt;[IFrameComponent]&lt;/code&gt;&amp;nbsp;attribute to populate the Add Gadgets window within the CMS Editor interface:&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/4e76e725f84b403881ffa86520d11db1.aspx?1725023534237&quot; alt=&quot;Gadget Selector in Optimizely CMS 12 Editor Interface&quot; width=&quot;1308&quot; height=&quot;1013&quot; /&gt;&lt;/p&gt;
&lt;p&gt;When the editor interface loads your gadget, it will include an&amp;nbsp;&lt;code&gt;id&lt;/code&gt;&amp;nbsp;query string parameter containing a versioned content reference in string format (e.g., 123_456). The number on the left represents the permanent identity of the content, while the number on the right denotes the specific version of that content item. This information can be used to load the specific version of a content item and incorporate it into your gadget&#39;s model.&lt;/p&gt;
&lt;p&gt;Below is an example of the minimum required setup for your controller. Note the inclusion of the [Authorize] attribute on the controller and the [HttpGet] attribute on the action. These ensure that the user is authenticated and that the interface cannot be accessed using an unexpected HTTP verb. Note how I use the &lt;code&gt;id&lt;/code&gt;&amp;nbsp;query string parameter to load the specific version of the page the user has selected within the CMS Editor interface to provide context specific information to the user.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;[Authorize(Policy = OptimizelyAddOnConstants.AuthorizationPolicy)]
[IFrameComponent(
    Url = &quot;/optimizely-addon/gadget/index/&quot;,
    Title = &quot;Example Gadget&quot;,
    Description = &quot;An example gadget for the CMS Editor Interface.&quot;,
    Categories = &quot;content&quot;,
    PlugInAreas = &quot;/episerver/cms/assets&quot;,
    MinHeight = 200,
    MaxHeight = 800,
    ReloadOnContextChange = true)]
public sealed class GadgetController : Controller
{
    private readonly IContentLoader _contentLoader;

    public GadgetController(IContentLoader contentLoader)
    {
        _contentLoader = contentLoader;
    }

    [HttpGet]
    [Route(&quot;~/optimizely-addon/gadget/index&quot;)]
    public IActionResult Index()
    {
        var model = new GadgetViewModel
        {
            Page = GetPageData(Request),
            ContentId = Request.Query[&quot;Id&quot;].ToString()
        };

        return View(&quot;~/Views/OptimizelyAddOn/Gadget/Index.cshtml&quot;, model);
    }

    private PageData? GetPageData(HttpRequest request)
    {
        var contentReferenceValue = request.Query[&quot;Id&quot;].ToString() ?? string.Empty;
        if (string.IsNullOrWhiteSpace(contentReferenceValue))
        {
            return null;
        }

        var contentReference = new ContentReference(contentReferenceValue);
        if (_contentLoader.TryGet&amp;lt;PageData&amp;gt;(contentReference, out var pageData))
        {
            return pageData;
        }

        return null;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the gadget I developed for&amp;nbsp;&lt;a href=&quot;https://github.com/GeekInTheNorth/Stott.Security.Optimizely&quot;&gt;Stott Security&lt;/a&gt;, I focus exclusively on rendering a preview of the security headers for the currently selected page.&amp;nbsp; I added this feature as Stott Security supports extending the Content Security Policy for any given page.&amp;nbsp; To ensure the user can be clear on the context for the header preview, I load the page using the same helper method and add it to the view model for my Gadget.&lt;/p&gt;
&lt;div class=&quot;markdown-heading&quot;&gt;
&lt;h2&gt;&lt;img src=&quot;/link/4d30cc76c02042a19babb4b0a96e6982.aspx&quot; /&gt;&lt;/h2&gt;
&lt;h2&gt;Extending IFrameComponent&lt;/h2&gt;
&lt;p&gt;When your user logs into the CMS, they will be given your new Gadget by default.&amp;nbsp; Now you can ensure that only specific roles have access to the Gadget by setting the &amp;nbsp;&lt;code&gt;AllowedRoles&lt;/code&gt;&amp;nbsp;property within the &amp;nbsp;&lt;code&gt;[IFrameComponent]&lt;/code&gt;&amp;nbsp; declaration.&amp;nbsp; If your AddOn allows the developer to define a custom security policy for accessing your module, you cannot simply specify the roles within the attribute.&amp;nbsp; For next version of the Stott Security AddOn, I have created a &lt;code&gt;SecureIFrameComponentAttribute&lt;/code&gt; that inherits the &lt;code&gt;IFrameComponentAttribute&lt;/code&gt; and dynamically resolves the roles that are allowed access to the Gadget based on that security profile.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;[AttributeUsage(AttributeTargets.Class)]
public sealed class SecureIFrameComponentAttribute : IFrameComponentAttribute
{
    public SecureIFrameComponentAttribute() : base()
    {
        try
        {
            var authorizationOptions = ServiceLocator.Current.GetService(typeof(IOptions&amp;lt;AuthorizationOptions&amp;gt;)) as IOptions&amp;lt;AuthorizationOptions&amp;gt;;
            var policy = authorizationOptions?.Value?.GetPolicy(CspConstants.AuthorizationPolicy);
            var roles = policy?.Requirements?.OfType&amp;lt;RolesAuthorizationRequirement&amp;gt;().ToList();
            var roleNames = roles?.SelectMany(x =&amp;gt; x.AllowedRoles).ToList() ?? new List&amp;lt;string&amp;gt; { Roles.WebAdmins, Roles.CmsAdmins, Roles.WebAdmins };

            AllowedRoles = string.Join(&#39;,&#39;, roleNames);
        }
        catch(Exception)
        {
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Telling the CMS Editor Interface About Our AddOn&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;There are two steps to enable the Editor Interface to recognize our AddOn.&amp;nbsp; The first step is to declare our assembly in a&amp;nbsp;&lt;code&gt;module.config&lt;/code&gt;&amp;nbsp;file.&amp;nbsp; Personnally this doesn&#39;t feel like it should be a requirement as all of the information is provided in the&amp;nbsp;&lt;code&gt;[IFrameComponent]&lt;/code&gt;&amp;nbsp;attribute, though it appears that a validation during application startup mandates that this configuration file exists.&amp;nbsp; I suspect this is a requirement tied to much deeper integrations with the UI.&amp;nbsp; E.g. custom DOJO editor code etc.&lt;/p&gt;
&lt;p&gt;Below is an example of a&amp;nbsp;&lt;code&gt;module.config&lt;/code&gt;&amp;nbsp;file. Note the inclusion of an Authorization Policy as an attribute of the module node; this should correspond to the policy required by your AddOn. Additionally, ensure that the full name of the assembly containing your gadget is listed within the assemblies node.&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot; ?&amp;gt;
&amp;lt;module loadFromBin=&quot;true&quot; clientResourceRelativePath=&quot;&quot; viewEngine=&quot;Razor&quot; authorizationPolicy=&quot;MyAddOn:Policy&quot; moduleJsonSerializerType=&quot;None&quot; prefferedUiJsonSerializerType=&quot;Net&quot;&amp;gt;
  &amp;lt;assemblies&amp;gt;
    &amp;lt;add assembly=&quot;MyAddOnAssemblyName&quot; /&amp;gt;
  &amp;lt;/assemblies&amp;gt;

  &amp;lt;clientModule&amp;gt;
    &amp;lt;moduleDependencies&amp;gt;
      &amp;lt;add dependency=&quot;CMS&quot; /&amp;gt;
    &amp;lt;/moduleDependencies&amp;gt;
  &amp;lt;/clientModule&amp;gt;
&amp;lt;/module&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;markdown-heading&quot;&gt;
&lt;p&gt;If you are simply adding a gadget to a specific Optimizely CMS build, the assembly declaration can be included in the&amp;nbsp;&lt;code&gt;module.config&lt;/code&gt;&amp;nbsp;file located at the root of your website application. However, in the context of an AddOn, this declaration should be placed within a protected modules folder, using a path such as&amp;nbsp;&lt;code&gt;[MyCmsWebsite]/modules/_protected/[MyAddOn]/module.config&lt;/code&gt;. There are some extra steps required to achieve this when creating a NuGet package and I address these in Part Three of this series which is focused entirely on the NuGet package process.&lt;/p&gt;
&lt;p&gt;The second step that is needed to inform the CMS of our AddOn is to ensure that is included within the &lt;code&gt;ProtectedModuleOptions&lt;/code&gt;.&amp;nbsp; This can be achieved within a service extensions method that you call within your &lt;code&gt;startup.cs&lt;/code&gt;&amp;nbsp;as follows:&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public static class OptimizelyAddOnServiceExtensions
{
    public static IServiceCollection AddOptimizelyAddOn(this IServiceCollection services)
    {
        services.Configure&amp;lt;ProtectedModuleOptions&amp;gt;(
            options =&amp;gt;
            {
                if (!options.Items.Any(x =&amp;gt; string.Equals(x.Name, &quot;MyAddOnAssemblyName&quot;, StringComparison.OrdinalIgnoreCase)))
                {
                    options.Items.Add(new ModuleDetails { Name = &quot;MyAddOnAssemblyName&quot; });
                }
            });

        return services;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Add the&amp;nbsp;&lt;code&gt;[IFrameComponent]&lt;/code&gt;&amp;nbsp;attribute to your controller to define the gadget for the CMS Editor Interface.&lt;/li&gt;
&lt;li&gt;Add the&amp;nbsp;&lt;code&gt;[Authorize]&lt;/code&gt;&amp;nbsp;attribute to your controller to secure it.&lt;/li&gt;
&lt;li&gt;Add HTTP verb attributes such as&amp;nbsp;&lt;code&gt;[HttpGet]&lt;/code&gt;&amp;nbsp;to your controller actions to prevent unexpected access attempts.&lt;/li&gt;
&lt;li&gt;Add a&amp;nbsp;&lt;code&gt;module.config&lt;/code&gt;&amp;nbsp;file to the&amp;nbsp;&lt;code&gt;[MyCmsWebsite]/modules/_protected/[MyAddOn]/module.config&lt;/code&gt;&amp;nbsp;folder so that CMS can validate your gadget assembly.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;You can find all of my content collated on &lt;a href=&quot;https://www.stott.pro/&quot;&gt;https://www.stott.pro/&lt;/a&gt;&lt;/p&gt;</id><updated>2024-08-30T15:43:33.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Creating an Optimizely AddOn - Getting Started</title><link href="https://world.optimizely.com/blogs/mark-stott/dates/2024/8/creating-an-optimizely-addon---getting-started/" /><id>&lt;p&gt;When Optimizely CMS 12 was launched in the summer of 2021, I created my first AddOn for Optimizely CMS and talked about some of the lessons I learned in my article&amp;nbsp;&lt;a href=&quot;/link/aa901c2390b44fce99cfd6b8838262d5.aspx&quot;&gt;Custom Admin Pages in Optimizely 12&lt;/a&gt;. Three years on, I have two highly refined Optimizely AddOns and have had to resolve numerous challenges.&amp;nbsp; &amp;nbsp;In this new series of articles I will be covering how to create an Optimizely AddOn, how to solve the same challenges I have encountered as well as what I consider to be best practices. You can view examples from across this series within the this&amp;nbsp;&lt;a href=&quot;https://github.com/GeekInTheNorth/OptimizelyAddOnTemplate&quot;&gt;Optimizely AddOn Template&lt;/a&gt;&amp;nbsp;that I have been creating.&lt;/p&gt;
&lt;div class=&quot;markdown-heading&quot;&gt;
&lt;h2&gt;Having a Great Idea&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;The biggest hurdle to getting started with creating an AddOn is having a good idea. But here is the secret to having a great idea; it doesn&#39;t have to be great and it doesn&#39;t have to be original. My first AddOn was a very simple UI for managing robots.txt content within CMS 12 and I already knew a package existed for CMS 11 that had not been updated for a number of years. I&#39;ve now met Mark Everard (the original AddOn author) a number of times and my AddOn has had nearly 200k downloads.&lt;/p&gt;
&lt;p&gt;Your AddOn does not need to be pretty, your end user is a CMS editor or administrator, it needs to be easy to understand and it needs to be functional.&lt;/p&gt;
&lt;p&gt;Finally, whatever idea you come up with, you need to be passionate about it as it will consume a lot of your time. I average roughly a commit per hour, based on this I have spent 145 hours on Stott Robots Handler and 587 hours on Stott Security. When converted to an average working day, that is 19 and 78 days respectively. You may also never make a single penny, there are many great AddOns out there that are open source, running on an MIT license without a license fee.&lt;/p&gt;
&lt;div class=&quot;markdown-heading&quot;&gt;
&lt;h2&gt;Solution Structure&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;When creating an AddOn for Optimizely CMS, I recommend that you package it as a single&amp;nbsp;&lt;a href=&quot;https://learn.microsoft.com/en-us/aspnet/core/razor-pages/ui-class?view=aspnetcore-8.0&amp;amp;tabs=visual-studio&quot;&gt;Razor Class Library (RCL)&lt;/a&gt;. This simplifies the process by consolidating all classes, controllers, razor files, and static content into one NuGet package. This can reduce administrative tasks and potential upgrade issues for users.&amp;nbsp; An RCL also allows consuming websites to override your Razor files if needed, providing flexibility for UI modifications. However, you must carefully manage file paths to avoid conflicts, the best way to do this is to organize all of your Razor files into a specific path within the Views folder, e.g. &amp;nbsp;&lt;code&gt;~/Views/MyAddOn/LandingPage.cshtml&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;In the &lt;a href=&quot;https://github.com/GeekInTheNorth/OptimizelyAddOnTemplate&quot;&gt;Optimizely AddOn Template&lt;/a&gt; repository, the structure of my projects looks like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Sample &lt;em&gt;&lt;strong&gt;(Contains just the CMS to test against)&lt;/strong&gt;&lt;/em&gt;
&lt;ul&gt;
&lt;li&gt;SampleCms.sln&lt;/li&gt;
&lt;li&gt;nuget.config&lt;/li&gt;
&lt;li&gt;SampleCms
&lt;ul&gt;
&lt;li&gt;SampleCms.csproj &lt;em&gt;&lt;strong&gt;(Web Project)&lt;/strong&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;... Other folders and files for just the Sample CMS&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;src &lt;em&gt;&lt;strong&gt;(contains the source code for the AddOn)&lt;/strong&gt;&lt;/em&gt;
&lt;ul&gt;
&lt;li&gt;OptimizelyAddOn.sln&lt;/li&gt;
&lt;li&gt;nuget.config&lt;/li&gt;
&lt;li&gt;OptimizelyAddOn
&lt;ul&gt;
&lt;li&gt;OptimizelyAddOn.csproj &lt;em&gt;&lt;strong&gt;(Razor Class Library)&lt;/strong&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;Views
&lt;ul&gt;
&lt;li&gt;OptimizelyAddOn
&lt;ul&gt;
&lt;li&gt;Administration
&lt;ul&gt;
&lt;li&gt;Index.cshtml&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Gadget
&lt;ul&gt;
&lt;li&gt;Index.cshtml&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;... Other folders and files that make up the AddOn functionality&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;OptimizelyAddOn.Tests
&lt;ul&gt;
&lt;li&gt;OptimizelyAddOn.Tests.csproj &lt;em&gt;&lt;strong&gt;(Test Class Library)&lt;/strong&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;... Other folders and files for organised unit tets.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Please note that there is a one-to-one relationship between projects and NuGet packages; each project you add will require its own NuGet package.&amp;nbsp; Lets say you have one project called MyAddOn.Core and a second project called MyAddOn.Optimizely, you will end up with two NuGet packages: MyAddOn.Core.nupkg and MyAddOn.Optimizely.nupkg.&amp;nbsp; When developing these, you can use project references and when you package them into nuget files, the project references will automatically be converted into package dependencies.&lt;/p&gt;
&lt;div class=&quot;markdown-heading&quot;&gt;
&lt;h2&gt;JavaScript and Stylesheets In Secure Systems&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;When I initially developed the user interface for the&amp;nbsp;&lt;a href=&quot;https://github.com/GeekInTheNorth/Stott.Optimizely.RobotsHandler&quot;&gt;Stott Robots Handler&lt;/a&gt;&amp;nbsp;AddOn, I utilized externally hosted JQuery and Bootstrap JavaScript (JS) and CSS resources. This approach reduced the complexity of launching my AddOn and enabled a rapid entry to the market. I later undertook a comprehensive refactoring of the UI, rebuilding it with React and delivering optimized JS and CSS files directly packaged with the AddOn. This transition ensured that the UI adhered to best architectural and security practices.&lt;/p&gt;
&lt;p&gt;When designing a UI for your own AddOn, you will likely encounter similar JavaScript (JS) and stylesheet (CSS) requirements. Both of these elements come with inherent security concerns, particularly in environments governed by a&amp;nbsp;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP&quot;&gt;Content Security Policy (CSP)&lt;/a&gt;. A CSP serves as an allowlist of domains and specifies their permissions on your site. To ensure compatibility and maintain robust security, consider the following guidelines:&lt;/p&gt;
&lt;p&gt;Ideally, you should build and distribute optimized and compiled JS and CSS files within your AddOn package within the&amp;nbsp;&lt;code&gt;wwwroot&lt;/code&gt;&amp;nbsp;folder of your&amp;nbsp;&lt;a href=&quot;https://learn.microsoft.com/en-us/aspnet/core/razor-pages/ui-class?view=aspnetcore-8.0&amp;amp;tabs=visual-studio&quot;&gt;Razor Class Library (RCL)&lt;/a&gt;. This approach enables the CSP to utilize the&amp;nbsp;&lt;code&gt;&#39;self&#39;&lt;/code&gt;&amp;nbsp;source directive, allowing your scripts and styles to be safely executed and applied.&lt;/p&gt;
&lt;p&gt;Avoid using inline style attributes and JavaScript event handlers. Instead, attach styles and behaviors through classes defined within your JS and CSS files. This practice aligns more closely with CSP standards and significantly enhances security. It is important to note that inline Style attributes and JavaScript event attributes cannot be secured using a&amp;nbsp;&lt;code&gt;nonce&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Ensure that the&amp;nbsp;&lt;code&gt;nonce&lt;/code&gt;&amp;nbsp;attribute of all&amp;nbsp;&lt;code&gt;script&lt;/code&gt;&amp;nbsp;and&amp;nbsp;&lt;code&gt;style&lt;/code&gt;&amp;nbsp;elements are populated with a value provided by Optimizely CMS&amp;rsquo;s&amp;nbsp;&lt;code&gt;ICspNonceService&lt;/code&gt;&amp;nbsp;interface. For further information, refer to Optimizely&#39;s&amp;nbsp;&lt;a href=&quot;https://docs.developers.optimizely.com/content-management-system/docs/content-security-policy&quot;&gt;Content Security Policy&lt;/a&gt;&amp;nbsp;documentation. Additionally, security AddOns, such as&amp;nbsp;&lt;a href=&quot;https://github.com/GeekInTheNorth/Stott.Security.Optimizely&quot;&gt;Stott Security&lt;/a&gt;, automatically configure the&amp;nbsp;&lt;code&gt;ICspNonceService&lt;/code&gt;&amp;nbsp;and integrate it into the CSP for you.&lt;/p&gt;
&lt;p&gt;If you choose to utilize JS and CSS resources hosted by third parties, additional precautions are necessary. Ensure that your script and link tags include a&amp;nbsp;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity&quot;&gt;Subresource Integrity&lt;/a&gt;&amp;nbsp;(SRI) attribute. This attribute enables the browser to verify that the files have not been tampered with by checking them against a specified checksum. Bear in mind that each external resource you employ may require the site consuming your AddOn to adjust its CSP settings to accommodate these resources. Consequently, allowing third-party resources for your AddOn UI could inadvertently permit those resources site-wide.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Please note that at the time of writing, Optimizely does not support the&amp;nbsp;&lt;code&gt;nonce&lt;/code&gt;&amp;nbsp;attribute for it&#39;s Editor or Admin interface, but there is an intention to correct this. Please do not be the developer that stops this from being adopted further down the line.&lt;/em&gt;&lt;/p&gt;
&lt;div class=&quot;markdown-heading&quot;&gt;
&lt;h2&gt;Extending The Menu&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;If your AddOn has it&#39;s own interface, then you will want to expose that interface to your users by creating a class that inherits&amp;nbsp;&lt;code&gt;IMenuProvider&lt;/code&gt;&amp;nbsp;that is also decorated with&amp;nbsp;&lt;code&gt;[MenuProvider]&lt;/code&gt;&amp;nbsp;attribute. Optimizely will automatically identify these classes and use them to extend the menu.&lt;/p&gt;
&lt;p&gt;In the following example, I am returning a single&amp;nbsp;&lt;code&gt;UrlMenuItem&lt;/code&gt;&amp;nbsp;which takes three parameters: the name of the link within the menu, the path within the menu and the MVC controller route for where my interface exists. I am then extending this to say that it is always available and sorting this to the end of the list of menu items. I am also defining the authorization policy required to access this menu item.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;[MenuProvider]
public sealed class ExampleMenuProvider : IMenuProvider
{
    public IEnumerable&amp;lt;MenuItem&amp;gt; GetMenuItems()
    {
        return new UrlMenuItem(
          &quot;My AddOn&quot;,
          &quot;/global/cms/my.addon&quot;,
          &quot;/my-addon-controller-route/&quot;)
        {
            IsAvailable = context =&amp;gt; true,
            SortIndex = SortIndex.Last + 1,
            AuthorizationPolicy = &quot;required.security.policy&quot;
        };
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you start the menu path with&amp;nbsp;&lt;code&gt;global&lt;/code&gt;&amp;nbsp;then your AddOn will become visible within the module selector. If you start it with&amp;nbsp;&lt;code&gt;global/cms&lt;/code&gt;, then your AddOn will become visible under AddOns in the left hand menu of the CMS.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/551d4511819d481e9ad7ee4d5ebf49f3.aspx?1725023387612&quot; alt=&quot;Left Hand Menu in Optimizely CMS&quot; width=&quot;412&quot; height=&quot;680&quot; /&gt;&lt;/p&gt;
&lt;p&gt;If you have multiple menu items that you want to present in a hierarchial fashion, then you can simply return multiple UrlMenuItems, making sure to define the paths for the child menu items under their parent.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;[MenuProvider]
public class ExampleMenuProvider : IMenuProvider
{
    public IEnumerable&amp;lt;MenuItem&amp;gt; GetMenuItems()
    {
        yield return new UrlMenuItem(
          &quot;My Addon Parent&quot;,
          &quot;/global/cms/myaddon.menu.example&quot;,
          &quot;/my-addon-controller-route/parent/&quot;)
        {
            IsAvailable = context =&amp;gt; true,
            SortIndex = SortIndex.Last + 1,
            AuthorizationPolicy = &quot;required.security.policy&quot;
        };

        yield return new UrlMenuItem(
          &quot;My Addon Child One&quot;,
          &quot;/global/cms/myaddon.menu.example/child.one&quot;,
          &quot;/my-addon-controller-route/child-one/&quot;)
        {
            IsAvailable = context =&amp;gt; true,
            SortIndex = SortIndex.Last + 1,
            AuthorizationPolicy = &quot;required.security.policy&quot;
        };

        yield return new UrlMenuItem(
          &quot;My Addon Child Two&quot;,
          &quot;/global/cms/myaddon.menu.example/child.two&quot;,
          &quot;/my-addon-controller-route/child-two/&quot;)
        {
            IsAvailable = context =&amp;gt; true,
            SortIndex = SortIndex.Last + 2,
            AuthorizationPolicy = &quot;required.security.policy&quot;
        };
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Parent and child menu items will manifest as it&#39;s own menu when it opens and this adopts the same style as the administrator interface. Please note that this consumes an additional 120 pixels of horizonal real estate and you may want to override the styles on your pages. If your interface is built as a single page application, then you can set the child menus to have the same URL as the parent but with anchor tags and toggle UI visibility based on this.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/bf620391c58a4404b5e7fd12c644859f.aspx?1725023398544&quot; alt=&quot;Left Hand Menu in Optimizely CMS&quot; width=&quot;1050&quot; height=&quot;512&quot; /&gt;&lt;/p&gt;
&lt;div class=&quot;markdown-heading&quot;&gt;
&lt;h2&gt;Handling Authentication&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;If your AddOn UI is intended to be accessible to editors, then you can decorate your controllers with the&amp;nbsp;&lt;code&gt;[Authorize(Roles = Roles.CmsEditors)]&lt;/code&gt;&amp;nbsp;attribute. Likewise if you want to make the UI available to CMS Administrators, then you can decorate your controller with&amp;nbsp;&lt;code&gt;[Authorize(Roles = Roles.CmsAdmins)]&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Depending on the Domain of your AddOn, you may wish to provide your consumers with the ability to fine tune access to your AddOn. For&amp;nbsp;&lt;a href=&quot;https://github.com/GeekInTheNorth/Stott.Optimizely.RobotsHandler&quot;&gt;Stott Robots Handler&lt;/a&gt;&amp;nbsp;and&amp;nbsp;&lt;a href=&quot;https://github.com/GeekInTheNorth/Stott.Security.Optimizely&quot;&gt;Stott Security&lt;/a&gt;&amp;nbsp;I wanted to give consumers the ability to grant access to the AddOns to specific users such as an SEO Adminstrator or a Developer without granting that user access to the CMS Administrator interface. In order to do this, you will want to include the ability to define the authentication policy for your AddOn within your service extension:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public static IServiceCollection AddMyAddOn(
    this IServiceCollection services,
    Action&amp;lt;AuthorizationOptions&amp;gt;? authorizationOptions = null)
{
    // Authorization
    if (authorizationOptions != null)
    {
        services.AddAuthorization(authorizationOptions);
    }
    else
    {
        var allowedRoles = new List&amp;lt;string&amp;gt; { &quot;CmsAdmins&quot;, &quot;Administrator&quot;, &quot;WebAdmins&quot; };
        services.AddAuthorization(authorizationOptions =&amp;gt;
        {
            authorizationOptions.AddPolicy(&quot;My.AddOn.Policy.Name&quot;, policy =&amp;gt;
            {
                policy.RequireRole(allowedRoles);
            });
        });
    }

    return services;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you have made this optional, then the consumer can choose to use your defaults or define the policy themselves within their startup.cs:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;// Use Default Authentication Policy
services.AddMyAddOn();

// Use Custom Authentication Policy
services.AddMyAddOn(authorizationOptions =&amp;gt; 
{
    authorizationOptions.AddPolicy(&quot;My.AddOn.Policy.Name&quot;, policy =&amp;gt;
    {
        // This line is required if you are using Opti Id
        policy.AddAuthenticationSchemes(OptimizelyIdentityDefaults.SchemeName);

        // This defines the roles required for this policy
        policy.RequireRole(&quot;WebAdmins&quot;, &quot;SeoAdmins&quot;);
    });
});&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;All of your controllers should then instead be decorated with the&amp;nbsp;&lt;code&gt;[Authorize(Policy = &quot;My.AddOn.Policy.Name&quot;)]&lt;/code&gt;&amp;nbsp;attribute and menu items within your&amp;nbsp;&lt;code&gt;IMenuProvider&lt;/code&gt;&amp;nbsp;should also be created with the same policy name.&lt;/p&gt;
&lt;div class=&quot;markdown-heading&quot;&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Your AddOn does not need to be unique or grand.&lt;/li&gt;
&lt;li&gt;You will need to be passionate about your AddOn.&lt;/li&gt;
&lt;li&gt;Do try to contain your AddOn into a single Razor Class Library to make installs and updates easier.&lt;/li&gt;
&lt;li&gt;Do try to support a stricter Content Security Policy by:
&lt;ul&gt;
&lt;li&gt;Compiling and package your JavaScript and Stylesheets as part of the AddOn.&lt;/li&gt;
&lt;li&gt;Attach styles and events using classes referenced by your JavaScript and Stylesheets.&lt;/li&gt;
&lt;li&gt;Use&amp;nbsp;&lt;code&gt;ICspNonceService&lt;/code&gt;&amp;nbsp;to add a&amp;nbsp;&lt;code&gt;nonce&lt;/code&gt;&amp;nbsp;attribute to your script and style elements.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Add items into the menu by implementing&amp;nbsp;&lt;code&gt;IMenuProvider&lt;/code&gt;&amp;nbsp;and decorating it with&amp;nbsp;&lt;code&gt;[MenuProvider]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Do secure all of your controllers to ensure users have to have access to the CMS Interface.
&lt;ul&gt;
&lt;li&gt;Consider using your own Authentication Policy if you want consumers to be able to apply more specific restrictions.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;You can find all of my content collated on &lt;a href=&quot;https://www.stott.pro/&quot;&gt;https://www.stott.pro/&lt;/a&gt;&lt;/p&gt;</id><updated>2024-08-28T11:55:55.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Stott Security Version 2 So Far</title><link href="https://world.optimizely.com/blogs/mark-stott/dates/2024/5/stott-security-version-2-so-far/" /><id>&lt;p&gt;In December 2023, I unveiled the initial version of Stott Security version 2. Although I typically announce each version I release on LinkedIn and the GitHub repository, these announcements have often been concise, lacking in-depth explanations. Let&amp;rsquo;s delve into that now.&lt;/p&gt;
&lt;h2&gt;Cross-Origin Resource Sharing Headers&lt;/h2&gt;
&lt;p&gt;Cross-Origin Resource Sharing (CORS) headers serve as your web server&amp;rsquo;s means of specifying which domains, other than its own, are permitted to access its APIs. If you aim to bolster your website&amp;rsquo;s security, implementing both CORS and CSP is advisable, as they safeguard your website from different angles. The UI for managing CORS within Stott Security can be seen below:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/3b63cc6b882c41749f2265f95670c04b.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Interestingly, integrating a CMS-editable CORS policy turned out to be simpler than anticipated. In the .NET Core framework, there exists a default implementation of the&amp;nbsp;&lt;strong&gt;ICorsPolicyProvider&lt;/strong&gt;&amp;nbsp;interface, which is automatically injected unless a custom implementation is provided. Leveraging this, my implementation extends the default provider and its interface. Upon a request, if a specific policy has been applied to the route, it&amp;rsquo;s loaded using the default implementation. Conversely, when no policy name is specified, the CORS policy defined by the module is loaded. The invocation of the custom implementation adheres to the standard protocol within .NET Core.&lt;/p&gt;
&lt;p&gt;For further insights into the original CORS implementation within Stott Security, refer to&amp;nbsp;&lt;a href=&quot;/link/a539beae799741f886a37cb0f383aec3.aspx&quot;&gt;Adding CORS Management to Optimizely CMS 12&lt;/a&gt;. Notably, this functionality has been updated to accommodate both fixed CORS policies in the code and the globally utilized policy by Stott Security.&lt;/p&gt;
&lt;h2&gt;Improved Source and Violation Filtering&lt;/h2&gt;
&lt;p&gt;When I initially developed Stott Security, I didn&amp;rsquo;t foresee the sheer volume of sources that would be added to each site. It&amp;rsquo;s become apparent that some implementations contain upwards of 50 sources or more, making the list difficult to navigate. Additionally, the CSP Violation List can become overwhelming with potentially hundreds of violated sources.&lt;/p&gt;
&lt;p&gt;To enhance user experience on these tabs, I&amp;rsquo;ve introduced the option to filter the list based on partial matches in the Source URL and the desired Directive. This feature aims to streamline navigation and improve usability for users dealing with extensive lists. The free text filter will filter the list of sources to just those sources that contain the term entered. The drop down will filter the sources to just those which have been granted access to the specifically chosen directive; this can make it a lot easier to see which sources can perform a specific action.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/7a393ff5c583480bafd200ba47dd2a86.aspx&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Language Updates&lt;/h2&gt;
&lt;p&gt;I decided to rename the feature formerly known as &amp;ldquo;Remote CSP Whitelist&amp;rdquo; to &amp;ldquo;Remote CSP Allowlist&amp;rdquo;. This choice reflects a shift towards more inclusive and descriptive terminology, opting for terms like Allowlist and Blocklist which are more overt in their meaning.&lt;/p&gt;
&lt;p&gt;There is a lot of history to the terms &amp;ldquo;Whitelist&amp;rdquo; and &amp;ldquo;Blacklist&amp;rdquo; and often it is claimed that these have origins in trust and criminality respectively. Blacklisting however has a darker history where it was used as a means of oppression with individuals being unable to gain employment for performing acts that were either not approved of by their employers (such as striking) essentially leading to them being pushed into poverty. Overall in the english language, where &amp;ldquo;White&amp;rdquo; is used in a term it is often associated with &amp;ldquo;Good&amp;rdquo; while &amp;ldquo;Black&amp;rdquo; is associated with &amp;ldquo;Bad&amp;rdquo;. Irrespective of the origin of these terms, the continued use of these terms is a form of micro-aggression that continues to reinforce racial divides and systemic racism. I do not pretend to be in a position of Authority on subject matters like this. How can I be when I am not the victim of racism? How can I tell you how that makes another person feel? What I want to do here is continue to learn and to try to show my fellow humans the respect they deserve while making my software as user friendly and intiuitive as possible.&lt;/p&gt;
&lt;p&gt;The switch to Allowlist and Blocklist is a good move in terms of clarity. The terms Whitelist and Blacklist may not be immediately comprehensible to those unfamiliar with them as they rely on simple color prefixes. In a casual discussion with a lay-person, I found that while the concept of a Blacklist was quickly grasped, the Whitelist prompted confusion and required explanation. On the other hand, when I asked about Allowlist and Blocklist, they swiftly understood their meanings without ambiguity or prior knowledge.&lt;/p&gt;
&lt;p&gt;Microsoft has already embraced the language of Allowlist and Blocklist. This is immediately apparant in articles such as&amp;nbsp;&lt;a href=&quot;https://learn.microsoft.com/en-us/deployedge/microsoft-edge-security-endpoints&quot;&gt;Allow list for Microsoft Edge endpoints&lt;/a&gt;&amp;nbsp;and&amp;nbsp;&lt;a href=&quot;https://learn.microsoft.com/en-gb/azure/azure-sql/database/firewall-configure?view=azuresql&quot;&gt;Azure SQL Database and Azure Synapse IP firewall rules&lt;/a&gt;. Google is another one of the big players that have started to use this newer language as we can see from their documentation such as&amp;nbsp;&lt;a href=&quot;https://support.google.com/a/answer/60752?hl=en&quot;&gt;Allowlists, denylists, and approved senders&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Customisable Reporting Endpoints&lt;/h2&gt;
&lt;p&gt;Stott Security has provided support for internal reporting endpoints since its inception, but the development journey for this feature has taken various paths. Initially, it employed a view component that generated a JavaScript tag to intercept browser events and transmit violations to the module&amp;rsquo;s endpoint. Later, I modified this to align with standard definitions for the report-uri and report-to schema.&lt;/p&gt;
&lt;p&gt;The community using the module requested the ability to deactivate internal endpoints and define external ones. As a result, the option to disable internal endpoints was introduced. While this reduces traffic to the CMS instance, it also prevents any further updates to the violation report screen and renders the Agency Allowlist functionality inactive. Additionally, the functionality to specify external endpoints for report-uri and report-to was added, although this data remains unused by the module.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m interested in exploring options to integrate the violation report screen with external endpoints. However, this may necessitate the creation of a distinct reporting service.&lt;/p&gt;
&lt;h2&gt;CSP NONCE Support&lt;/h2&gt;
&lt;p&gt;A nonce is defined as a word or phrase that is meant to be used exactly once. For a Content Security Policy, the nonce is a code that should be used once. The browser will limit the execution of a script tag or style tag to just once per nonce value. This can protect the website and it&amp;rsquo;s users from replay attacks.&lt;/p&gt;
&lt;p&gt;Stott Security was designed to protect the entire CMS website (both front and back end). The Optimizely CMS interface is however incompatabile with nonce values and the use of a nonce within the CMS interface will outright block the entire UI. In version 1.x of Stott Security, the nonce functionality was omitted for this reason. In version 2.3 of Stott Security, the nonce functionality was added into the solution. However due to a continued lack of support for nonce in the CMS interface, the Content Security Policy is instead generated both with and without a nonce depending on whether you are visiting a content page or attempting to visit the CMS interface.&lt;/p&gt;
&lt;h2&gt;Preview Headers&lt;/h2&gt;
&lt;p&gt;One downside of designing a UI for administrator ease of use is that it hides the fully compiled list of headers from the administrator. To address this, a new tab has been added to the interface, displaying the compiled list of HTTP Headers included with each request. This allows technical administrators to review the generated headers more thoroughly. Additionally, the same API powering this tab can be accessed for headless solutions.&lt;/p&gt;
&lt;p&gt;Excluding the Cross Origin Resource Sharing (CORS) headers from this list was a deliberate choice for two primary reasons. Firstly, I defer to the underlying framework to generate these headers dynamically in response to requests, rather than manually generating them. Secondly, certain headers are exclusively visible during pre-flight requests and may vary depending on the origin of the request.&lt;/p&gt;
&lt;h2&gt;Import &amp;amp; Export Functionality&lt;/h2&gt;
&lt;p&gt;The user base for the module requested the ability to import and export configurations, with a common scenario being the need to transfer CSP configurations from a Preproduction environment to Production. Users may also wish to export settings before making and testing changes and have the option to revert to the last known good configuration.&lt;/p&gt;
&lt;p&gt;This feature allows exporting all configurations as a JSON file, which can be downloaded and subsequently uploaded into the desired environment. Similar to any changes made directly within the user interface, the import process is fully audited as it may involve various actions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Creating or updating Content Security Policy settings&lt;/li&gt;
&lt;li&gt;Creating, updating, or deleting Content Security Policy sources&lt;/li&gt;
&lt;li&gt;Creating or updating Cross Origin Resource Policy settings&lt;/li&gt;
&lt;li&gt;Creating or updating miscellaneous response headers.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Improved Performance&lt;/h2&gt;
&lt;p&gt;I received a report concerning performance issues occurring in production with the Stott Security module. Notable stability concerns were raised regarding the instantiation of numerous DB Contexts, resulting in connection limit breaches. Optimizely typically configures the Production environment with the most conservative setup initially, scaling up as necessary. This strategy is to keep the costs down on the DXP platform. However, in this instance, the limit of around 100 concurrent connections was surpassed, indicating that the database may be scaled too low for a large client. In response, Optimizely temporarily resolved the issue by scaling up the database instance.&lt;/p&gt;
&lt;p&gt;Upon receiving this report, I introduced additional logging into the DB context and key repositories to monitor instantiation frequency. Swiftly, I pinpointed areas for significant performance enhancement:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Implemented caching for the CSP Settings Service.&lt;/li&gt;
&lt;li&gt;Replaced usages of the CSP Settings Repository with the Service.
&lt;ul&gt;
&lt;li&gt;The repository handles data access while the service coordinates with the caching layer and repository.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Extended cache duration from 1 hour to 12.&lt;/li&gt;
&lt;li&gt;Implemented lazy instantiation for select repositories and the DB Context.
&lt;ul&gt;
&lt;li&gt;This prevented unnecessary instantiation of the DB Context unless needed.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In testing, there was a minimum reduction of 95% in all DB Context instantiation and database query calls. Deployment of these enhancements to the affected client resolved the stability issues. Within a week of resolving this issue, a second report surfaced, and I promptly advised the client to update the module within their solution.&lt;/p&gt;
&lt;h2&gt;Release Notes&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/GeekInTheNorth/Stott.Security.Optimizely/discussions/157&quot;&gt;Version 2.1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/GeekInTheNorth/Stott.Security.Optimizely/discussions/171&quot;&gt;Version 2.2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/GeekInTheNorth/Stott.Security.Optimizely/discussions/178&quot;&gt;Version 2.3&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/GeekInTheNorth/Stott.Security.Optimizely/discussions/181&quot;&gt;Version 2.4&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/GeekInTheNorth/Stott.Security.Optimizely/discussions/201&quot;&gt;Version 2.5&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/GeekInTheNorth/Stott.Security.Optimizely/discussions/207&quot;&gt;Version 2.6&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;You can find all of my content collated on &lt;a href=&quot;https://www.stott.pro/&quot;&gt;https://www.stott.pro/&lt;/a&gt;&lt;/p&gt;</id><updated>2024-05-22T09:18:31.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Working Programmatically With List Block properties</title><link href="https://world.optimizely.com/blogs/mark-stott/dates/2023/10/working-programmatically-with-list-block-properties/" /><id>&lt;p&gt;Recently I encountered some issues with a third party plugin with the latest version of Optimizely CMS 12. As the Go Live clock was ticking down fast for a client, we couldn&#39;t afford to wait for a fix to the third party plugin and I re-modelled the content to use a block list property instead. Unfortunatley there were a significant number of instances of the content type that needed to be remodelled so it was decided that we would have to migrate the client&#39;s content for them rather than have the client bear the burden.&lt;/p&gt;
&lt;p&gt;The built in Optimizely Migration Step functionality is really good for when you want to do something like rename a content type or property, but if that property is an entirely different type, then you have to manage this change yourself.&lt;/p&gt;
&lt;p&gt;As I wanted to migrate the old property onto the new property and the old property was a complex object, I chose to hide the old property rather than remove it so I would have full access to it&#39;s structure.&amp;nbsp; To do this I added the &lt;strong&gt;[ScaffoldColumn(false)]&lt;/strong&gt; attribute to the property which tells the CMS Interface not to render the property to the CMS Editor.&amp;nbsp; I also added the &lt;strong&gt;[Obsolete] &lt;/strong&gt;attribute, technically I didn&#39;t need to, but it highlights to other developers that the property should be removed and it shows up in tools such as SonarCloud as a reminder to remove the property later on.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;[Display(Name = &quot;New Field Name&quot;)]
[MaxElements(20)]
public virtual IList&amp;lt;MyNewBlock&amp;gt;? NewField { get; set; }

[Obsolete(&quot;Remove this property after deployment to prod has migrated this property to &#39;New Field Name&#39;.&quot;)]
[ScaffoldColumn(false)]
public virtual ThirdPartyPackageProperty? OldField { get; set; }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I then created a Migration Step class that inherits the Optimizely &lt;strong&gt;MigrationStep &lt;/strong&gt;and I added an override for the &lt;strong&gt;AddChanges()&lt;/strong&gt; method.&amp;nbsp; When making a Migration Step, I typically keep this method small and focused around catching and handling errors.&amp;nbsp; When the application starts up, Optimizely attempts to perform the migration step before it creates any new property types; this means that the first time this migration step is executed will result in a failure.&amp;nbsp; By catching and swallowing the error I can allow the first start up of the site to succeed and generate the new properties before triggering a second application restart so that the actual migration can take place.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public sealed class MyMigrationStep : MigrationStep
{
    public override void AddChanges()
    {
        try
        {
            MigratePages();
        }
        catch (Exception ex)
        {
            var logger = ServiceLocator.Current.GetInstance&amp;lt;ILogger&amp;lt;MyMigrationStep&amp;gt;&amp;gt;();
            logger.LogError(ex, &quot;Failure encountered when attempting to migrate the content type.&quot;);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;strong&gt;MigratePages()&lt;/strong&gt; method then uses the &lt;strong&gt;IContentTypeRepository&lt;/strong&gt; to load the &lt;strong&gt;ContentType&lt;/strong&gt; definition for the content type I want to perform the migration on.&amp;nbsp; I then pass this into an instance of&amp;nbsp; &lt;strong&gt;IContentModelUsage&lt;/strong&gt; which will then return a complete list of every language and version of that content type in a content usage model.&amp;nbsp; I want to convert all versions of every instance of my page type to allow for CMS Editors to compare across historical versions of the content as they will no longer be able to access the old property.&amp;nbsp; I then loop through each content usage and load the full content version using the TryGet method of the &lt;strong&gt;IContentRepository&lt;/strong&gt;.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;private static void MigratePages()
{
	var contentTypeRepository = ServiceLocator.Current.GetInstance&amp;lt;IContentTypeRepository&amp;gt;();
	var contentModelUsage = ServiceLocator.Current.GetInstance&amp;lt;IContentModelUsage&amp;gt;();
	var contentRepository = ServiceLocator.Current.GetInstance&amp;lt;IContentRepository&amp;gt;();

	var contentType = contentTypeRepository.Load(typeof(ExistingPageToChange));
	var usages = contentModelUsage.ListContentOfContentType(contentType);

	foreach (var contentUsage in usages)
	{
		if (contentRepository.TryGet&amp;lt;ExistingPageToChange&amp;gt;(
				contentUsage.ContentLink,
				new CultureInfo(contentUsage.LanguageBranch),
				out var ExistingPageToChange))
		{
			MigratePage(contentRepository, ExistingPageToChange);
		}
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;strong&gt;MigratePage &lt;/strong&gt;method starts off with a little protection to make sure that if the new property is only updated if the new property does not have a value and the old property does have a value.&amp;nbsp; As this migration might run multiple times, you may want to add a boolean to your content type which indicates if a migration has already been performed, but in my case I already had a plan to remove the old properties and the migration step in a rapid follow up release.&lt;/p&gt;
&lt;p&gt;When you retrieve a piece of content from IContentLoader or IContentRepository, the object model is in a read only state. &amp;nbsp;In order to edit a piece of content, you first have to create a writeable clone by calling the &lt;strong&gt;CreateWriteableClone()&lt;/strong&gt; method against content item.&amp;nbsp; This method exists upon the &lt;strong&gt;PageData&lt;/strong&gt;&lt;strong&gt;&amp;nbsp;&lt;/strong&gt;object and clones the content item in a writable state, but the method has a return type of &lt;strong&gt;PageData &lt;/strong&gt;so you will have to recast it as the type you are editing.&amp;nbsp; I then have a method called ConvertProperty that takes the old collection property and generates the new block list property.&amp;nbsp; To avoid casting issues when saving, I implicitly set the variable as an &lt;strong&gt;IList&amp;lt;NewPropertyBlock&amp;gt;&lt;/strong&gt; before setting the property.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;private static void MigratePage(IContentRepository contentRepository, ExistingPageToChange instance)
{
	var requiresMigration = instance.NewListBlockProperty.IsNullOrEmpty() &amp;amp;&amp;amp; !instance.OldThirdPartyProperty.IsNullOrEmpty();
	if (!requiresMigration)
	{
		return;
	}

	var editableVersion = (ExistingPageToChange)instance.CreateWritableClone();

	// The variable has to be an IList&amp;lt;&amp;gt; in order to avoid a casting error.
	IList&amp;lt;NewPropertyBlock&amp;gt; list = ConvertProperty(contentRepository, instance.ContentLink, editableVersion.OldThirdPartyProperty).ToList();
	editableVersion.NewListBlockProperty = list;

	contentRepository.Save(editableVersion, SaveAction.Patch, AccessLevel.NoAccess);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Even though &lt;strong&gt;IList&amp;lt;Block&amp;gt;&lt;/strong&gt; properties are saved as part of the &lt;strong&gt;PageData &lt;/strong&gt;and not as separate content, it&#39;s still important to use the Content Repository to set up a default writable instance of the blocks within the collection.&amp;nbsp; If you just instantiate them as &lt;strong&gt;new NewPropertyBlock()&lt;/strong&gt; then you will get an error when saving the block list against the page.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;private static IEnumerable&amp;lt;NewPropertyBlock&amp;gt; ConvertProperty(
	IContentRepository contentRepository,
	ContentReference parentReference,
	ThirdPartyPackageProperty? oldPropertyList)
{
	if (oldPropertyList is not { Count: &amp;gt; 0 })
	{
		yield break;
	}

	foreach (var oldProperty in oldPropertyList)
	{
		var NewPropertyBlock = contentRepository.GetDefault&amp;lt;NewPropertyBlock&amp;gt;(parentReference);
		NewPropertyBlock.Link = new LinkItem
		{
			Href = oldProperty.Href,
			Title = oldProperty.Title,
			Target = oldProperty.Target,
			Text = oldProperty.Text
		};
		NewPropertyBlock.HoverImage = oldProperty.HoverImage;

		yield return NewPropertyBlock;
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The final step after finishing your edits to the content is to save the content back to the database.&amp;nbsp; Because I was aiming to update all content versions to the new model without creating new content versions I had to call the save function as follows:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;contentRepository.Save(editableVersion, SaveAction.Patch, AccessLevel.NoAccess);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;SaveAction.Patch updates the existing version of the content without creating a new version or triggering any validation.&amp;nbsp; As the save is being performed outside of the context of a user action, I had to pass in AccessLevel.NoAccess as the minimum access rights needed for the save to complete.&amp;nbsp; Had I passed in AccessLevel.Publish, then the save action would have to take place as part of a user action where the user had &quot;publish&quot; permissions to the content.&lt;/p&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;If you are changing the name of a property because it&#39;s type is the same
&lt;ul&gt;
&lt;li&gt;Create a MigrationStep and add a line to the AddChanges() method like this:
&lt;ul&gt;
&lt;li&gt;ContentType(nameof(MyContentType)).Property(nameof(MyContentType.NewPropertyName)).UsedToBeNamed(&quot;OldPropertyName&quot;);&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;If you are changing the type of the property:
&lt;ul&gt;
&lt;li&gt;Give the new property an entirely new name, this will prevent casting issues with the property on the content type.&lt;/li&gt;
&lt;li&gt;Create a MigrationStep for handling the property type change.
&lt;ul&gt;
&lt;li&gt;Use IContentTypeRepository to get the content type&lt;/li&gt;
&lt;li&gt;Use IContentModelUsage to get all instances of the content type&amp;nbsp;&lt;/li&gt;
&lt;li&gt;Use IContentRepository to create default instances of content types before creating new instances of a content type.&lt;/li&gt;
&lt;li&gt;Use MyContentType.CreateWriteableClone() to get an editable version of the content you are changing.&lt;/li&gt;
&lt;li&gt;Make sure you cast properties to the correct types when assigning them.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</id><updated>2023-11-23T13:05:54.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Adding CORS Management to Optimizely CMS 12</title><link href="https://world.optimizely.com/blogs/mark-stott/dates/2023/10/adding-cors-management-to-optimizely-cms-12/" /><id>&lt;p&gt;In December 2021, I started working on a new add-on for Optimizely CMS 12. This add-on introduced a new way to manage the Content Security Policy within the CMS that was designed to be more accessible to non-technical people. This Add-on allowed the CMS Administrator to define on an origin-by-origin basis what origin was allowed to do what action with the website with every change being fully audited. This Add-on is now live on a number of Optimizely CMS websites and has gone through a number of penetration tests.&lt;/p&gt;
&lt;p&gt;I planned the next evolution of this Add-on to include the ability to manage Cross-origin Resource Sharing (CORS) headers from within the CMS administration interface. CORS allows an API or Website to define what third parties can consume the resource as well as how they can consume the resource. This can be an important security requirement if you are building a Headed or Hybrid CMS that exposes endpoints to be consumed by another website.&lt;/p&gt;
&lt;p&gt;The intent for this update to the Stott Security add-on was to allow the CMS editor to set all of the following headers in an easy to manage interface:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials&quot;&gt;Access-Control-Allow-Credentials&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers&quot;&gt;Access-Control-Allow-Headers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods&quot;&gt;Access-Control-Allow-Methods&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin&quot;&gt;Access-Control-Allow-Origin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers&quot;&gt;Access-Control-Expose-Headers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age&quot;&gt;Access-Control-Max-Age&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I have introduced a new tab into the Stott Security Add-on named &amp;ldquo;CORS&amp;rdquo; that allows you to edit and save all of these headers together.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://geekinthenorth.github.io/assets/StottSecurityCorsUi.png&quot; alt=&quot;CORS Interface within the Stott Security Add-on&quot; /&gt;&lt;/p&gt;
&lt;p&gt;If the &amp;ldquo;Enable Cross-Origin Resource Sharing (CORS)&amp;rdquo; option is turned off, when a request is made to the website from third party, the response will be treated as if CORS is fully disallowed. If the option is turned on, then the default behaviour is to allow all Origins; It isn&amp;rsquo;t until an entry is added to Allowed Origins that other origins will be denied access again. The same behaviour applies for headers and HTTP methods, all are considered allowed until specific methods or headers are defined as being allowed.&lt;/p&gt;
&lt;p&gt;There has been a distinct challenge in making the UI responsive and to reveal enough information to the user without overloading them with information. There are plenty of online resources on websites such&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers&quot;&gt;MDN Web Docs&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;that describe CORS and the various CORS headers that more detailed descriptions were deemed as too much for this UI.&lt;/p&gt;
&lt;h2&gt;Implementing CORS&lt;/h2&gt;
&lt;p&gt;Once I had the UI for managing CORS and the tables sorted to store the configuration, I was able to start looking at how I could implement the behaviour. At first, I attempted to build my own middleware to manage this, and it quickly dawned on me that this would be very complex to achieve. The&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Access-Control-Allow-Origin&lt;/code&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;header for example could only ever contain&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;*&lt;/code&gt;, the origin of the site making the request or being absent entirely. This prevents a website from revealing to third-parties who can consume them, only that the third-party website is allowed to consume them or not.&lt;/p&gt;
&lt;p&gt;In an attempt to understand a better way of handling this, I added CORS to my sample website using the standard microsoft libraries by calling the&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AddCors()&lt;/code&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;and&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UseCors()&lt;/code&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;methods within my&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;startup.cs&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;By navigating to the decompiled version of&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UseCors()&lt;/code&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;I was able to see the&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/dotnet/aspnetcore/blob/main/src/Middleware/CORS/src/Infrastructure/CorsMiddleware.cs&quot;&gt;CorsMiddleware&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;that was implemented by microsoft and see that it used an implementation of&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ICorsPolicyProvider&lt;/code&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;to retrieve a&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CorsPolicy&lt;/code&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;object and an implementation of&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ICorsService&lt;/code&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;to evaluate the&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/dotnet/aspnetcore/blob/main/src/Middleware/CORS/src/Infrastructure/CorsPolicy.cs&quot;&gt;CorsPolicy&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;itself. Looking at the decompiled code for&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AddCors()&lt;/code&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;I could see that the dependency injection calls for&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ICorsPolicyProvider&lt;/code&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;and&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ICorsService&lt;/code&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;were using TryAdd which only adds the default implementations of these interfaces if they have not already been declared.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;/// &amp;lt;summary&amp;gt;
/// Adds cross-origin resource sharing services to the specified &amp;lt;see cref=&quot;IServiceCollection&quot; /&amp;gt;.
/// &amp;lt;/summary&amp;gt;
/// &amp;lt;param name=&quot;services&quot;&amp;gt;The &amp;lt;see cref=&quot;IServiceCollection&quot; /&amp;gt; to add services to.&amp;lt;/param&amp;gt;
/// &amp;lt;returns&amp;gt;The &amp;lt;see cref=&quot;IServiceCollection&quot;/&amp;gt; so that additional calls can be chained.&amp;lt;/returns&amp;gt;
public static IServiceCollection AddCors(this IServiceCollection services)
{
    if (services == null)
    {
        throw new ArgumentNullException(nameof(services));
    }

    services.AddOptions();

    services.TryAdd(ServiceDescriptor.Transient&amp;lt;ICorsService, CorsService&amp;gt;());
    services.TryAdd(ServiceDescriptor.Transient&amp;lt;ICorsPolicyProvider, DefaultCorsPolicyProvider&amp;gt;());

    return services;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span&gt;Implementation then appeared to be relatively simple. Create a custom Implementation of&amp;nbsp;&lt;/span&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ICorsPolicyProvider&lt;/code&gt;&lt;span&gt;&amp;nbsp;that consumed the data saved by my Add-On to create an instance of&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/dotnet/aspnetcore/blob/main/src/Middleware/CORS/src/Infrastructure/CorsPolicy.cs&quot;&gt;CorsPolicy&lt;/a&gt;&lt;span&gt;&amp;nbsp;to be consumed by the default implementations of&amp;nbsp;&lt;/span&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ICorsService&lt;/code&gt;&lt;span&gt;. The interface of&amp;nbsp;&lt;/span&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ICorsPolicyProvider&lt;/code&gt;&lt;span&gt;&amp;nbsp;contained just a single method for me to implement.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;/// &amp;lt;summary&amp;gt;
/// A type which can provide a &amp;lt;see cref=&quot;CorsPolicy&quot;/&amp;gt; for a particular &amp;lt;see cref=&quot;HttpContext&quot;/&amp;gt;.
/// &amp;lt;/summary&amp;gt;
public interface ICorsPolicyProvider
{
    /// &amp;lt;summary&amp;gt;
    /// Gets a &amp;lt;see cref=&quot;CorsPolicy&quot;/&amp;gt; from the given &amp;lt;paramref name=&quot;context&quot;/&amp;gt;
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;param name=&quot;context&quot;&amp;gt;The &amp;lt;see cref=&quot;HttpContext&quot;/&amp;gt; associated with this call.&amp;lt;/param&amp;gt;
    /// &amp;lt;param name=&quot;policyName&quot;&amp;gt;An optional policy name to look for.&amp;lt;/param&amp;gt;
    /// &amp;lt;returns&amp;gt;A &amp;lt;see cref=&quot;CorsPolicy&quot;/&amp;gt;&amp;lt;/returns&amp;gt;
    Task&amp;lt;CorsPolicy?&amp;gt; GetPolicyAsync(HttpContext context, string? policyName);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span&gt;To minimise performance issues, my data storage is a single table with a very flat record that is transformed into a domain object with multiple properties and collections. To reduce further round trips to the SQL Server and constructing the domain object and then mapping that over to the&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/dotnet/aspnetcore/blob/main/src/Middleware/CORS/src/Infrastructure/CorsPolicy.cs&quot;&gt;CorsPolicy&lt;/a&gt;&lt;span&gt;&amp;nbsp;object, I also stored the compiled&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/dotnet/aspnetcore/blob/main/src/Middleware/CORS/src/Infrastructure/CorsPolicy.cs&quot;&gt;CorsPolicy&lt;/a&gt;&lt;span&gt;&amp;nbsp;object into Optimizely&amp;rsquo;s&amp;nbsp;&lt;/span&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ISynchronizedObjectInstanceCache&lt;/code&gt;&lt;span&gt;&amp;nbsp;which would then be invalidated on any subsequent update of the CORS policy.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public async Task&amp;lt;CorsPolicy?&amp;gt; GetPolicyAsync(HttpContext context, string? policyName)
{
    var policy = _cache.Get&amp;lt;CorsPolicy&amp;gt;(CacheKey);
    if (policy == null)
    {
        policy = await LoadPolicy();
        _cache.Add(CacheKey, policy);
    }

    return policy;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The end result is a fully functioning CMS editable CORS Policy that can be updated and can take effect immediately without requiring any code changes or deployments. To learn more about how CORS is implemented within .NET Core, you can read&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://andrewlock.net/a-deep-dive-in-to-the-asp-net-core-cors-library/&quot;&gt;A deep dive into the ASP.NET Core CORS library&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;written by&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://andrewlock.net/about/&quot;&gt;Andrew Lock&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;and view the open-source code for the&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/dotnet/aspnetcore/blob/main/src/Middleware/CORS/src/Infrastructure/CorsMiddleware.cs&quot;&gt;CorsMiddleware&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;on GitHub.&lt;/p&gt;
&lt;h2&gt;In Beta&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;m classing version 2.0.0 of this Add-on as being in beta until one or more CMS websites are live and consuming the CORS configuration and any raised issues are either prioritised or fixed. If you&amp;rsquo;d like to use the 2.0.0 beta version, do feel free to go ahead and install it into your Optimizely CMS 12 website and join the&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/GeekInTheNorth/Stott.Security.Optimizely/discussions/143&quot;&gt;discussion&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;page on the&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/GeekInTheNorth/Stott.Security.Optimizely/discussions/143&quot;&gt;GitHub Repository&lt;/a&gt;.&lt;/p&gt;</id><updated>2023-10-09T11:14:38.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Adding Block Specific JavaScript and CSS to the body and head of the page</title><link href="https://world.optimizely.com/blogs/mark-stott/dates/2023/9/adding-block-specific-javascript-and-css-to-the-body-and-head-of-the-page/" /><id>&lt;p&gt;A common requirement for CMS implementations includes the ability to embed third party content into a website as if it formed part of the website itself. Sometimes this takes the form of a full page embed like a campaign page and in other cases this can be smaller artefacts within an embed block placed between differing blocks within the flow of a page. Often these embeds come with additional JavaScript and Stylesheet content, which in turn can lead to a Content Security Policy being opened up for an entire site for a single embed on a single page.&lt;/p&gt;
&lt;p&gt;The following block provides three separate properties:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Embed Code: A text area that will be rendered in it&#39;s raw state on a razor file.&lt;/li&gt;
&lt;li&gt;Hosted JavaScript File: A content reference for an optimized production JavaScript file that has been uploaded to the CMS to be used by this block and rendered at the bottom of the body of the page.&lt;/li&gt;
&lt;li&gt;Hosted CSS File: A content reference for an optimized production stylesheet file that has been uploaded to the CMS to be used by this block and rendered within the head element of the page.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;[ContentType(
    DisplayName = &quot;Embed Block&quot;,
    GUID = &quot;0de2bdc7-9fc1-407d-8a60-2c2fb0e0b85a&quot;,
    Description = &quot;Allows the content editor to embed third party code snippets.&quot;,
    GroupName = SystemTabNames.Content)]
public class EmbedBlock : BlockData
{
    [Display(
        Name = &quot;Embed Code&quot;,
        Description = &quot;Please paste the full snippet of code to included within the HTML for the page and block.&quot;,
        GroupName = SystemTabNames.Content,
        Order = 10)]
    [UIHint(UIHint.Textarea)]
    [Required]
    public virtual string? EmbedCode { get; set; }

    [Display(
        Name = &quot;Hosted JavaScript File&quot;,
        Description = &quot;A CMS Hosted JavaScript file to be rendered at the bottom of the &#39;body&#39; element.&quot;,
        GroupName = GroupNames.Content,
        Order = 20)]
    [AllowedTypes(typeof(JavaScriptContent))]
    [CultureSpecific]
    public virtual ContentReference? JavaScriptFile { get; set; }

    [Display(
        Name = &quot;Hosted CSS File&quot;,
        Description = &quot;A CMS Hosted Cascading Style Sheet file to be rendered at the bottom of the &#39;head&#39; element.&quot;,
        GroupName = GroupNames.Content,
        Order = 40)]
    [AllowedTypes(typeof(CssContent))]
    public virtual ContentReference? CssFile { get; set; }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When a block is rendered within a content area, any attempt to render the sources for the JavaScript and CSS files will end up being rendered within the container for the block. If you are using the standard razor rendering method for pages within the website, you will have used &lt;strong&gt;@Html.RequiredClientResources(RenderingTags.Header)&lt;/strong&gt; to render Optimizely generated styles within the header of the page and scripts within the footer of the body. In this case we can consume the built in Client Resources functionality and register our files to be placed appropriately within the document.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public sealed class EmbedBlockViewComponent : BlockComponent&amp;lt;EmbedBlock&amp;gt;
{
    private readonly IUrlResolver _urlResolver;

    public EmbedBlockViewComponent(IUrlResolver urlResolver)
    {
        _urlResolver = urlResolver;
    }

    protected override IViewComponentResult InvokeComponent(EmbedBlock currentContent)
    {
        if (!ContentReference.IsNullOrEmpty(currentContent.JavaScriptFile))
        {
            ClientResources.RequireScript(_urlResolver.GetUrl(currentContent.JavaScriptFile)).AtFooter();
        }

        if (!ContentReference.IsNullOrEmpty(currentContent.CssFile))
        {
            ClientResources.RequireStyle(_urlResolver.GetUrl(currentContent.CssFile)).AtHeader();
        }

        return View(currentContent);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;strong&gt;ClientResources.RequireScript&lt;/strong&gt; and &lt;strong&gt;ClientResources.RequireStyle&lt;/strong&gt; methods will ensure that the resources are rendered once per unique file name.&amp;nbsp; This means that if you have two blocks which reference the same JavaScript file, then that JavaScript file is rendered once within the HTML document.&amp;nbsp; Now this is great, but what if you need to defer these embedded resources in order to improve your website performance?&lt;/p&gt;
&lt;p&gt;Both of these methods come with additional overrides that allow you to define the name for the resource, it&#39;s dependencies and it&#39;s additional attributes. For example, the following call:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;ClientResources.RequireScript(
    _urlResolver.GetUrl(currentContent.JavaScriptFile),
    currentContent.JavaScriptFile.ID.ToString(), // A nominated unique name for the javascript file
    new List&amp;lt;string&amp;gt;(0), // Dependencies
    new Dictionary&amp;lt;string, string&amp;gt; { { &quot;defer&quot;, &quot;defer&quot; } } // Additional Attributes
    ).AtFooter();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Will result in the follow script tag being rendered:&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;&amp;lt;script defer=&quot;defer&quot; src=&quot;/globalassets/embeds/test.js&quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can extend this same functionality to render externally hosted files with additional attributes, which could include cross origin and subresource integrity hashes like the following example&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;var externalJavaScript = &quot;https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js&quot;;
var externalJavaScriptSri = &quot;sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL&quot;;

var attributes = new Dictionary&amp;lt;string, string&amp;gt;
{
    { &quot;defer&quot;, &quot;defer&quot; },
    { &quot;crossorigin&quot;, &quot;anonymous&quot; },
    { &quot;integrity&quot;, externalJavaScriptSri }
};

ClientResources.RequireScript(
    externalJavaScript,
    externalJavaScript, // A nominated unique name for the javascript file
    new List&amp;lt;string&amp;gt;(0), // Dependencies
    attributes
    ).AtFooter();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Which would then render like so:&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;&amp;lt;script crossorigin=&quot;anonymous&quot; defer=&quot;defer&quot; integrity=&quot;sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL&quot; src=&quot;https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;In Summary&lt;/h2&gt;
&lt;p&gt;When constructing an Optimizely CMS website that allows the embedding of JavaScript and CSS resources, leverage the use of the static &lt;strong&gt;ClientResources.RequireScript&lt;/strong&gt; and &lt;strong&gt;ClientResources.RequireStyle&lt;/strong&gt; static methods within your controllers and view components to register your resources in order to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ensure that Stylesheet resources are rendered correctly within the header&lt;/li&gt;
&lt;li&gt;Ensure that JavaScript resources are rendered correctly at the bottom of the body&lt;/li&gt;
&lt;li&gt;Ensure that all resources are rendered uniquely&lt;/li&gt;
&lt;li&gt;Ensure that all resources are rendered with standard attributes for performance.&lt;/li&gt;
&lt;/ul&gt;</id><updated>2023-10-03T08:54:17.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Extending Geta Optimizely Sitemaps to Include Image Sitemaps</title><link href="https://world.optimizely.com/blogs/mark-stott/dates/2023/7/extending-geta-optimizely-sitemaps-to-include-image-sitemaps/" /><id>&lt;h2&gt;The Requirement&lt;/h2&gt;
&lt;p&gt;&lt;span&gt;One of our Optimizely CMS 12 clients has a media heavy site and part of our SEO brief was to include an Image XML Sitemap to improve visibility of media to search engines such as google.&amp;nbsp; For all of our CMS 12 builds, we like to use the Geta Optimizely Sitemaps plugin as this puts a lot of power into the hands of content editors and SEO specialists.&amp;nbsp; Unfortunately for this particular client, the plugin does not handle image sitemaps.&lt;/span&gt;&lt;/p&gt;
&lt;h3&gt;Image XML Sitemap Format&lt;/h3&gt;
&lt;p&gt;&lt;span&gt;There are a few reasons why you might want to use an image XML sitemap. Perhaps images on your site are not easily indexable by search engines due to the way they are loaded into the browser, possibly from a separate javascript event so that they do not form part of the initially rendered HTML. Perhaps your site is mobile optimized and only optimized versions of your images are available in your HTML and you want to expose the original images to search engines. So you decide that you do in fact want to use an image XML sitemap, but then there are two separate ways to do implement this.&lt;/span&gt;&lt;/p&gt;
&lt;h4&gt;Image only XML Sitemap&lt;/h4&gt;
&lt;p&gt;&lt;span&gt;In this format, the image (image:image) node exists as a child of the urlset node and exists once per image within your site:&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;urlset xmlns:image=&quot;http://www.google.com/schemas/sitemap-image/1.1&quot;&amp;gt;
    &amp;lt;image:image&amp;gt;
        &amp;lt;image:loc&amp;gt;https://www.example.com/image-1.jpg&amp;lt;/image:loc&amp;gt;
        &amp;lt;image:caption&amp;gt;Everything you need to know about Images&amp;lt;/image:caption&amp;gt;
        &amp;lt;image:geo_location&amp;gt;London, United Kingdom&amp;lt;/image:geo_location&amp;gt;
        &amp;lt;image:title&amp;gt;Image Sitemap Example&amp;lt;/image:title&amp;gt;
        &amp;lt;image:license&amp;gt;Creator: Acme, Credit Line: Acme SEO, Copyright Notice: Free to Use&amp;lt;/image:licence&amp;gt;
    &amp;lt;/image:image&amp;gt;
&amp;lt;/urlset&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span&gt;Initially I thought that the level of information packaged with the image was excellent as it added a lot of context.&amp;nbsp; However as of May 2022, all nodes except&amp;nbsp;&lt;/span&gt;&lt;strong&gt;image:image&lt;/strong&gt;&lt;span&gt;&amp;nbsp;and&amp;nbsp;&lt;/span&gt;&lt;strong&gt;image:loc&lt;/strong&gt;&lt;span&gt;&amp;nbsp;were deprecated and are no longer consumed by google.&amp;nbsp; I also tied setting up a sitemap within Geta Optimizely Sitemaps that used an image asset node as it&amp;rsquo;s root and this included decorating the image content types with the sitemap configuration properties, however this just resulted in an empty sitemap.xml.&lt;/span&gt;&lt;/p&gt;
&lt;h4&gt;Standard XML Sitemap with images&lt;/h4&gt;
&lt;p&gt;&lt;span&gt;In this format, the image nodes exist as a child of the url node and exists once per image that should be associated with that page:&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;urlset xmlns=&quot;http://www.sitemaps.org/schemas/sitemap/0.9&quot;
        xmlns:image=&quot;http://www.google.com/schemas/sitemap-image/1.1&quot;&amp;gt;
  &amp;lt;url&amp;gt;
    &amp;lt;loc&amp;gt;https://example.com/sample1.html&amp;lt;/loc&amp;gt;
    &amp;lt;lastmod&amp;gt;2023-07-03T16:29:07+01:00&amp;lt;/lastmod&amp;gt;
    &amp;lt;changefreq&amp;gt;weekly&amp;lt;/changefreq&amp;gt;
    &amp;lt;priority&amp;gt;0.5&amp;lt;/priority&amp;gt;
    &amp;lt;image:image&amp;gt;
      &amp;lt;image:loc&amp;gt;https://example.com/image.jpg&amp;lt;/image:loc&amp;gt;
    &amp;lt;/image:image&amp;gt;
    &amp;lt;image:image&amp;gt;
      &amp;lt;image:loc&amp;gt;https://example.com/photo.jpg&amp;lt;/image:loc&amp;gt;
    &amp;lt;/image:image&amp;gt;
  &amp;lt;/url&amp;gt;
  &amp;lt;url&amp;gt;
    &amp;lt;loc&amp;gt;https://example.com/sample2.html&amp;lt;/loc&amp;gt;
    &amp;lt;image:image&amp;gt;
      &amp;lt;image:loc&amp;gt;https://example.com/picture.jpg&amp;lt;/image:loc&amp;gt;
    &amp;lt;/image:image&amp;gt;
  &amp;lt;/url&amp;gt;
&amp;lt;/urlset&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span&gt;In this format, the context of an image is directly related to the page, but other than defining that the image exists, no additional context is provided.&amp;nbsp; This format did have the benefit of being able to follow the content tree which simplified the extending of the Geta Optimizely Sitemap solution.&lt;/span&gt;&lt;/p&gt;
&lt;h2&gt;The Solution&lt;/h2&gt;
&lt;p&gt;The core generation functionality for XML sitemaps within Geta Optimizely Sitemaps comes down to one primary abstract class called&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/Geta/geta-optimizely-sitemaps/blob/master/src/Geta.Optimizely.Sitemaps/XML/SitemapXmlGenerator.cs&quot;&gt;SitemapXmlGenerator&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;and all the XML Sitemap formats all inherit and extend this class by overriding specific methods.&amp;nbsp; The core logic has been split out into lots of different methods, each with a single responsibility that has been marked as virtual to allow for them to be overridden.&lt;/p&gt;
&lt;p&gt;The first step was to make our own implementation of&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/Geta/geta-optimizely-sitemaps/blob/master/src/Geta.Optimizely.Sitemaps/XML/IStandardSitemapXmlGenerator.cs&quot;&gt;IStandardSitemapXmlGenerator&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;that inherits the&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/Geta/geta-optimizely-sitemaps/blob/master/src/Geta.Optimizely.Sitemaps/XML/SitemapXmlGenerator.cs&quot;&gt;SitemapXmlGenerator&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;and to then override the dependency injection to replace Geta&amp;rsquo;s implementation with our own:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public class StandardAndImageSitemapXmlGenerator : SitemapXmlGenerator, IStandardSitemapXmlGenerator
{
    public StandardAndImageSitemapXmlGenerator(
        ISitemapRepository sitemapRepository,
        IContentRepository contentRepository,
        IUrlResolver urlResolver,
        ISiteDefinitionRepository siteDefinitionRepository,
        ILanguageBranchRepository languageBranchRepository,
        IContentFilter contentFilter,
        IUriAugmenterService uriAugmenterService,
        ISynchronizedObjectInstanceCache objectCache,
        IMemoryCache cache,
        ILogger&amp;lt;StandardSitemapXmlGenerator&amp;gt; logger)
        : base(
            sitemapRepository,
            contentRepository,
            urlResolver,
            siteDefinitionRepository,
            languageBranchRepository,
            contentFilter,
            uriAugmenterService,
            objectCache,
            cache,
            logger)
    {
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public static class GetaOptimizelySitemapsServiceExtension
{
    public static IServiceCollection AddGetaOptimizelySitemapsHandler(this IServiceCollection serviceCollection)
    {
        serviceCollection.AddSitemaps();

        // Geta injects the generators as Transient, so maintain the same scoping:
        serviceCollection.AddTransient&amp;lt;IStandardSitemapXmlGenerator, StandardAndImageSitemapXmlGenerator&amp;gt;();

        return serviceCollection;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span&gt;The first method that needed to be overridden is called GenerateRootElement and this is responsible for creating the urlset node and adding the standard namespace for an XML sitemap.&amp;nbsp; In our case we need to add an additional namespace for the image sitemaps:&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public class StandardAndImageSitemapXmlGenerator : SitemapXmlGenerator, IStandardSitemapXmlGenerator
{
    private readonly ILogger&amp;lt;StandardSitemapXmlGenerator&amp;gt; _logger;
    private readonly XNamespace _imageNamespace;
    private readonly XAttribute _imageAttribute;

    public StandardAndImageSitemapXmlGenerator(...) : base(...)
    {
        _logger = logger;

        _imageNamespace = XNamespace.Get(&quot;http://www.google.com/schemas/sitemap-image/1.1&quot;);
        _imageAttribute = new XAttribute(XNamespace.Xmlns + &quot;image&quot;, _imageNamespace.NamespaceName);
    }

    protected override XElement GenerateRootElement()
    {
        var rootElement = new XElement(SitemapXmlGenerator.SitemapXmlNamespace + &quot;urlset&quot;, _imageAttribute);

        if (this.SitemapData.IncludeAlternateLanguagePages)
            rootElement.Add((object)new XAttribute(XNamespace.Xmlns + &quot;xhtml&quot;, (object)SitemapXmlGenerator.SitemapXhtmlNamespace));
        return rootElement;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This overridden method is a clone of the base method. In order for the image namespace to be rendered correctly within the XML document, we have to create the image attribute as a direct child of the urlset node at the point in time the node is created.&amp;nbsp; Attempting to use the base method and then appending the XAttribute led to undesired prefixing of namespaces within the parent node.&lt;/p&gt;
&lt;p&gt;The next step was to make sure content types had a common method that could be used to identify images that should be included with the page in the XML sitemap. This could easily be a property that allows the CMS Editor to have full control of said images.&amp;nbsp; This would also need to be identifiable to the generation logic, so I added an interface that could be used to identify pages with images:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public interface ISitePageWithSitemapImages : IContent
{
    IList&amp;lt;ContentReference&amp;gt; SitemapImages { get; }
}

public class SitePageData : PageData, ISitePageWithSitemapImages
{
    public virtual IList&amp;lt;ContentReference&amp;gt; GetSitemapImages()
    {
        var images = new List&amp;lt;ContentReference&amp;gt;();

       // Image resolution logic goes here and is overridden for different content types

        return images;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span&gt;The final part of the puzzle is then to override the GenerateSiteElement method within the&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/Geta/geta-optimizely-sitemaps/blob/master/src/Geta.Optimizely.Sitemaps/XML/SitemapXmlGenerator.cs&quot;&gt;SitemapXmlGenerator&lt;/a&gt;&lt;span&gt;.&amp;nbsp; This method&amp;rsquo;s responsibility is to create the URL node and all of it&amp;rsquo;s child elements.&amp;nbsp; In this case I was able to leverage the base method and extend it to parse and add image nodes only if the page implemented my interface:&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public class StandardAndImageSitemapXmlGenerator : SitemapXmlGenerator, IStandardSitemapXmlGenerator
{
    private readonly ILogger&amp;lt;StandardSitemapXmlGenerator&amp;gt; _logger;
    private readonly XNamespace _imageNamespace;
    private readonly XAttribute _imageAttribute;

    // Constructor and GenerateRootElement go here

    protected override XElement GenerateSiteElement(IContent contentData, string url)
    {
        var pageElement = base.GenerateSiteElement(contentData, url);
        var urlResolverArgs = new UrlResolverArguments { ForceAbsolute = true };
        
        if (contentData is ISitePageWithSitemapImages pageData)
        {
            try
            {
                foreach (var sitemapImage in pageData.GetSitemapImages())
                {
                    var imageUrl = UrlResolver.GetUrl(sitemapImage, &quot;en&quot;, urlResolverArgs);
                    var image = new XElement(_imageNamespace + &quot;loc&quot;, (object)imageUrl);
                    pageElement.Add(new XElement(_imageNamespace + &quot;image&quot;, image));
                }
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, &quot;Oh No!&quot;);
            }
        }

        return pageElement;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span&gt;The final generated XML Sitemap looks like this (please note I&amp;rsquo;ve sanitised the URLs generated here):&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&amp;gt;
&amp;lt;urlset xmlns:image=&quot;http://www.google.com/schemas/sitemap-image/1.1&quot; xmlns=&quot;http://www.sitemaps.org/schemas/sitemap/0.9&quot;&amp;gt;
    &amp;lt;url&amp;gt;
        &amp;lt;loc&amp;gt;https://www.example.com/en/test-page-one/&amp;lt;/loc&amp;gt;
        &amp;lt;lastmod&amp;gt;2023-07-03T16:29:07+01:00&amp;lt;/lastmod&amp;gt;
        &amp;lt;changefreq&amp;gt;weekly&amp;lt;/changefreq&amp;gt;
        &amp;lt;priority&amp;gt;0.5&amp;lt;/priority&amp;gt;
        &amp;lt;image:image&amp;gt;
            &amp;lt;image:loc&amp;gt;https://www.example.com/globalassets/images/image-one.jpg&amp;lt;/image:loc&amp;gt;
        &amp;lt;/image:image&amp;gt;
        &amp;lt;image:image&amp;gt;
            &amp;lt;image:loc&amp;gt;https://www.example.com/globalassets/images/image-two.jpg&amp;lt;/image:loc&amp;gt;
        &amp;lt;/image:image&amp;gt;
    &amp;lt;/url&amp;gt;
    &amp;lt;url&amp;gt;
        &amp;lt;loc&amp;gt;https://localhost:5000/en/test-page-two/&amp;lt;/loc&amp;gt;
        &amp;lt;lastmod&amp;gt;2023-07-07T14:44:30+01:00&amp;lt;/lastmod&amp;gt;
        &amp;lt;changefreq&amp;gt;weekly&amp;lt;/changefreq&amp;gt;
        &amp;lt;priority&amp;gt;0.5&amp;lt;/priority&amp;gt;
        &amp;lt;image:image&amp;gt;
            &amp;lt;image:loc&amp;gt;https://www.example.com/globalassets/images/image-three.jpg&amp;lt;/image:loc&amp;gt;
        &amp;lt;/image:image&amp;gt;
        &amp;lt;image:image&amp;gt;
            &amp;lt;image:loc&amp;gt;https://www.example.com/globalassets/images/image-four.jpg&amp;lt;/image:loc&amp;gt;
        &amp;lt;/image:image&amp;gt;
    &amp;lt;/url&amp;gt;
&amp;lt;/urlset&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;The end result is that we can generate XML Sitemaps with image elements.&amp;nbsp; Some consideration needs to be made around your own implementation and how you want to manage images.&amp;nbsp; Do you want your SEO team to be able to curate this or do you want it to be automated through some custom logic?&amp;nbsp; This could be enhanced futher to include video elements and news elements. More details around these XML Sitemap variants can be found here:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://moz.com/learn/seo/xml-sitemaps&quot;&gt;https://moz.com/learn/seo/xml-sitemaps&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Limitations&lt;/h3&gt;
&lt;p&gt;There are some additional considerations to be made in terms of the size of XML Sitemaps here, especially if you start adding all of the media types.&amp;nbsp; Here are some limitations extrapolated from google&amp;rsquo;s developer documentation (which is worth a read around best practices):&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://developers.google.com/search/docs/crawling-indexing/sitemaps/build-sitemap&quot;&gt;https://developers.google.com/search/docs/crawling-indexing/sitemaps/build-sitemap&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Maximum XML Sitemap file size: 50MB&lt;/li&gt;
&lt;li&gt;Maximum number of URL nodes per sitemap: 50000&lt;/li&gt;
&lt;li&gt;Maximum number of Image nodes per URL node: 1000&lt;/li&gt;
&lt;li&gt;Maximum number of News nodes per URL node: 1000&lt;/li&gt;
&lt;li&gt;Maximum number of Video nodes per URL node: 1000? (This is based on other limits, but was not overtly declared on google&amp;rsquo;s guidance)&lt;/li&gt;
&lt;/ul&gt;</id><updated>2023-07-31T08:20:14.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Making Content Recommendations easy for content editors</title><link href="https://world.optimizely.com/blogs/mark-stott/dates/2023/6/making-content-recommendations-consistent-and-easy-for-content-editors/" /><id>&lt;h2&gt;The Problem&lt;/h2&gt;
&lt;p&gt;One of our clients had purchased the Content Recommendations module for use within their newly build CMS 12 corporate site.&amp;nbsp; Our challenge was that the main content team lacked development expertese and would outsource functionality like embedded content to external parties.&amp;nbsp; On top of this, the client had a desire for consistency in design across the system.&amp;nbsp; In it&#39;s default implementation, the addition of content recommendations across the system would require content editors to make changes to the handlebars scripts for every instance of the block.&lt;/p&gt;
&lt;h2&gt;The Solution&lt;/h2&gt;
&lt;p&gt;Our solution for this was to create a Custom Content Recommendations Block that obfuscates a number of the options on the default Content Recommendations Block and added additional properties that would allow them to render Content Recommendations as if it were the same design as another block within the site.&amp;nbsp; The key properties that I hid in this case were the Number of Recommendations and Recommendations Template properties.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;// Display and ContentType attributes omitted for brevity.
public class CustomContentRecommendationsBlock : ContentRecommendationsBlock
{
    public virtual string? Title { get; set; }

    public virtual string? Subtitle { get; set; }

    public virtual bool OmitCardDescriptions { get; set; }

    public virtual string? ReadMoreText { get; set; }

    public virtual ContentReference? FallBackImage { get; set; }

    [ScaffoldColumn(false)]
    public override int NumberOfRecommendations
    {
        get { return 4;}
        set { _ = value; }
    }

    [ScaffoldColumn(false)]
    public override string? RecommendationsTemplate
    {
        get { return &quot;Template is in the razor file.&quot;; }
        set { _ = value; }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The razor file itself was then created to conditionally render elements within the Content Recommendations script tag on the server side in accordance with the designs by our creative team. To handle recommendations which lacked a main image, a fallback image was rendered server side within the &lt;code&gt;{{^main_image_url}}&lt;/code&gt; handlebars tag. &amp;nbsp;Markup with &lt;code&gt;{{^main_image_url}}&lt;/code&gt; is then only rendered by the content recommendations code when the &lt;code&gt;main_image_url&lt;/code&gt; is null.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;@model CustomContentRecommendationsBlock

&amp;lt;script class=&quot;idio-recommendations&quot; type=&quot;text/x-mustache&quot; data-api-key=&quot;@Model.DeliveryApiKey&quot; data-rpp=&quot;@Model.NumberOfRecommendations&quot;&amp;gt;
&amp;lt;div class=&quot;container&quot;&amp;gt;
    &amp;lt;div class=&quot;cards-container&quot;&amp;gt;

        @if (!string.IsNullOrWhiteSpace(Model.Title) || !string.IsNullOrWhiteSpace(Model.Subtitle))
        {
            &amp;lt;div class=&quot;block__intro&quot;&amp;gt;
                @if (!string.IsNullOrWhiteSpace(Model.Title))
                {
                    &amp;lt;h2&amp;gt;@Html.PropertyFor(x =&amp;gt; x.Title)&amp;lt;/h2&amp;gt;
                }
                @if (!string.IsNullOrWhiteSpace(Model.Subtitle))
                {
                    &amp;lt;p&amp;gt;@Html.PropertyFor(x =&amp;gt; x.Subtitle)&amp;lt;/p&amp;gt;
                }
            &amp;lt;/div&amp;gt;
        }

        &amp;lt;div class=&quot;cards-rows&quot;&amp;gt;
            &amp;lt;div class=&quot;cards&quot; data-component=&quot;Cards&quot;&amp;gt;
                {{#content}}
                &amp;lt;div class=&quot;card&quot; data-item&amp;gt;
                    &amp;lt;!-- Teaser Card --&amp;gt;
                    &amp;lt;!-- Image --&amp;gt;
                    {{#main_image_url}}
                        &amp;lt;div class=&quot;card__image&quot;&amp;gt;
                            &amp;lt;img src=&quot;{{main_image_url}}?width=560&amp;amp;height=299&amp;amp;quality=90&amp;amp;rmode=crop&quot; alt=&quot;{{title}}&quot;&amp;gt;
                        &amp;lt;/div&amp;gt;
                    {{/main_image_url}}
                    {{^main_image_url}}
                        @if (!Model.FallbackImage.IsNullOrEmpty())
                        {
                            &amp;lt;div class=&quot;card__image&quot;&amp;gt;
                                &amp;lt;img src=&quot;@Url.ImageUrl(Model.FallBackImage, 560, 299)&quot; alt=&quot;@Model.FallBackImage.GetImageAltText()&quot;&amp;gt;
                            &amp;lt;/div&amp;gt;
                        }
                    {{/main_image_url}}
                    &amp;lt;!-- Category --&amp;gt;
                    {{#topics}}
                        &amp;lt;div class=&quot;card__category&quot;&amp;gt;{{title}}&amp;lt;/div&amp;gt;
                    {{/topics}}

                    &amp;lt;!-- Content --&amp;gt;
                    &amp;lt;div class=&quot;card__content&quot;&amp;gt;
                        &amp;lt;h4&amp;gt;{{title}}&amp;lt;/h4&amp;gt;
                        @if (!Model.OmitCardDescriptions)
                        {
                            &amp;lt;p&amp;gt;{{abstract}}&amp;lt;/p&amp;gt;
                        }
                        &amp;lt;a href=&quot;{{link_url}}&quot; class=&quot;solid-arrow-link&quot; title=&quot;{{title}}&quot; data-label_1=&quot;{{title}}&quot;&amp;gt;@Model.ReadMoreText&amp;lt;/a&amp;gt;
                    &amp;lt;/div&amp;gt;
                &amp;lt;/div&amp;gt;
                {{/content}}
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There were additional concerns that had to be addressed, the first being that the design of the block only allowed for a single category (Topic) to be rendered for each content card. &amp;nbsp;In this case we addressed this by using CSS styles to render hide off all but the first category.&lt;/p&gt;
&lt;h3&gt;Alternative Solution&lt;/h3&gt;
&lt;p&gt;Another approach to the solution would have been for us to look at overriding the default value assigned to the RecommendationsTemplate property so that when new Content Recommendations block was created, it would retain the ability for the content editor to customize the layout of the content recommendations.&amp;nbsp; This was an approach we chose not to go with in this situation as it still presented non-technical content editors with a technical property that they were not comfortable using.&amp;nbsp; Our chosen approach also allowed for functionality such as the fall back image to be updated centrally and immediately affect all custom content recommendation blocks within the site.&lt;/p&gt;
&lt;h2&gt;Client Expectations vs Reality&lt;/h2&gt;
&lt;p&gt;Within the CMS, the editor has the ability to prioritise categories that are displayed on cards shown on other blocks and they have the ability to provide an optional Teaser Title and Teaser Description against a page that is used across the site for that page that is different to standard meta data. &amp;nbsp;The client&#39;s expectation was that all of the content recommendations would render the exact same content and categories as if the teaser content had been curated by themselves on a regular block within the site. We had to explain how Content Recommendations worked:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;That Content Recommendations is based on a scan of the fully rendered page&lt;/li&gt;
&lt;li&gt;That Content Recommendations does not have direct access to the raw CMS data.&lt;/li&gt;
&lt;li&gt;That it would not be a live replication of changes within the CMS.&lt;/li&gt;
&lt;li&gt;That Content Recommendations come from the cloud service and not directly from the CMS.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Ultimately the client understood this and is now using the custom content recommendations block across their live site. In our next phase of Content Recommendations, we want to look at pushing additional meta data into Content Recommendations that matches the teaser data defined by the content editor and then hopefully retrieve and display this data within the results from Content Recommendations.&lt;/p&gt;</id><updated>2023-07-07T12:18:20.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Build it Once (A proposal for CMS 12 builds)</title><link href="https://world.optimizely.com/blogs/mark-stott/dates/2023/3/build-it-once-reusable-optimizely-features-using-razor-class-libraries/" /><id>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;The following is based on a 30 minute presentation performed by myself at Optimizely North in Manchester on the 1st March 2023.&amp;nbsp; Some of this content is based on question and answer sections performed on the day as well as observing builds that I have contributed to that have origins both internal and external to the agency that I am currently with. It should be noted that this is the proposition of an approach to help reduce the duplication of effort and to allow developers to focus on what is truely unique to a client.&lt;/p&gt;
&lt;h2&gt;The Problem&lt;/h2&gt;
&lt;p&gt;As Agencies we build sites for multiple customers and commonly end up building a lot of the same components over and over again.&amp;nbsp; Most agencies have considered at least one solution to how we can save our development teams from repeating the same tasks with every single build.&amp;nbsp; Solutions including:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Having a starter solution that all client builds start from.
&lt;ul&gt;
&lt;li&gt;Optimizely have similar solutions such as Alloy, Quicksilver and Foundation, however these may not fit in with coding standards or architectural standards of individual agencies.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Having a folder of pre-built common components that are copied into a new build that are then modified for the specific client&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These are essentially a copy and paste situations and do ultimately save on some time within a new website build and as part of this presentation and now blog, I attempt to offer another possible solution if we consider a change in how we build Optimizely websites.&lt;/p&gt;
&lt;h2&gt;The Technology&lt;/h2&gt;
&lt;p&gt;Optimizely CMS 12 has now been available to the community for 18+ months and as part of the shift to .NET 6+, we have a new development tools available that we can use to componentise our builds.&amp;nbsp; A personal favourite of mine is the &lt;a href=&quot;https://learn.microsoft.com/en-us/aspnet/core/razor-pages/ui-class?view=aspnetcore-7.0&amp;amp;amp;amp;amp;tabs=visual-studio&quot;&gt;Razor Class Library&lt;/a&gt; (RCL) which I have used now to build two different plugins for Optimizely CMS 12.&amp;nbsp; A Razor Class Library is a standard class library that can also contain Razor files and static assets within it&#39;s own wwwroot folder.&amp;nbsp; These can in turn be packaged as their own nuget package or dll that contains it&#39;s own razor files and static assets that can be added to any other build.&lt;/p&gt;
&lt;p&gt;The great thing about Razor Class Libraries is that if you do not specify a layout file for your razor files within the library, the layout file from the website consuming the Razor Class Library will be used instead.&amp;nbsp; And if you provide a Razor file on the same folder structure as that contained within the Razor Class Library, the razor file from the consuming website is used in place of the razor file in the razor class library.&amp;nbsp; This allows you to optionally replace the render for any given component packaged within the Razor Class Library.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;For example:&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;In my &lt;a href=&quot;https://github.com/GeekInTheNorth/opti-north-feb-2023&quot;&gt;demo build&lt;/a&gt; for this topic I structured the code into three separate projects:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;OptiNorthDemo.Core
&lt;ul&gt;
&lt;li&gt;This project contained common assets that would theoretically be utilized by all product based projects.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;OptiNorthDemo.News
&lt;ul&gt;
&lt;li&gt;This project contained components that made up a news feature including:
&lt;ul&gt;
&lt;li&gt;A News Article Page&lt;/li&gt;
&lt;li&gt;A News Listing Page&lt;/li&gt;
&lt;li&gt;News Listing and filtering logic&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;This project could then be expanded to contain additional News features:
&lt;ul&gt;
&lt;li&gt;Releated News Block&lt;/li&gt;
&lt;li&gt;Front end News Listing component built using the preferred framework of your agency (e.g. React)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;OptiNorthDeno
&lt;ul&gt;
&lt;li&gt;This project was the base website for the demo which then referenced OptiNorthDemo.News and OptiNorthDemo.Core as nuget packages.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This solution contains a Razor file on the path of &lt;strong&gt;OptiNorthDemo.News/Views/NewsArticlePage/Index.cshtml&lt;/strong&gt; and when I ran the solution the razor file from that path was used to render the article.&amp;nbsp; When I then added a razor file on the path of &lt;strong&gt;OptiNorthDemo/Views/NewsArticlePage/Index.cshtml&lt;/strong&gt;, the razor file from the &lt;strong&gt;OptiNorthDemo&lt;/strong&gt; project was used instead while all of the existing business logic and controller logic from the &lt;strong&gt;OptiNorthDemo.News&lt;/strong&gt; project facilitated the program flow.&lt;/p&gt;
&lt;h2&gt;The Proposition&lt;/h2&gt;
&lt;p&gt;If we analyze all that we build across all of the websites we currently manage / develop, we should be able to identify common themes or features.&amp;nbsp; We should then be able to identify what makes these individual features work well and design a content structure and business logic structure that should fulfil the needs of most if not all usages of that feature.&amp;nbsp; We should then fully build out that feature and manage it as a product.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Going back to our news example again, a single Razor Class Library would reference our core package and then contain the News Article Page, News Listing page, a Related News Block and all of the core business logic that makes it work e.g. Optimizely Search and Navigation queries, API endpoints and frontend listing and search components.&lt;/p&gt;
&lt;p&gt;In order to make this work, we need to think of our content structures in a more Atomic Content way.&amp;nbsp; This means less fixed fields and page flow and more thought about flexible content block usage.&amp;nbsp; We also need to think about minimising the usage of Content Type restrictions to targeting interfaces that allow specific functional categories of blocks.&amp;nbsp; For example, we can think of blocks as &quot;Hero Blocks&quot; for use at the top of pages, &quot;Content Blocks&quot; as core page content and as &quot;Related Blocks&quot; as additional content that could follow core page content.&amp;nbsp; All of these would have to be declared in a core project that all of our feature would use.&amp;nbsp; e.g.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;namespace OptiNorthDemo.Core.Blocks;

using EPiServer.Core;
using EPiServer.Shell;

/// &amp;lt;summary&amp;gt;
/// Used to define a standard content block to be allowed in main content areas.
/// This combined with &amp;lt;see cref=&quot;ContentBlockUiDescriptor&quot;/&amp;gt; simplify these declarations.
/// &amp;lt;/summary&amp;gt;
public interface IContentBlock : IContentData
{
}

/// &amp;lt;summary&amp;gt;
/// Used to help Optimizely CMS UI Recognize the &amp;lt;see cref=&quot;IContentBlock&quot;/&amp;gt; interface.
/// This allows us to simplify which blocks are allowed in main content areas.
/// &amp;lt;/summary&amp;gt;
[UIDescriptorRegistration]
public class ContentBlockUiDescriptor : UIDescriptor&amp;lt;IContentBlock&amp;gt;
{
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Going back to our News Page example, we would inherit a base set of page properties from the core project and then apply our content area restrictions based on interfaces defined within the core project but implemented either in additional packages or directly within a client specific build.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;[ContentType(DisplayName = &quot;News Article Page&quot;, Description = &quot;A flexible news article page.&quot;, GUID = &quot;DADC30C2-7F68-45F3-B279-FD4446B9B3CA&quot;, GroupName = GroupNames.Content)]
public class NewsArticlePage : SitePageData
{
    // Some other news article specific fields...

    [Display(
        Name = &quot;Article Content&quot;,
        Description = &quot;A content area that allows blocks that have been specifically designed as core content.&quot;,
        GroupName = GroupNames.Content,
        Order = 110)]
    [AllowedTypes(typeof(IContentBlock))]
    public virtual ContentArea? ArticleContent { get; set; }

    [Display(
        Name = &quot;Additional Content&quot;,
        Description = &quot;A content area that allows blocks that have been specifically designed as related content.&quot;,
        GroupName = GroupNames.Content,
        Order = 120)]
    [AllowedTypes(typeof(IRelatedContentBlock))]
    public virtual ContentArea? AdditionalContent { get; set; }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As agencies it is common to sell development time.&amp;nbsp; The dream here with this approach is to allow us to sell Value and Time.&amp;nbsp; So lets imagine that a client comes in and as part of their discovery they understand that they need an Optimizely Site, that it comes with Case Studies, News, Events, Products, General Content etc.&amp;nbsp; As part of applying that cost we turn around to the client and we give them the something closer to the following list of made up numbers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Case Studies: &amp;pound;20,000 package plus 3 days for customization&lt;/li&gt;
&lt;li&gt;News: &amp;pound;20,000 package plus 3 days for customization&lt;/li&gt;
&lt;li&gt;Products: 20 days develop and build (product properties vary wildly between business types)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The end result being less time spent developing the common stuff and being able to either improve profit margins or do more for our clients with their budgets.&lt;/p&gt;
&lt;h3&gt;Problems with this approach&lt;/h3&gt;
&lt;p&gt;It is not expected that that this would be achieveable to fulfill for all possible agencies to use a single feature.&amp;nbsp; Each agency will have it&#39;s own coding standards, it&#39;s own preferred list of plugins they like to use within each of their builds, it&#39;s own preferred architectural structures and frontend libraries.&amp;nbsp; e.g. different agencies might have frontend teams that specialise in React, Angular, Vue etc.&amp;nbsp; It is also expected that this would only work if you are adopting a more Atomic content structure with minimal content type and block type restrictions.&lt;/p&gt;
&lt;h4&gt;1. Who looks after this and how do we stop it going out of date?&lt;/h4&gt;
&lt;p&gt;To keep a product or feature up to date, it needs to be allocated a Product Owner, someone who is responsible for the product or feature and ensuring it is not forgotten.&amp;nbsp; After it&#39;s initially been built, keeping the module up to date can either be factored in to the additional needs of a specific client or the needs of keeping packages up to date.&lt;/p&gt;
&lt;h4&gt;2. What if a client leaves and our nuget feed is internal?&lt;/h4&gt;
&lt;p&gt;For any given feature, this would require the nuget reference is removed from the website project and the matching version of the project is added to the solution.&amp;nbsp; This would mean that you should version each &quot;release&quot; of a given feature and ensure that the relevant commit within the repository is correctly tagged with the release version to make this task as easy as possible.&lt;/p&gt;
&lt;h4&gt;3. What if I need to change the structure of the markup for a specific build?&lt;/h4&gt;
&lt;p&gt;This would be a simple case of creating a new razor file on the same path but within the client specific website.&lt;/p&gt;
&lt;h4&gt;4. What if there are properties that client does not need that I want to hide?&lt;/h4&gt;
&lt;p&gt;It is possible to create an &lt;strong&gt;EditorDescriptor&lt;/strong&gt; that is decorated with the &lt;strong&gt;EditorDescriptorRegistration&lt;/strong&gt; attribute and then use this to hide properties from the CMS editor.&amp;nbsp; In the following code snippet I have created a base &lt;strong&gt;EditorDescriptor&lt;/strong&gt; called &lt;span&gt;&lt;strong&gt;HideDefaultFieldsEditorDescriptor&lt;/strong&gt; and then created additional classes which inherit from &lt;strong&gt;HideDefaultFieldsEditorDescriptor&lt;/strong&gt;&amp;nbsp;and simply have the correct target field types applied.&amp;nbsp; This logic is executed when a field is rendered within the CMS interface and then sets it&#39;s visibility to false if it is within a given list of hidden page and properties.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;[EditorDescriptorRegistration(EditorDescriptorBehavior = EditorDescriptorBehavior.PlaceLast, TargetType = typeof(ContentArea))]
public class HideDefaultContentAreasEditorDescriptor : HideDefaultFieldsEditorDescriptor
{
}

[EditorDescriptorRegistration(EditorDescriptorBehavior = EditorDescriptorBehavior.PlaceLast, TargetType = typeof(DateTime?))]
public class HideDefaultDateTimesEditorDescriptor : HideDefaultFieldsEditorDescriptor
{
}

public class HideDefaultFieldsEditorDescriptor : EditorDescriptor
{
	public override void ModifyMetadata(ExtendedMetadata metadata, IEnumerable&amp;lt;Attribute&amp;gt; attributes)
	{
		base.ModifyMetadata(metadata, attributes);

		var contentType = metadata.FindOwnerContent()?.GetOriginalType()?.Name;
		var propertyName = metadata.PropertyName;
		if (ShouldHideField(contentType, propertyName))
		{
			metadata.ShowForEdit = false;
		}
	}

	private static bool ShouldHideField(string? contentType, string? propertyName)
	{
		var hiddenFields = new List&amp;lt;Tuple&amp;lt;string, string&amp;gt;&amp;gt;
		{
			new(&quot;NewsArticlePage&quot;, &quot;AdditionalContent&quot;),
			new(&quot;NewsArticlePage&quot;, &quot;DisplayPublishedDate&quot;)
		};

		return hiddenFields.Any(x =&amp;gt;
			string.Equals(x.Item1, contentType, StringComparison.OrdinalIgnoreCase) &amp;amp;&amp;amp;
			string.Equals(x.Item2, propertyName)
		);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;In Summary&lt;/h2&gt;
&lt;p&gt;With the advent of .NET 6+, CMS 12 and the concept of Atomic Content, are we now truely in a place where we can conceptualize content features as packages that are built once and installed into multiple clients and then customized to match their design?&amp;nbsp; With the power of Razor Class Libraries we are now able to create extensible collections of content features, the question does remain if we are able to make that next leap forward into selling value.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://learn.microsoft.com/en-us/aspnet/core/razor-pages/ui-class?view=aspnetcore-7.0&amp;amp;amp;amp;amp;tabs=visual-studio&quot;&gt;https://learn.microsoft.com/en-us/aspnet/core/razor-pages/ui-class?view=aspnetcore-7.0&amp;amp;tabs=visual-studio&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/GeekInTheNorth/opti-north-feb-2023&quot;&gt;https://github.com/GeekInTheNorth/opti-north-feb-2023&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</id><updated>2023-03-05T17:37:45.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>404 Error on Static Assets Within an Optimizely plugin</title><link href="https://world.optimizely.com/blogs/mark-stott/dates/2022/9/404-error-on-static-assets-within-an-optimizely-plugin/" /><id>&lt;h2&gt;Background&lt;/h2&gt;
&lt;p&gt;With the move to CMS 12 and .NET 5/6, developers are now able to build Plugins and Extensions using Razor Class Libraries (RCL).&amp;nbsp; These are a fantastic option as it allows you to bundle up any client side scripts, styles and razor files into the same compiled dll as your buisness logic.&amp;nbsp; The structure of the Razor files and static files within an RCL is the same structure as what you would expect to see within a Web project.&lt;/p&gt;
&lt;p&gt;The biggest benefit is that it makes it very clean to install and remove said plugin as it does not require any zip files to be deployed into a modules folder and really compartmentalizes your plugin.&amp;nbsp; To date I have built two different plugins which utilize Razor Class Libraries:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/GeekInTheNorth/Stott.Optimizely.RobotsHandler&quot;&gt;Stott.Optimizely.RobotsHandler&lt;/a&gt;&amp;nbsp;currently at v2.1.0&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/GeekInTheNorth/Stott.Security.Optimizely&quot;&gt;Stott.Security.Optimizely&lt;/a&gt; currently in early access at 0.4.0&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The Problem&lt;/h2&gt;
&lt;p&gt;I was contacted by &lt;a href=&quot;/link/5341f632537c4b0ab6b8fb651bd310f8.aspx?userId=fd64fb7a-ba91-e911-a968-000d3a441525&quot;&gt;Praveen Soni&lt;/a&gt; on the Optimizely Community slack about an issue they were encountering.&amp;nbsp; In development the &lt;a href=&quot;https://github.com/GeekInTheNorth/Stott.Optimizely.RobotsHandler&quot;&gt;Stott.Optimizely.RobotsHandler&lt;/a&gt; module was working as anticipated, however when it deployed into DXP they were receiving a 404 error on the following file:&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;https://example.dxcloud.episerver.net/_content/Stott.Optimizely.RobotsHandler/RobotsAdmin.js  &lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Praveen and I chatted some more, we talked about the requirements within the startup.cs file, I got them to share their error logs in hopes of trying to understand why this javascript file from the Razor Class Library was not being served.&lt;/p&gt;
&lt;p&gt;I later stumbled upon this article that resonated with the issue that we were encountering: &lt;a href=&quot;https://dev.to/j_sakamoto/how-to-deal-with-the-http-404-content-foo-bar-css-not-found-when-using-razor-component-package-on-asp-net-core-blazor-app-aai&quot;&gt;How to deal with the &quot;HTTP 404 &#39;_content/Foo/Bar.css&#39; not found&quot; when using Razor component package on ASP.NET Core Blazor app&lt;/a&gt; and this led me back to this article on the microsoft website &lt;a href=&quot;https://learn.microsoft.com/en-us/aspnet/core/razor-pages/ui-class?view=aspnetcore-6.0&amp;amp;amp;amp;tabs=visual-studio&quot;&gt;Create reusable UI using the Razor class library project in ASP.NET Core&lt;/a&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;The Solution&lt;/h2&gt;
&lt;p&gt;If your Razor Class library includes Razor Pages, then the following methods need to called with the the Startup.cs of the consuming application:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;services.AddRazorPages();

app.MapRazorPages();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If your Razor Class library includes static files like .css and .js files, then a call to &lt;strong&gt;UseStaticWebAssetts()&lt;/strong&gt; must be used in the Program.cs of the consuming application:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public static class Program
{
    public static void Main(string[] args) =&amp;gt; CreateHostBuilder(args).Build().Run();

    public static IHostBuilder CreateHostBuilder(string[] args) =&amp;gt;
        Host.CreateDefaultBuilder(args)
            .ConfigureCmsDefaults()
            .ConfigureWebHostDefaults(webBuilder =&amp;gt;
            {
                webBuilder.UseStartup&amp;lt;Startup&amp;gt;();
                webBuilder.UseStaticWebAssets();
            });
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now here is the fun part: if the &lt;strong&gt;ASPNETCORE_ENVIRONMENT&lt;/strong&gt; environment variable is set to &lt;strong&gt;Development&lt;/strong&gt;, then .NET automatically includes a call to &lt;strong&gt;UseStaticWebAssets()&lt;/strong&gt;, if it is set to any other value, then you have to manually add this to your code!&amp;nbsp; It is this final part of the puzzle which explains why the code worked perfectly in some environments but failed in others.&lt;/p&gt;</id><updated>2022-09-23T08:07:09.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Unit Testing with Dynamic Data Store in CMS 12 - Part 2</title><link href="https://world.optimizely.com/blogs/mark-stott/dates/2022/5/unit-testing-with-dynamic-data-store-in-cms-12---part-2/" /><id>&lt;h2&gt;Background&lt;/h2&gt;
&lt;p&gt;In the previous article of this series,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;/link/2e3e343bbab64aeebee0526bfa2ac366.aspx&quot;&gt;Unit Testing Optimizely CMS 12 Dynamic Data Store - Part 1&lt;/a&gt;, an introduction was given to what the Dynamic Data Store is, and an approach was given for unit testing by mocking the&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;code&gt;DynamicDataStore&lt;/code&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;object.&lt;/p&gt;
&lt;p&gt;In this article we are covering a different approach where we abstract away the usage of the Dynamic Data Store behind an interface with minimal logic.&lt;/p&gt;
&lt;h2&gt;Approach&lt;/h2&gt;
&lt;p&gt;Developers who are familiar with Entity Framework (EF) or Unit of Work (UOW) pattern may see some familiarity with this approach, and EF was indeed my inspiration here.&lt;/p&gt;
&lt;p&gt;For this example, I am using a custom data object which implements the&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;code&gt;IDynamicData&lt;/code&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;interface required for use with Dynamic Data Store:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public class MyCustomDataObject : IDynamicData
{
   public Identity Id { get; set; }

    public string UniqueText { get; set; }

    public string SomeOtherText { get; set; }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span&gt;Next we add an interface for data context, and since this will potentially expose multiple data collections, we also add an interface for operations on a data collection:&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public interface IDdsContext
{
   IDdsEntityCollection&amp;lt;MyCustomDataObject&amp;gt; MyCustomDataObjects { get; }
}

public interface IDdsEntityCollection&amp;lt;TModel&amp;gt;
   where TModel : IDynamicData
{
   IOrderedQueryable&amp;lt;TModel&amp;gt; Items();

    IEnumerable&amp;lt;TModel&amp;gt; AllItems();

    TModel Get(Identity identity);

    IEnumerable&amp;lt;TModel&amp;gt; Find(string propertyName, object propertyValue);

    Identity Save(TModel entity);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span&gt;Next we need to add a implementation of the&amp;nbsp;&lt;code&gt;IDdsEntityCollection&amp;lt;TModel&amp;gt;&lt;/code&gt;&amp;nbsp;to facilitate data operations against a singular data type. &amp;nbsp;As you will see below, each of the implemented methods is as light weight as possible. &amp;nbsp;If you are using additional methods within the Dynamic Data Store, then you will need to extend this interface and implementation.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public class DdsEtityCollection&amp;lt;TModel&amp;gt; : IDdsEntityCollection&amp;lt;TModel&amp;gt;
   where TModel : IDynamicData
{
   private DynamicDataStore _dynamicDataStore;

   public DdsEtityCollection(DynamicDataStoreFactory dynamicDataStoreFactory)
   {
       _dynamicDataStore = dynamicDataStoreFactory.CreateStore(typeof(TModel));
   }

   public IOrderedQueryable&amp;lt;TModel&amp;gt; Items()
   {
       return _dynamicDataStore.Items&amp;lt;TModel&amp;gt;();
   }

   public IEnumerable&amp;lt;TModel&amp;gt; AllItems()
   {
       return _dynamicDataStore.LoadAll&amp;lt;TModel&amp;gt;() ?? Enumerable.Empty&amp;lt;TModel&amp;gt;();
   }

   public TModel Get(Identity identity)
   {
       return _dynamicDataStore.Load&amp;lt;TModel&amp;gt;(identity);
   }

   public IEnumerable&amp;lt;TModel&amp;gt; Find(string propertyName, object propertyValue)
   {
       return _dynamicDataStore.Find&amp;lt;TModel&amp;gt;(propertyName, propertyValue);
   }

   public Identity Save(TModel entity)
   {
       return _dynamicDataStore.Save(entity);
   }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span&gt;Now we have an implementation of&amp;nbsp;&lt;code&gt;IDdsEntityCollection&amp;lt;TModel&amp;gt;&lt;/code&gt;, we can now implement&amp;nbsp;&lt;code&gt;IDdsContext&lt;/code&gt;. In this example, the implementation of&amp;nbsp;&lt;code&gt;IDdsEntityCollection&amp;lt;TModel&amp;gt;&lt;/code&gt;&amp;nbsp;is not instantiated until it is actually required by the consuming code.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public class DdsContext : IDdsContext
{
   private DynamicDataStoreFactory _dataStoreFactory;

   public DdsContext(DynamicDataStoreFactory dataStoreFactory)
   {
       _dataStoreFactory = dataStoreFactory;
   }

   private IDdsEntityCollection&amp;lt;MyCustomDataObject&amp;gt; _myCustomDataObjects = null;

   public IDdsEntityCollection&amp;lt;MyCustomDataObject&amp;gt; MyCustomDataObjects
   {
       get
       {
           if (_myCustomDataObjects == null)
           {
               _myCustomDataObjects = new DdsEtityCollection&amp;lt;MyCustomDataObject&amp;gt;(_dataStoreFactory);
           }

           return _myCustomDataObjects;
       }
   }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span&gt;Now we can update the repository we created in&amp;nbsp;&lt;a href=&quot;/link/2e3e343bbab64aeebee0526bfa2ac366.aspx&quot;&gt;Part 1&lt;/a&gt;&amp;nbsp;of this series, stripping out the Dynamic Data Store objects and injecting the&amp;nbsp;&lt;code&gt;IDdsContext&lt;/code&gt;&amp;nbsp;instead.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public class MyCustomDataObjectRepository
{
   private readonly IDdsContext _context;

   public MyCustomDataObjectRepository(IDdsContext context)
   {
       _context = context;
   }

   public void Save(Guid id, string uniqueText, string someOtherText)
   {
       var matchingRecord = _context.MyCustomDataObjects.Find(nameof(MyCustomDataObject.UniqueText), uniqueText).FirstOrDefault();

       if (matchingRecord != null &amp;amp;&amp;amp; !matchingRecord.Id.ExternalId.Equals(id))
       {
           throw new EntityExistsException($&quot;An entry already exists for the unique value of &#39;{uniqueText}&#39;.&quot;);
       }

       var recordToSave = Guid.Empty.Equals(id) ? CreateNewRecord() : _context.MyCustomDataObjects.Get(Identity.NewIdentity(id));
       recordToSave.UniqueText = uniqueText;
       recordToSave.SomeOtherText = someOtherText;
       _context.MyCustomDataObjects.Save(recordToSave);
   }

   private static MyCustomDataObject CreateNewRecord()
   {
       return new MyCustomDataObject { Id = Identity.NewIdentity() };
   }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span&gt;Finally, we can write the same unit tests again, however we are now mocking interfaces inside of our repositories instead of mocking the implementations of the Dynamic Data Store.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;[TestFixture]
public class MyCustomDataObjectRepositoryTests
{
   private Mock&amp;lt;IDdsEntityCollection&amp;lt;MyCustomDataObject&amp;gt;&amp;gt; _mockDataCollection;
   private Mock&amp;lt;IDdsContext&amp;gt; _mockContext;
   private MyCustomDataObjectRepository _repository;

   [SetUp]
   public void SetUp()
   {
       _mockDataCollection = new Mock&amp;lt;IDdsEntityCollection&amp;lt;MyCustomDataObject&amp;gt;&amp;gt;();
       _mockContext = new Mock&amp;lt;IDdsContext&amp;gt;();
       _mockContext.Setup(x =&amp;gt; x.MyCustomDataObjects).Returns(_mockDataCollection.Object);
       _repository = new MyCustomDataObjectRepository(_mockContext.Object);
   }

   [Test]
   public void GivenUniqueTextExistsAgainstAnotherEntity_ThenAnEntityExistsExceptionShouldBeThrown()
   {
       // Arrange
       var uniqueText = &quot;i-am-unique&quot;;
       var someOtherText = &quot;some-other-text&quot;;

       var existingRecord = new MyCustomDataObject
       {
           Id = Guid.NewGuid(),
           UniqueText = uniqueText,
           SomeOtherText = &quot;original-other-text&quot;
       };

        _mockDataCollection.Setup(x =&amp;gt; x.Find(It.IsAny&amp;lt;string&amp;gt;(), It.IsAny&amp;lt;object&amp;gt;()))
                           .Returns(new List&amp;lt;MyCustomDataObject&amp;gt; { existingRecord });

        // Assert
       Assert.Throws&amp;lt;EntityExistsException&amp;gt;(() =&amp;gt; _repository.Save(Guid.Empty, uniqueText, someOtherText));
   }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;This approach to unit testing with the Dynamic Data Store did the following.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Created a data collection interface and implementation using generics for operations on the&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;code&gt;DynamicDataStore&lt;/code&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;object for a given type.&lt;/li&gt;
&lt;li&gt;Created an interface and implementation of a data context which exposed only interfaces for the data collections.&lt;/li&gt;
&lt;li&gt;Injected the interface data context into repositories instead of the Dynamic Data Store.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Which Approach&lt;/h2&gt;
&lt;p&gt;When deciding which approach to take, consider just how many operations and data types you will be using with the Dynamic Data Store. &amp;nbsp;Consider whether all of the scaffolding of the data context approach in this article out weighs your usage of the Dynamic Data Store. &amp;nbsp;Remember the&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/KISS_principle&quot;&gt;KISS&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;and&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Don%27t_repeat_yourself&quot;&gt;DRY&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;principals and I&#39;m sure you&#39;ll get the right solution for your project.&lt;/p&gt;
&lt;p&gt;Finally, if you are finding you are heavily using the Dynamic Data Store, consider using your own tables using Entity Framework or any other Object Relational Mapper (ORM). &amp;nbsp;You will have better performance with data tables which are designed to suit your data needs.&lt;/p&gt;</id><updated>2022-05-26T11:03:27.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Unit Testing with Dynamic Data Store in CMS 12 - Part 1</title><link href="https://world.optimizely.com/blogs/mark-stott/dates/2022/5/unit-testing-with-dynamic-data-store-in-cms-12---part-1/" /><id>&lt;h2&gt;Background&lt;/h2&gt;
&lt;p&gt;If you&#39;ve never heard of it, the&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://docs.developers.optimizely.com/content-cloud/v12.0.0-content-cloud/docs/dynamic-data-store&quot;&gt;Dynamic Data Store (DDS)&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;which is a component that offers an API and infrastructure for CRUD operations of custom data objects.&amp;nbsp; It allows developers to access and store data without needing to provision custom tables or databases using either classes or property bags which are stored within shared tables contained in the CMS database.&lt;/p&gt;
&lt;p&gt;Optimizely also use the same functionality for some of their own features. &amp;nbsp;For example, Optimizely Search &amp;amp; Navigation comes with a feature known as Best Bets which allows content editors to provide search suggestions for given search terms. &amp;nbsp;Each Best Bet has it&#39;s own entry inside of the Dynamic Data Store.&lt;/p&gt;
&lt;p&gt;This article is the first of two articles showing developers how they can unit test with the Dynamic Data Store, each with it&#39;s own separate technical approach. &amp;nbsp;Both approaches are valid, but the choice in approach may come down to how many different operations you are under taking with the Dynamic Data Store.&amp;nbsp; Part 1 deals with the direct mocking of the Dynamic Data Store objects while &lt;a href=&quot;/link/1970091e3424453ca0593f0a35f15fa8.aspx&quot;&gt;Part 2&lt;/a&gt; deals with abstraction of the Dynamic Data Store behind multiple interfaces.&lt;/p&gt;
&lt;h2&gt;The Repository Under Test&lt;/h2&gt;
&lt;p&gt;Lets assume we have a custom data object that we want to store in the Dynamic Data Store as follows:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public class MyCustomDataObject : IDynamicData
{
   public Identity Id { get; set; }
   public string UniqueText { get; set; }
   public string SomeOtherText { get; set; }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The object&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;code&gt;MyCustomDataObject&lt;/code&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;has an Identity property in order to meet the&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;code&gt;IDynamicData&lt;/code&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;interface which is the minimum requirement for storage within the Dynamic Data Store. &amp;nbsp;The UniqueText property should be unique to that instance of the&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;code&gt;MyCustomDataObject&lt;/code&gt;, the&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;code&gt;SomeOtherText&lt;/code&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;is just additional information to be stored with that record.&lt;/p&gt;
&lt;p&gt;A repository that handles the saving of&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;code&gt;MyCustomDataObject&lt;/code&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;records may look something like this:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public class MyCustomDataObjectRepository
{
   private readonly DynamicDataStore _dataStore;

   public MyCustomDataObjectRepository(DynamicDataStoreFactory dataStoreFactory)
   {
       _dataStore = dataStoreFactory.CreateStore(typeof(MyCustomDataObject));
   }

   public void Save(Guid id, string uniqueText, string someOtherText)
   {
       var matchingRecord = _dataStore.Find&amp;lt;MyCustomDataObject&amp;gt;(nameof(MyCustomDataObject.UniqueText), uniqueText).FirstOrDefault();

       if (matchingRecord != null &amp;amp;&amp;amp; !matchingRecord.Id.ExternalId.Equals(id))
       {
           throw new EntityExistsException($&quot;An entry already exists for the unique value of &#39;{uniqueText}&#39;.&quot;);
       }

       var recordToSave = Guid.Empty.Equals(id) ? CreateNewRecord() : _dataStore.Load&amp;lt;MyCustomDataObject&amp;gt;(Identity.NewIdentity(id));
       recordToSave.UniqueText = uniqueText;
       recordToSave.SomeOtherText = someOtherText;
       _dataStore.Save(recordToSave);
   }

   private static MyCustomDataObject CreateNewRecord()
   {
       return new MyCustomDataObject { Id = Identity.NewIdentity() };
   }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Unit Testing&lt;/h2&gt;
&lt;p&gt;In order to unit test the repository behaviour, we need to mock the Dynamic Data Store. &amp;nbsp;Optimizely does not provide any interfaces to assist with the writing of unit tests and we have to mock implementations instead.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;The mocking of implementations can come with it&#39;s own complications. &amp;nbsp;With a mock behaviour of &#39;strict&#39;, the mocked object will behave like the true implementation and will throw exceptions as expected. &amp;nbsp;With a mock behaviour of &#39;loose&#39;, no exceptions will be thrown and default values will be returned as necessary. &amp;nbsp;The default mock behavior of any mocked object is actually the strict behaviour.&lt;/p&gt;
&lt;p&gt;The strict mock behavior makes unit testing of the Dynamic Data Store impossible with exceptions being thrown during the set up of the test. &amp;nbsp;So in the following setup method, I have mocked the&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;code&gt;StoreDefinition&lt;/code&gt;, the&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;code&gt;DynamicDataStore&lt;/code&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;and the&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;code&gt;DynamicDataStoreFactory&lt;/code&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;with a loose mock behavior. &amp;nbsp;This is done by supplying the behavior in the mock constructor. The additional parameters applied to the mock constructors are default types which fulfil the constructor of the implementation being mocked.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;[TestFixture]
public class MyCustomDataObjectRepositoryTests
{
   private Mock&amp;lt;DynamicDataStoreFactory&amp;gt; _mockDynamicDataStoreFactory;

    private Mock&amp;lt;DynamicDataStore&amp;gt; _mockDynamicDataStore;

    private Mock&amp;lt;StoreDefinition&amp;gt; _mockStoreDefinition;

    private MyCustomDataObjectRepository _repository;

   [SetUp]
   public void SetUp()
   {
       _mockStoreDefinition = new Mock&amp;lt;StoreDefinition&amp;gt;(
           MockBehavior.Loose,
           string.Empty,
           new List&amp;lt;PropertyMap&amp;gt;(0),
           null);

        _mockDynamicDataStore = new Mock&amp;lt;DynamicDataStore&amp;gt;(
           MockBehavior.Loose,
           _mockStoreDefinition.Object);

       _mockDynamicDataStoreFactory = new Mock&amp;lt;DynamicDataStoreFactory&amp;gt;();
       _mockDynamicDataStoreFactory.Setup(x =&amp;gt; x.CreateStore(typeof(MyCustomDataObject)))
                                   .Returns(_mockDynamicDataStore.Object);

        _repository = new MyCustomDataObjectRepository(_mockDynamicDataStoreFactory.Object);
   }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span&gt;At this point we are now free to write unit tests as easily as we would do when mocking interfaces. &amp;nbsp;In the following test example, an existing record is created and is set to be the result of the mocked&amp;nbsp;&lt;/span&gt;&lt;code&gt;Find&lt;/code&gt;&lt;span&gt;&amp;nbsp;method against the&amp;nbsp;&lt;/span&gt;&lt;code&gt;DynamicDataStore&lt;/code&gt;&lt;span&gt;. When we try to save a record with a matching uniqueText, the desired outcome is that an&amp;nbsp;&lt;/span&gt;&lt;code&gt;EntityExistsException&lt;/code&gt;&lt;span&gt;&amp;nbsp;is thrown and our assertion is constructed to prove that.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;[Test]
public void GivenUniqueTextExistsAgainstAnotherEntity_ThenAnEntityExistsExceptionShouldBeThrown()
{
   // Arrange
   var uniqueText = &quot;i-am-unique&quot;;
   var someOtherText = &quot;some-other-text&quot;;

   var existingRecord = new MyCustomDataObject
   {
       Id = Guid.NewGuid(),
       UniqueText = uniqueText,
       SomeOtherText = &quot;original-other-text&quot;
   };

    _mockDynamicDataStore.Setup(x =&amp;gt; x.Find&amp;lt;MyCustomDataObject&amp;gt;(It.IsAny&amp;lt;string&amp;gt;(), It.IsAny&amp;lt;object&amp;gt;()))
                           .Returns(new List&amp;lt;MyCustomDataObject&amp;gt; { existingRecord });

    // Assert
   Assert.Throws&amp;lt;EntityExistsException&amp;gt;(() =&amp;gt; _repository.Save(Guid.Empty, uniqueText, someOtherText));
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;In order to unit test against the Dynamic Data Store, you must do the following in order to write unit tests as normal:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Mock the&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;code&gt;StoreDefinition&lt;/code&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;with&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;code&gt;MockBehavior.Loose&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Mock the&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;code&gt;DynamicDataStore&lt;/code&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;with&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;code&gt;MockBehavior.Loose&lt;/code&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;and pass in the mock of the&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;code&gt;StoreDefinition&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Mock the&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;code&gt;DynamicDataStoreFactory&lt;/code&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;with either mock behavior and set up the&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;code&gt;CreateStore&lt;/code&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;method to return the mocked&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;code&gt;DynamicDataStore&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To be continued in &lt;a href=&quot;/link/1970091e3424453ca0593f0a35f15fa8.aspx&quot;&gt;Part 2&lt;/a&gt;...&lt;/p&gt;</id><updated>2022-05-26T11:01:24.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>A Robots.Txt Handler for Optimizely CMS 12</title><link href="https://world.optimizely.com/blogs/mark-stott/dates/2021/12/a-robots-txt-handler-for-optimizely-cms-12/" /><id>&lt;h2&gt;Stott.Optimizely.RobotsHandler v1.0.1 Released&lt;/h2&gt;
&lt;p&gt;Stott.Optimizely.RobotsHandler is a new robots.txt handler for Optimizely CMS 12 that fully supports multi-site builds with potentially different robots.txt content to be delivered by site. This package is inspired by work previously delivered with &lt;a href=&quot;https://github.com/made-to-engage/MadeToEngage.RobotsTxtHandler&quot;&gt;&lt;span&gt;POSSIBLE.RobotsTxtHandler&lt;/span&gt;&lt;/a&gt; which was built for CMS 11.&amp;nbsp; Stott.Optimizely.RobotsHandler has been built from the ground up initially as a learning exercise building on lessons learned in my previous post: &lt;a href=&quot;/link/aa901c2390b44fce99cfd6b8838262d5.aspx&quot;&gt;Extending The Admin Interface in Optimizely CMS 12&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;The Interface&lt;/h2&gt;
&lt;p&gt;The interface is built using a standard .NET 5.0 MVC Controller with a Razor view with a small supporting JS file that is compiled as a Razor Class Library.&amp;nbsp; The benefit of building this as a Razor Class Library is that Nuget only needs to provide the DLL keeping your solution otherwise clean of any artefacts from the package.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;The UI is a single page using Bootstrap 5.0 and JQuery that renders a complete list of all sites configured within the CMS instance.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/21722c8257fd45bba76d45ce8bb116c7.aspx?1639608020667&quot; width=&quot;1101&quot; height=&quot;298&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The content of the robots.txt for any site is shown in a modal dialog and saved via an API call that stores the robots.txt content into the Dynamic Data Store.&amp;nbsp; Custom tables have not been used as the expection is that there will be a 1-to-1 relationship between sites and their robots.txt content.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/f74a5cdd73924c8db7ce706a355fe946.aspx&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Installation &amp;amp; Configuration&lt;/h2&gt;
&lt;p&gt;Installation is straight forward, the &lt;strong&gt;Stott.Optimizely.RobotsHandler&lt;/strong&gt; package can be installed either from the Optimizely nuget feed or from the nuget.org feed.&amp;nbsp; You will then need to add the following lines to the Startup class in your .NET 5.0 solution:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();
    services.AddRobotsHandler();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The call to services.AddRazorPages() is a standard .NET 5.0 call to ensure razor pages are included in your solution.&lt;/p&gt;
&lt;p&gt;The call to services.AddRobotsHandler() sets up the dependency injection requirements for the RobotsHandler solution and is required to ensure the solution works as intended. This works by following the Services Extensions pattern defined by microsoft.&lt;/p&gt;
&lt;h2&gt;Resolving robots.txt content&lt;/h2&gt;
&lt;p&gt;A standard controller is configured to respond to requests to &lt;a href=&quot;http://www.example.com/robots.txt&quot;&gt;www.example.com/robots.txt&lt;/a&gt;. When recieving the request, the controller interogates the domain of the request and uses this to resolve the relevant site and the returns the robots.txt content for that site.&amp;nbsp; If not content has been previously defined for the site, the the default content will be returned as follows:&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;User-agent: *
Disallow: /episerver/
Disallow: /utils/&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Contributing and Licencing&lt;/h2&gt;
&lt;p&gt;Stott.Optimizely.RobotsHandler has been built and uses the MIT licence.&amp;nbsp; If you find any defects with the package, then please log them as issues on the the repositories &lt;a href=&quot;https://github.com/GeekInTheNorth/Stott.Optimizely.RobotsHandler/issues&quot;&gt;issues&lt;/a&gt; page.&amp;nbsp;If you would like to contribute to changes for the solution, then feel free to clone the repository and submit a pull request against the develop branch.&lt;/p&gt;</id><updated>2021-12-15T22:38:34.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Adding Secure Headers in Optimizely CMS 12</title><link href="https://world.optimizely.com/blogs/mark-stott/dates/2021/11/adding-secure-headers-in-optimizely-cms-12/" /><id>&lt;p&gt;I have previously blogged about this in a pure .NET 5.0 context on my own blog here: &lt;a href=&quot;https://stotty.azurewebsites.net/article/secure_headers_in__net_5_0&quot;&gt;Secure Headers in .NET 5.0&lt;/a&gt;. In this version of this post I have adapted the solution to work with an Optimizely CMS Build.&lt;/p&gt;
&lt;p&gt;I have been building .NET 5.0 websites for personal projects as well as reviewing the move from .NET 4.8 to .NET 5.0 by Optimizely and seeing the evolution of the CMS solution. &amp;nbsp;In any website build it is best practice to remove any headers that your website may produce which expose the underlying technology stack and version. &amp;nbsp;This is known as information leakage and provides malicious actors with information that allows them to understand the security flaws in the hosting technologies utilized by your website. &amp;nbsp;It is also best practice to provide headers which instruct the user&#39;s browser as to how your website can use third parties and be used by third parties in order to offer the best protection for the user.&lt;/p&gt;
&lt;p&gt;.NET 5.0 and .NET Core 3.1 follow a common pattern in how you build websites. &amp;nbsp;Web.config is meant to be a thing of the past with configuration of the site moving to appsettings.json and code. &amp;nbsp;A good place to add security headers to your requests is to create a security header middleware. However the routing of the CMS content does not pass through this middleware so I&#39;ve fallen back to Action Attributes:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;namespace MyProject.Features.Security
{
    using Microsoft.AspNetCore.Mvc.Filters;

    public class SecurityHeaderActionAttribute : ActionFilterAttribute
    {
        public override void OnResultExecuting(ResultExecutingContext context)
        {
            base.OnResultExecuting(context);

            context.HttpContext.Response.Headers.Add(&quot;X-Frame-Options&quot;, &quot;SAMEORIGIN&quot;);
            context.HttpContext.Response.Headers.Add(&quot;X-Xss-Protection&quot;, &quot;1; mode=block&quot;);
            context.HttpContext.Response.Headers.Add(&quot;X-Content-Type-Options&quot;, &quot;nosniff&quot;);
            context.HttpContext.Response.Headers.Add(&quot;Referrer-Policy&quot;, &quot;no-referrer&quot;);

            context.HttpContext.Response.Headers.Add(&quot;Content-Security-Policy&quot;, &quot;default-src &#39;self&#39;; style-src &#39;self&#39; &#39;unsafe-inline&#39; https://cdn.jsdelivr.net; script-src &#39;self&#39; &#39;unsafe-inline&#39; https://cdn.jsdelivr.net; img-src &#39;self&#39; data: https:; frame-src &#39;self&#39; https://www.youtube-nocookie.com/;&quot;);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span&gt;I did find that I couldn&#39;t remove the server and x-powered-by headers using this method. &amp;nbsp;As it turns out, these are added by the hosting technology, in this case IIS. &amp;nbsp;The only way to remove these headers was to add a minimal web.config file to the web solution that contained just enough configuration to remove these headers.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&amp;gt;
&amp;lt;configuration&amp;gt;
   &amp;lt;system.webServer&amp;gt;
       &amp;lt;httpProtocol&amp;gt;
           &amp;lt;customHeaders&amp;gt;
               &amp;lt;remove name=&quot;X-Powered-By&quot; /&amp;gt;
           &amp;lt;/customHeaders&amp;gt;
       &amp;lt;/httpProtocol&amp;gt;
       &amp;lt;security&amp;gt;
           &amp;lt;requestFiltering removeServerHeader=&quot;true&quot; /&amp;gt;
       &amp;lt;/security&amp;gt;
   &amp;lt;/system.webServer&amp;gt;
&amp;lt;/configuration&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span&gt;I spent a good while looking for this solution, almost every solution I could find was based on hosting within Kestrel rather than IIS. &amp;nbsp;If you are hosting with Kestrel, then you can remove the server header by updating your CreateHostBuilder method in program.cs to set the options for Kestrel to exclude the server header.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public static IHostBuilder CreateHostBuilder(string[] args, bool isDevelopment)
{
   return Host.CreateDefaultBuilder(args)
              .ConfigureCmsDefaults()
              .ConfigureWebHostDefaults(webBuilder =&amp;gt;
              {
                  webBuilder.UseStartup&amp;lt;Startup&amp;gt;();
                  webBuilder.UseKestrel(options =&amp;gt; { options.AddServerHeader = false; });
              });
}&lt;/code&gt;&lt;/pre&gt;</id><updated>2021-11-19T09:06:03.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Optimizely Search Wildcard Queries and Best Bets</title><link href="https://world.optimizely.com/blogs/mark-stott/dates/2021/11/optimizely-search-wildcard-queries-and-best-bets/" /><id>&lt;p&gt;In a recent client build, I have been tasked with updating their search to use wild card searches.&amp;nbsp; I had read a number of posts pointing to the same solution as detailed by &lt;span&gt;Joel Abrahamsson&#39;s 2012 blog post,&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;http://joelabrahamsson.com/wildcard-queries-with-episerver-find/&quot;&gt;Wildcard Queries with Episerver Find&lt;/a&gt; and Drew Null&#39;s post &lt;a href=&quot;/link/1a47337598024ccea1e43e86b1f5586c.aspx&quot;&gt;Episerver Find Wildcard Queries and Best Bets&lt;/a&gt;. These solutions included building an extension method that built a bool query wrapping a wild card query. This included complications in how to get best bets to work with wildcards.&lt;/p&gt;
&lt;p&gt;While the solution did work, after reviewing the performance of the query and the structure of the query being sent to Optimizely Search and Navigation, I discovered that the solution was actually much simpler and did not need any new scaffolding of custom query building.&amp;nbsp; The For method has overloads which exposes the QueryStringQuery object that allows you to customise the query behaviour.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;private ITypeSearch&amp;lt;T&amp;gt; GetQuerySearch&amp;lt;T&amp;gt;(
	string query, 
	bool isWildCardSearch) where T : MyVariant
{
	var specificQuery = isWildCardSearch ? $&quot;*{query}*&quot; : query;
	return _findClient.Search&amp;lt;T&amp;gt;()
                      .For(query, options =&amp;gt;
                      {
                          options.Query = specificQuery;
                          options.AllowLeadingWildcard = isWildCardSearch;
                          options.AnalyzeWildcard = isWildCardSearch;
                          options.RawQuery = query;
                      })
                      .InField(f =&amp;gt; f.FieldOne, _settings.FieldOneBoost)
                      .InField(f =&amp;gt; f.FieldTwo, _settings.FieldTwoBoost)
                      .InField(f =&amp;gt; f.FieldThree, _settings.FieldThreeBoost)
                      .InField(f =&amp;gt; f.FieldFour)
                      .InField(f =&amp;gt; f.FieldFive)
                      .InField(f =&amp;gt; f.FieldSix)
                      .InField(f =&amp;gt; f.FieldSeven)
                      .InField(f =&amp;gt; f.FieldEight)
                      .UsingSynonyms()
                      .UsingAutoBoost(TimeSpan.FromDays(_settings.AutoBoostTimeSpanDays))
                      .ApplyBestBets()
                      .FilterForVisitor();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By passing in the wild card version of the query string into options.Query and setting options.AllowLeadingWildcard and options.AnalyzeWildcard to true was all I needed for wildcard search to be functional.&amp;nbsp; I also passed in the unaltered query into options.RawQuery but this was not required in order to make Best Bets work.&lt;/p&gt;
&lt;p&gt;The main downside to this approach is that synonym functionality no longer worked.&amp;nbsp; The query with the wildcards would never match a synonym but it would work with best bets.&amp;nbsp; I resolved this by using a Multi Search query and passed in the unaltered query and then the wild card query.&amp;nbsp; In Multi Search, each result set is returned in the same order in which it has been defined and they are packaged in a single API call.&amp;nbsp; It was then a simple case of selecting the first result set with at least one match.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;var searchResult _findClient.MultiSearch&amp;lt;DentalVariantProjectionModel&amp;gt;()
                                            .Search&amp;lt;MyVariant, MyProjectionModel&amp;gt;(x =&amp;gt; GetQuerySearch(query, false))
                                            .Search&amp;lt;MyVariant, MyProjectionModel&amp;gt;(x =&amp;gt; GetQuerySearch(query, true))
                                            .GetResult();

var resultToUse = searchResult.FirstOrDefault(x =&amp;gt; x.TotalMatching &amp;gt; 0) ?? searchResult.First();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Performance wise, the difference in sending two queries in a multi search request was negligable compared to a sending a single query.&lt;/p&gt;</id><updated>2021-11-19T08:46:10.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Setting Up Search &amp; Navigation in .NET 5.0</title><link href="https://world.optimizely.com/blogs/mark-stott/dates/2021/10/setting-up-search--navigation-in--net-5-0/" /><id>&lt;p&gt;UPDATE: The documentation has now been updated and can be found here: &lt;a href=&quot;/link/5d022ec58a134858bd1e8341bad44f10.aspx&quot;&gt;https://world.optimizely.com/documentation/developer-guides/search-navigation/getting-started/creating-your-project/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Optimizely CMS for .NET 5.0 is a very exciting thing.&amp;nbsp; Recently the .NET Core Preview documentation was archived and some of that documentation was merged into the live developer guides. Optimizely have a huge amount of developer documentation and it&#39;s going to take a while for this content to be updated for .NET 5.0.&amp;nbsp; Sadly the current documentation for Optimizely Search and Navigation is still focused on the .NET 4.x world&lt;/p&gt;
&lt;p&gt;1. Add the EPiServer.Find.CMS nuget package to your .NET 5.0 solution.&lt;/p&gt;
&lt;p&gt;2. Generate a new Search and Navigation index at &lt;a href=&quot;https://find.episerver.com/&quot;&gt;https://find.episerver.com/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;3. Configure the index details in app.config...&lt;/p&gt;
&lt;p&gt;When you create the index, you will be shown this snippet to add to your web.config:&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;&amp;lt;configuration&amp;gt;
    &amp;lt;configSections&amp;gt;
        &amp;lt;section
            name=&quot;episerver.find&quot; type=&quot;EPiServer.Find.Configuration, EPiServer.Find&quot; requirePermission=&quot;false&quot;/&amp;gt;
    &amp;lt;/configSections&amp;gt;
    &amp;lt;episerver.find
        serviceUrl=&quot;https://demo01.find.episerver.net/RXQGZ5QpXU9cuRSN2181hqA77ZFrUq2e/&quot;
        defaultIndex=&quot;yourname_indexname&quot;/&amp;gt;
&amp;lt;/configuration&amp;gt; &lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You will instead add this to appsettings.json like so:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;{
    &quot;EPiServer&quot;: {
        &quot;Find&quot;: {
            &quot;ServiceUrl&quot;: &quot;https://demo01.find.episerver.net/RXQGZ5QpXU9cuRSN2181hqA77ZFrUq2e/&quot;,
            &quot;DefaultIndex&quot;: &quot;yourname_indexname&quot;
        } 
    } 
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;4. Configure your startup.cs to include the find configuration:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public void ConfigureServices(IServiceCollection services)
{
    services.AddCmsAspNetIdentity&amp;lt;ApplicationUser&amp;gt;();
    services.AddMvc();
    services.AddCms();
    services.AddFind();
 }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You should now have everything you need to write your search queries as normal:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;var searchResult = _findClient.Search&amp;lt;SitePageData&amp;gt;()
                              .For(request.SearchText)
                              .UsingSynonyms()
                              .ApplyBestBets()
                              .Skip(skip)
                              .Take(pageSize)
                              .GetContentResult();&lt;/code&gt;&lt;/pre&gt;</id><updated>2021-10-01T15:13:11.0000000Z</updated><summary type="html">Blog post</summary></entry></feed>