<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Ynze]]></title><description><![CDATA[Web Developer • Optimizely MVP • Platform Lead]]></description><link>https://blog.ynzen.com</link><generator>RSS for Node</generator><lastBuildDate>Fri, 10 Apr 2026 17:12:50 GMT</lastBuildDate><atom:link href="https://blog.ynzen.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[How to add a custom property in Optimizely Graph]]></title><description><![CDATA[In the Optimizely CMS content can be synchronized to the Optimizely Graph service for it then to be exposed by the GraphQL API. In some cases, you may want to add or adjust a property when the content is being synchronized.
An example of this is when...]]></description><link>https://blog.ynzen.com/how-to-add-a-custom-property-in-optimizely-graph</link><guid isPermaLink="true">https://blog.ynzen.com/how-to-add-a-custom-property-in-optimizely-graph</guid><category><![CDATA[optimizely]]></category><category><![CDATA[cms]]></category><category><![CDATA[cms development]]></category><category><![CDATA[GraphQL]]></category><dc:creator><![CDATA[Ynze Nunnink]]></dc:creator><pubDate>Thu, 09 May 2024 00:29:14 GMT</pubDate><content:encoded><![CDATA[<p>In the Optimizely CMS content can be synchronized to the Optimizely Graph service for it then to be exposed by the GraphQL API. In some cases, you may want to add or adjust a property when the content is being synchronized.</p>
<p>An example of this is when media files have a file size property that represents the file size as the number of bytes. The UI that displays this information will need to format the bytes (e.g. <code>10000000</code>) into a readable string such as <code>10MB</code>. The server can take on this responsibility by adding another property to the Graph schema that will display the formatted string, which ensures consistency across the applications that require the formatted file size.</p>
<p>The code below will add a custom API model property to the Graph schema called <code>FileSizeFormatted</code>, which uses the <code>GetValue</code> method to get the <code>FileSize</code> property and format it to a readable string using the custom extension method <code>ToFormattedString()</code>.</p>
<pre><code class="lang-csharp">[<span class="hljs-meta">ServiceConfiguration(typeof(IContentApiModelProperty), Lifecycle = ServiceInstanceScope.Singleton)</span>]
<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">FileSizeContentApiModelProperty</span> : <span class="hljs-title">IContentApiModelProperty</span>
{
    <span class="hljs-comment">//To override an existing property you can use the same name.</span>
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> Name =&gt; <span class="hljs-string">"FileSizeFormatted"</span>;

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">object</span> <span class="hljs-title">GetValue</span>(<span class="hljs-params">ContentApiModel contentApiModel</span>)</span>
    {
        <span class="hljs-comment">//Validate that the model contains a FileSize property</span>
        <span class="hljs-comment">//Another option is to validate the content type</span>
        <span class="hljs-keyword">if</span> (!contentApiModel.Properties.TryGetValue(<span class="hljs-string">"FileSize"</span>, <span class="hljs-keyword">out</span> <span class="hljs-keyword">var</span> property))
            <span class="hljs-keyword">return</span> <span class="hljs-keyword">string</span>.Empty;

        <span class="hljs-keyword">if</span> (<span class="hljs-keyword">int</span>.TryParse(property.ToString(), <span class="hljs-keyword">out</span> <span class="hljs-keyword">var</span> fileSize))
            <span class="hljs-keyword">return</span> fileSize.ToFormattedString(); <span class="hljs-comment">//Custom extension method</span>

        <span class="hljs-keyword">return</span> <span class="hljs-keyword">string</span>.Empty;
    }
}
</code></pre>
<p>Then after running the <strong>content synchronization scheduled job</strong>, the GraphQL schema and content has been updated with the new property.</p>
<p><strong>Query</strong></p>
<pre><code class="lang-graphql">{
  PdfFile(<span class="hljs-symbol">limit:</span> <span class="hljs-number">100</span>) {
    items {
       FileSize
       FileSizeFormatted
    }
  }
}
</code></pre>
<p><strong>Result</strong></p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"data"</span>: {
    <span class="hljs-attr">"PdfFile"</span>: {
      <span class="hljs-attr">"items"</span>: [
        {
          <span class="hljs-attr">"FileSize"</span>: <span class="hljs-number">2107621</span>,
          <span class="hljs-attr">"FileSizeFormatted"</span>: <span class="hljs-string">"2mb"</span>
        },
        {
          <span class="hljs-attr">"FileSize"</span>: <span class="hljs-number">1070229</span>,
          <span class="hljs-attr">"FileSizeFormatted"</span>: <span class="hljs-string">"1045kb"</span>
        }
      ]
    }
  }
}
</code></pre>
<p>This is a basic example of creating a custom API model property, and there is room for improvement, because any custom property is added to all schema types. It's still early days for Optimizely Graph, so we can expect that property customization will be improved in the future.</p>
]]></content:encoded></item><item><title><![CDATA[Translating content blocks recursively in Optimizely CMS]]></title><description><![CDATA[Translating a lot of pages in Optimizely can become a real chore for content editors because pages generally have a lot of child content in the form of blocks. To make things more complicated, blocks can recursively also have child blocks. An average...]]></description><link>https://blog.ynzen.com/translating-content-blocks-recursively-in-optimizely-cms</link><guid isPermaLink="true">https://blog.ynzen.com/translating-content-blocks-recursively-in-optimizely-cms</guid><category><![CDATA[optimizely]]></category><category><![CDATA[cms]]></category><category><![CDATA[translation]]></category><category><![CDATA[optimizely-cms]]></category><dc:creator><![CDATA[Ynze Nunnink]]></dc:creator><pubDate>Fri, 14 Jul 2023 06:26:32 GMT</pubDate><content:encoded><![CDATA[<p>Translating a lot of pages in Optimizely can become a real chore for content editors because pages generally have a lot of child content in the form of blocks. To make things more complicated, blocks can recursively also have child blocks. An average page can easily have 10 or more blocks depending on the amount of content that is displayed.</p>
<p>By default, blocks have to be translated individually. This means that translating a page will require the editor to do a lot of clicking to translate the content. Luckily there is a sneaky configuration that makes it a lot quicker.</p>
<h2 id="heading-the-language-manager">The Language Manager</h2>
<p>The language manager can be installed as a <a target="_blank" href="https://nuget.optimizely.com/package/?id=EPiServer.Labs.LanguageManager">NuGet package</a> that does not come with the base installation of Optimizely CMS. It's very useful for managing the language variations for your content. There are options available for auto-translating and duplicating content into other languages, which by default does not affect child content.</p>
<p><a target="_blank" href="https://support.optimizely.com/hc/en-us/articles/4413199657741-Optimizely-Languages-app">Optimizely Languages app – Support Help Center</a></p>
<h3 id="heading-translating-content">Translating content</h3>
<p>To give an example of how the translation works by default, I will use the Language Manager to duplicate content from one language to another on a page that has blocks inside it.</p>
<p>I'm also using the <a target="_blank" href="https://docs.developers.optimizely.com/content-management-system/docs/projects">projects feature</a> to get a clear view of what content is being changed. I recommend using projects when translating content because it can also be used to export and import translation (XLIFF) files.</p>
<p>I've created a page called 'About us' and added some blocks.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1689313586392/95ef3c67-e2c7-45be-804e-383cf0861600.png" alt class="image--center mx-auto" /></p>
<p>The 'Accordion - about us' block also has some child blocks.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1689313627248/c245a319-4828-4a14-9b9e-70c9138a0fee.png" alt class="image--center mx-auto" /></p>
<p>Then I use the 'Duplicate content' feature to translate the 'About us' page to Japanese.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1689308778105/b23f2e2b-0e62-4cf0-ab63-c439c0d6d6fe.png" alt class="image--center mx-auto" /></p>
<p>When I look into my project, I can see that only the About page itself has been created in the Japanese language, while the blocks inside it are still only available in the Australian language.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1689313896097/c11d96b3-4ea2-4858-a912-e0002798f055.png" alt class="image--center mx-auto" /></p>
<p>Next, I'll show you how to configure the Language Manager to automatically include child blocks recursively in the translations.</p>
<h2 id="heading-configuring-the-content-types-that-should-include-blocks">Configuring the content types that should include blocks</h2>
<p>There is a somewhat hidden configuration that will make the Language Manager include blocks recursively using some of its translation features. To do this, we just need to configure the content types that should be checked for child blocks when being translated. That can be done in either the <code>AppSettings.json</code> or <code>Startup.cs</code> files, as can be seen in the examples below.</p>
<p><strong>AppSettings.json</strong></p>
<pre><code class="lang-json">{  
    <span class="hljs-attr">"Episerver"</span>: {  
        <span class="hljs-attr">"CmsUI"</span>: {  
            <span class="hljs-attr">"LanguageManager"</span>: {  
                <span class="hljs-attr">"TranslateOrCopyContentAreaChildrenBlockForTypes"</span> : [  
                  <span class="hljs-string">"MyProject.Models.Pages.StandardPage"</span>,  
                  <span class="hljs-string">"MyProject.Models.Pages.HomePage"</span>  
                ]  
            }  
        }  
    }  
}
</code></pre>
<p><strong>Startup.cs</strong></p>
<pre><code class="lang-csharp"><span class="hljs-keyword">using</span> EPiServer.Labs.LanguageManager;

<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">Startup</span>
{
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">ConfigureServices</span>(<span class="hljs-params">IServiceCollection services</span>)</span>
    {
        services.Configure&lt;LanguageManagerOptions&gt;(o =&gt;
        {
            o.TranslateOrCopyContentAreaChildrenBlockForTypes.Add(<span class="hljs-string">"MyProject.Models.Pages.StandardPage"</span>);
            o.TranslateOrCopyContentAreaChildrenBlockForTypes.Add(<span class="hljs-string">"MyProject.Models.Pages.HomePage"</span>);
        });
    }
}
</code></pre>
<p>Most of the time you will only need to do this for page types. Note that you need to include the entire namespace path, plus the name of the class.</p>
<h3 id="heading-the-result">The result</h3>
<p>The result is that when I duplicate the content using the Language Manager, as I've shown earlier in this article, the content of the blocks is automatically created in the target language.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1689314167401/beddf218-2e8b-49f6-a344-e157bb5f63cd.png" alt class="image--center mx-auto" /></p>
<p>Something to note is that if your block is published in the language that you're copying from, then the block will also be published automatically in the new language. It will not automatically publish your pages, which means you can still review the content fully before publishing it on the site.</p>
<p>When I was asked if there was an easier way to bulk translate content, I thought that there wasn't going to be an easy approach. Luckily I found the Language Manager configuration that I had never heard about, even though it is in the documentation <a target="_blank" href="https://docs.developers.optimizely.com/content-management-system/reference/optimizely-languages#configure-translate-children-blocks">here</a>. It's easy to miss, so I figured I should share it here!</p>
]]></content:encoded></item><item><title><![CDATA[Optimizely community meetup - Sept 29 (virtual + Melbourne)]]></title><description><![CDATA[Super excited to be presenting this Thursday the 29th of September at the Optimizely community meetup. For the full details and RSVP's see the official Meetup page:
https://www.meetup.com/optimizely-melbourne/events/288300244/
Meetup details
Luminary...]]></description><link>https://blog.ynzen.com/optimizely-community-meetup-sept-29</link><guid isPermaLink="true">https://blog.ynzen.com/optimizely-community-meetup-sept-29</guid><category><![CDATA[optimizely]]></category><category><![CDATA[cms]]></category><category><![CDATA[Meetup]]></category><category><![CDATA[events]]></category><dc:creator><![CDATA[Ynze Nunnink]]></dc:creator><pubDate>Tue, 27 Sep 2022 04:48:08 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1664253719608/4xb-KEsj-.JPG" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Super excited to be presenting this Thursday the 29th of September at the Optimizely community meetup. For the full details and RSVP's see the official Meetup page:
<a target="_blank" href="https://www.meetup.com/optimizely-melbourne/events/288300244/">https://www.meetup.com/optimizely-melbourne/events/288300244/</a></p>
<h2 id="heading-meetup-details">Meetup details</h2>
<p>Luminary and Optimizely are warmly welcoming back the Optimizely community with a mega meetup, including three great in-person talks (remote enabled). Hear about the latest platform developments as well as seeing some truly incredible projects in action.</p>
<p>This is an event for the entire Optimizely developer and user community.
There will be three 20-minute short talks, each with social breaks in between:</p>
<p>A showcase of Optimizely multi-site best-practices, featuring Fuso Trucks from Ynze Nunnink, Luminary Optimizely Lead &amp; Optimizely MVP
The evolution of personalisation – Nicola Ayan, Director of Technology and Growth, Optimizely APJ
Unveiling of exciting new developments from Optimizely (plus two books to give away!)
For those attending the event in person, Luminary is catering with substantial food and premium drink options.</p>
<p>The three best questions from guests (as voted by our speakers) will each receive a $100 Prezzee Gift Card, redeemable at more than 250 major retailers Australia-wide.</p>
<p>We are so excited to share this event with you!</p>
<h2 id="heading-how-to-join-online">How to join online?</h2>
<p>Keep an eye on the <a target="_blank" href="https://www.meetup.com/optimizely-melbourne/events/288300244/">meetup page</a> for the latest details on how to join. It will be a zoom meeting and you will be able to communicate and ask questions!</p>
]]></content:encoded></item><item><title><![CDATA[How to bypass the content creation view in Optimizely]]></title><description><![CDATA[Something that has come up a couple of times in the last few year is feedback from content editors about the editing view that comes up when creating new content. The view is based on properties (or content fields) that are required, so you cannot cr...]]></description><link>https://blog.ynzen.com/how-to-bypass-the-content-creation-view-in-optimizely</link><guid isPermaLink="true">https://blog.ynzen.com/how-to-bypass-the-content-creation-view-in-optimizely</guid><category><![CDATA[optimizely]]></category><category><![CDATA[optimizely content cloud]]></category><category><![CDATA[cms]]></category><category><![CDATA[C#]]></category><dc:creator><![CDATA[Ynze Nunnink]]></dc:creator><pubDate>Fri, 23 Sep 2022 04:43:51 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1663739377357/O8tj5vR5d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Something that has come up a couple of times in the last few year is feedback from content editors about the editing view that comes up when creating new content. The view is based on properties (or content fields) that are required, so you cannot create the content before you've provided values for those properties. The view only comes up if there are any required properties. </p>
<p>It can be a real nuisance because it's at least one extra step to complete every time you need to create a piece of content. When working with lots of pages it can be a real time waster. In some cases the editor just wants to set up the IA without having to worry about all the required content on each page. In practice it's not that important to provide the required values when creating the content, but it is important that the required values are provided before the content can be published.</p>
<h2 id="heading-creating-a-metadata-extender-class">Creating a metadata extender class</h2>
<p>The cool thing is that there is actually a way to skip the initial 'create content' editing view. The trick is to dynamically adjust the metadata of the properties to make them non-required when the content is being newly created. We need to make sure that the properties are still required <em>after</em> the content has been created, so that it cannot be published before the values are provided.</p>
<pre><code class="lang-C#"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">SkipContentCreateViewMetadataExtender</span> : <span class="hljs-title">IMetadataExtender</span>
{
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">ModifyMetadata</span>(<span class="hljs-params">ExtendedMetadata metadata, IEnumerable&lt;Attribute&gt; attributes</span>)</span>
    {
        <span class="hljs-comment">// When content is being created the content link is 0</span>
        <span class="hljs-keyword">if</span> (metadata.Model <span class="hljs-keyword">is</span> IContent data &amp;&amp; data.ContentLink.ID == <span class="hljs-number">0</span>)
        {
            <span class="hljs-keyword">foreach</span> (<span class="hljs-keyword">var</span> modelMetadata <span class="hljs-keyword">in</span> metadata.Properties)
            {
                <span class="hljs-keyword">var</span> property = (ExtendedMetadata)modelMetadata;

                property.ShowForEdit = <span class="hljs-literal">false</span>;

                <span class="hljs-comment">//If property if required you won't be able to create the content</span>
                <span class="hljs-comment">//Therefore set required to false</span>
                <span class="hljs-keyword">if</span> (property.IsRequired)
                    property.IsRequired = <span class="hljs-literal">false</span>;
            }
        }
    }
}
</code></pre>
<h3 id="heading-registering-the-metadata-extender">Registering the metadata extender</h3>
<p>Then in order for the metadata extender to work it needs to be registered using an initialization module. You can specify the content type that the metadata extender should be used for, in case you only want this to be used for a particular content type, and it also works inherently with base classes. In this case I used the <code>ContentData</code> type, which means that the extender will be executed for practically any content. If for example you only want to use this for pages then you can change it to <code>PageData</code>.</p>
<pre><code class="lang-C#">[<span class="hljs-meta">ModuleDependency(typeof(InitializationModule))</span>]
<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">MetadataHandlerInitialization</span> : <span class="hljs-title">IInitializableModule</span>
{
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Initialize</span>(<span class="hljs-params">InitializationEngine context</span>)</span>
    {
        <span class="hljs-keyword">var</span> registry = context.Locate.Advanced.GetInstance&lt;MetadataHandlerRegistry&gt;();
        registry.RegisterMetadataHandler(<span class="hljs-keyword">typeof</span>(ContentData), <span class="hljs-keyword">new</span> SkipContentCreateViewMetadataExtender());
    }

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Uninitialize</span>(<span class="hljs-params">InitializationEngine context</span>)</span>{ }
}
</code></pre>
<h2 id="heading-the-outcome">The outcome</h2>
<p>Now when you create any new content it will only the title will be required, making it much quicker to create a large amount of pages and blocks. On top of that required properties are still required before the content can be published.
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1663738538761/KeYOMxnOb.png" alt="image.png" /></p>
<h2 id="heading-closing-thoughts">Closing thoughts</h2>
<p>Quick shoutout to <a target="_blank" href="https://www.david-tec.com/2017/07/hiding-required-properties-on-the-create-new-page-in-episerver/">this blog post by David Knipe</a>, which inspired me to create the solution described in this article. Go check it out!</p>
]]></content:encoded></item><item><title><![CDATA[Conditional content properties in Optimizely]]></title><description><![CDATA[In a recent Optimizely project we were building multiple sites that could share content types between them. One problem we encountered was that some of the blocks and pages required slightly different content, and therefore separate content fields (k...]]></description><link>https://blog.ynzen.com/conditional-content-properties-in-optimizely</link><guid isPermaLink="true">https://blog.ynzen.com/conditional-content-properties-in-optimizely</guid><category><![CDATA[optimizely content cloud]]></category><category><![CDATA[optimizely]]></category><category><![CDATA[cms]]></category><category><![CDATA[C#]]></category><dc:creator><![CDATA[Ynze Nunnink]]></dc:creator><pubDate>Wed, 21 Sep 2022 01:43:47 GMT</pubDate><content:encoded><![CDATA[<p>In a recent Optimizely project we were building multiple sites that could share content types between them. One problem we encountered was that some of the blocks and pages required slightly different content, and therefore separate content fields (known as properties). The simplest solution is to have 2 properties and if one is empty then the other one is used. The page or block can then display the content accordingly depending on what the content editor has provided.</p>
<p>The problem with that approach is that it becomes increasingly complex for the content editor and can result in content inconsistencies on the website. To solve this problem we developed a solution that could show or hide properties depending on the site that the shared content type is used. The use case in this article is for the properties to be conditional based on the site, however the same techniques can be applied to other similar use cases.</p>
<h2 id="heading-transform-metadata-with-custom-attributes">Transform metadata with custom attributes</h2>
<p>Metadata is very important and useful in the Optimizely CMS because it controls how content is rendered in the editing view. The metadata of a property contains a bunch of setting values for things such as if the property is required, disabled or even hidden entirely. There are multiple ways to influence the metadata of a property — one option is to create a custom attribute that implements the <code>IDisplayMetadataProvider</code> interface. </p>
<h3 id="heading-hide-a-property-based-on-the-current-site">Hide a property based on the current site</h3>
<p>What we're trying to achieve here is to show the property only when it is rendered for a specific site. The code sample below contains a custom attribute that accepts a parameter containing names of the sites that the property should be available for. The <code>CreateDisplayMetadata</code> method from the <code>IDisplayMetadataProvider</code> interface is then used to dynamically update the metadata to only display the property if the current site matches those sites. </p>
<pre><code class="lang-C#">[<span class="hljs-meta">AttributeUsage(AttributeTargets.Property | AttributeTargets.Class)</span>]
<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">AvailableOnSitesAttribute</span> : <span class="hljs-title">Attribute</span>, <span class="hljs-title">IDisplayMetadataProvider</span>
{
    <span class="hljs-keyword">public</span> IEnumerable&lt;<span class="hljs-keyword">string</span>&gt; SiteNames{ <span class="hljs-keyword">get</span>; }

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">AvailableOnSitesAttribute</span>(<span class="hljs-params"><span class="hljs-keyword">params</span> <span class="hljs-keyword">string</span>[] siteNames</span>)</span>
    {
        SiteNames = siteNames;
    }

    <span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">bool</span> <span class="hljs-title">IsCurrentSite</span>(<span class="hljs-params"></span>)</span>
    {
        <span class="hljs-keyword">return</span> SiteNames.Any() &amp;&amp; SiteNames.Contains(SiteDefinition.Current.Name, StringComparer.InvariantCultureIgnoreCase);
    }

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">CreateDisplayMetadata</span>(<span class="hljs-params">DisplayMetadataProviderContext context</span>)</span>
    {
        <span class="hljs-keyword">if</span>(context.DisplayMetadata.AdditionalValues[ExtendedMetadata.ExtendedMetadataDisplayKey] <span class="hljs-keyword">is</span> ExtendedMetadata extendedMetadata)
        {
            extendedMetadata.ShowForEdit = IsCurrentSite();
        }
    }
}
</code></pre>
<p>There are many use cases where this type of attribute can be useful. It doesn't necessarily have to be based on the current site, it can be completely conditional based on any requirements. One thing to note is that it requires a page reload for the metadata to update, meaning that it's not an ideal method when the condition is related to another property on the same content type.</p>
<p>The custom attribute can be applied to any content type property. Below is a simple example of how a different introduction property can be displayed for the sites called Site1, Site2 and Site3. The names of the sites are configured in the 'manage sites' section of the admin UI. </p>
<pre><code class="lang-C#">[<span class="hljs-meta">AvailableOnSites(<span class="hljs-meta-string">"Site1"</span>)</span>]
<span class="hljs-keyword">public</span> <span class="hljs-keyword">virtual</span> <span class="hljs-keyword">string</span> Intro { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }

[<span class="hljs-meta">AvailableOnSites(<span class="hljs-meta-string">"Site2"</span>, <span class="hljs-meta-string">"Site3"</span>)</span>]
<span class="hljs-keyword">public</span> <span class="hljs-keyword">virtual</span> XhtmlString DetailedIntro { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
</code></pre>
<h2 id="heading-closing-thoughts">Closing thoughts</h2>
<p>The use case and examples in this article were somewhat simplified to make it less complex and confusing. To make things more robust you can adjust the metadata based on the site ID, or a site setting of sorts, instead of the name of the site.</p>
<p>As a final note I suggest customising the functionality to meet your requirements and firstly considering all your options — before you end up overcomplicating the properties on your content types. Handling properties this way can be very powerful and this article is meant to help developers meet specific requirements. </p>
]]></content:encoded></item><item><title><![CDATA[Content type mismatch exception in Optimizely]]></title><description><![CDATA[The TypeMismatchException exception came up recently after making some changes to namespaces and assemblies in an Optimizely project. This proved to be an interesting issue and I will explain my findings from investigating the problem — in the hopes ...]]></description><link>https://blog.ynzen.com/content-type-mismatch-exception-in-optimizely</link><guid isPermaLink="true">https://blog.ynzen.com/content-type-mismatch-exception-in-optimizely</guid><category><![CDATA[optimizely]]></category><category><![CDATA[cms]]></category><category><![CDATA[C#]]></category><dc:creator><![CDATA[Ynze Nunnink]]></dc:creator><pubDate>Fri, 19 Aug 2022 05:25:56 GMT</pubDate><content:encoded><![CDATA[<p>The <code>TypeMismatchException</code> exception came up recently after making some changes to namespaces and assemblies in an Optimizely project. This proved to be an interesting issue and I will explain my findings from investigating the problem — in the hopes that it will help another developer in the future. </p>
<h3 id="heading-the-exception">The exception</h3>
<p>As we can see in the exception below, there is a problem with a piece of content that does not inherit from the correct content type. The content is an image and it inherits from <code>ImageData</code> instead of the expected<code>ImageFile</code> type. </p>
<pre><code class="lang-html">An unhandled exception occurred while processing the request.
TypeMismatchException: Content with id '1234' is of type 'EPiServer.Core.ImageData_DynamicProxy' which does not inherit required type 'MyProject.Models.Media.ImageFile'
EPiServer.Core.Internal.DefaultContentLoader.ThrowTypeMismatchException(ContentReference link, Type actual, Type required)
</code></pre>
<h3 id="heading-whats-the-problem">What's the problem?</h3>
<p>The exception is caused by an invalid object cast. In my case I was casting image content to <code>ImageFile</code>, but the content had somehow changed to be of type <code>ImageData</code>. What is actually happening is that the custom content type is no longer recognized and the primary type is used as a fallback. The type does still exist in the solution, but it has been moved to a new assembly and namespace. The problem is that the content types in the new assembly are not picked up by the CMS.</p>
<h4 id="heading-content-type-synchronization">Content type synchronization</h4>
<p>On startup the assemblies of the application are automatically scanned for content types and then those types are synchronized to the database. Any of the referenced assemblies can also contain content types and will be synchronized in the same way. It's interesting to note that this only works for assemblies with a dependency to the <code>EPiServer.Framework</code> package. Besides that it also does not seem to work for assemblies that are added to the project as a direct DLL reference.</p>
<h3 id="heading-how-to-fix-it">How to fix it</h3>
<p>The new assembly that contains the content type was added to the project as a direct DLL reference. This assembly was not being picked up by the <code>AssemblyScanner</code> during startup and therefore the content type was not synchronized correctly — like I described in the section above. In order to solve this issue I had to reference the assembly in a different way and opted for a private nuget package. Another way is to add a reference to the project instead of to the DLL directly. </p>
<h3 id="heading-conclusion">Conclusion</h3>
<p>The issue turned out to be caused by something quite different than I originally expected. It was interesting to learn about how the content type synchronization works in Optimizely. I doubt many people will encounter the same issue, because it's really an unlucky edge case with assembly referencing specific to my architecture. Although if anyone finds themselves in a similar situation then hopefully this post will help them.</p>
]]></content:encoded></item><item><title><![CDATA[Contextualizing content type thumbnails in Optimizely CMS]]></title><description><![CDATA[In the CMS when creating a block or a page a list of all the available content types will be displayed. Each content type can have its own custom thumbnail image to help identify what the content is used for. One common practice is to take a screensh...]]></description><link>https://blog.ynzen.com/contextualizing-content-type-thumbnails-in-optimizely-cms</link><guid isPermaLink="true">https://blog.ynzen.com/contextualizing-content-type-thumbnails-in-optimizely-cms</guid><category><![CDATA[cms]]></category><category><![CDATA[optimizely]]></category><dc:creator><![CDATA[Ynze Nunnink]]></dc:creator><pubDate>Tue, 12 Jul 2022 04:23:53 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1657599741013/BAt0eEuzw.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the CMS when creating a block or a page a list of all the available content types will be displayed. Each content type can have its own custom thumbnail image to help identify what the content is used for. One common practice is to take a screenshot of the component as it was designed and use that as the thumbnail. To configure the thumbnail image you can add the <code>ImageUrl</code> attribute to any content type. The attribute has a parameter that can be used to set the path to the image.</p>
<p>Now I had a scenario where a block is used in multiple different sites — and each site has its own styling theme. If I were to use a screenshot of the block design then the styling will would be incorrect for all but one of the sites. This is because the thumbnail path can only be configured once per block type. To solve this I created a simple solution that enables a different thumbnail to be displayed for a block type based on the context. Note that for this example I'm only using block types, but it can be used for any content types.</p>
<p>The following snippet contains a custom attribute that inherits from the aforementioned <code>ImageUrlAttribute</code> and overrides the <code>Path</code> variable — in order to get the thumbnail path based on the name of the current site.</p>
<pre><code class="lang-C#"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">ContextualImageUrlAttribute</span> : <span class="hljs-title">ImageUrlAttribute</span>
{
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">ContextualImageUrlAttribute</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> name</span>) : <span class="hljs-title">base</span>(<span class="hljs-params"><span class="hljs-keyword">string</span>.Empty</span>)</span>
    {
        FileName = name;
    }

    <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> FileName { <span class="hljs-keyword">get</span>; }

    <span class="hljs-keyword">public</span> <span class="hljs-keyword">override</span> <span class="hljs-keyword">string</span> Path =&gt; <span class="hljs-string">$"~/<span class="hljs-subst">{SiteDefinition.Current.Name}</span>/Thumbnails/"</span> + FileName + <span class="hljs-string">".jpg"</span>;
}
</code></pre>
<p>For this to work the thumbnails have to be stored inside folders that are named after each site. For example the path to a thumbnail for one site could be <code>/MySite/Thumbnails/custom-block.jpg</code>.</p>
<p>The new attribute can then be added to any content type. Here you can see it being used on a block.</p>
<pre><code class="lang-C#">[<span class="hljs-meta">ContextualImageUrl(<span class="hljs-meta-string">"hero-banner-block"</span>)</span>]
<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">HeroBannerBlock</span> : <span class="hljs-title">BlockData</span>
{
}
</code></pre>
<p>This is a simple example of how you can use a custom attribute implementation to contextualize the thumbnail path. In other scenario's you may want to change the path based on a configurable setting or some other contextual property. Hopefully this example can help developers make the editing experience more user friendly in a similar  multi-site solution.</p>
]]></content:encoded></item><item><title><![CDATA[How to fix the property lazy loading error in Optimizely]]></title><description><![CDATA[While doing some refactoring of namespaces in my project the server error below popped up. I could not find a clear answer on how to resolve this issue except for that it was related to a custom PropertyList property.
Lazy loaded property value is no...]]></description><link>https://blog.ynzen.com/how-to-fix-the-property-lazy-loading-error-in-optimizely</link><guid isPermaLink="true">https://blog.ynzen.com/how-to-fix-the-property-lazy-loading-error-in-optimizely</guid><category><![CDATA[cms]]></category><category><![CDATA[C#]]></category><dc:creator><![CDATA[Ynze Nunnink]]></dc:creator><pubDate>Sat, 07 May 2022 02:03:15 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1651888943446/ItiwVkvHc.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>While doing some refactoring of namespaces in my project the server error below popped up. I could not find a clear answer on how to resolve this issue except for that it was related to a custom PropertyList property.</p>
<pre><code class="lang-TEXT">Lazy loaded property value is not supported by the current property instance
Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.

Exception Details: System.InvalidOperationException: Lazy loaded property value is not supported by the current property instance
</code></pre>
<p>After doing some digging I realized that a second property type definition had been created because I updated the namespace of my custom PropertyList property. 
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1651888742280/2wd7Fcof6.png" alt="image.png" />
The 'Edit Custom Property Types' view in the CMS shows all the property definitions and can be used to update and delete these in the database. <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1651888182030/mp1R4CxLk.png" alt="image.png" class="image--center mx-auto" /></p>
<p>The <strong>solution</strong> to this issue is to delete the duplicate property definition and make sure to update the definition to use the new namespace.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1651888613028/rNuhd9FBW.png" alt="image.png" /></p>
]]></content:encoded></item><item><title><![CDATA[Working with page header properties in Optimizely]]></title><description><![CDATA[The top section when editing content is the page header (also know as settings panel) and it contains some standard fields. In this post I will share how to hide, move or add properties within this header section. 

Hiding specific properties
The def...]]></description><link>https://blog.ynzen.com/working-with-page-header-properties-in-optimizely</link><guid isPermaLink="true">https://blog.ynzen.com/working-with-page-header-properties-in-optimizely</guid><category><![CDATA[dotnet]]></category><category><![CDATA[cms]]></category><dc:creator><![CDATA[Ynze Nunnink]]></dc:creator><pubDate>Wed, 06 Apr 2022 07:16:22 GMT</pubDate><content:encoded><![CDATA[<p>The top section when editing content is the page header (also know as settings panel) and it contains some standard fields. In this post I will share how to hide, move or add properties within this header section. 
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1649124450679/gdmc_8VZn.png" alt="image.png" /></p>
<h2 id="heading-hiding-specific-properties">Hiding specific properties</h2>
<p>The default properties are generally useful for every content type, however in some situations they are not used or require adjustments. For example the Category selector field and 'Display in navigation' checkbox that are rendered by default on every page. Now you might be thinking, I can easily override these properties in my page type and add the Ignore attribute to hide them, but unfortunately that does not work because of the way these properties are created in the <code>PageData</code> class.</p>
<p>It's still possible to hide those default properties by creating an <code>EditorDescriptor</code> class. The code in the following snippet will hide the 'Display in navigation' property. If we set the target type as 'bool' it will only be called for properties of type bool. Then the if statement will make sure we hide the correct property only on pages of type <code>TagPage</code>.</p>
<pre><code class="lang-C#">[<span class="hljs-meta">EditorDescriptorRegistration(TargetType = typeof(bool))</span>]
[<span class="hljs-meta">EditorDescriptorRegistration(TargetType = typeof(bool?))</span>]
<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">HideVisibleInMenuEditorDescriptor</span> : <span class="hljs-title">EditorDescriptor</span>
{
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">override</span> <span class="hljs-keyword">void</span> <span class="hljs-title">ModifyMetadata</span>(<span class="hljs-params">ExtendedMetadata metadata, IEnumerable&lt;Attribute&gt; attributes</span>)</span>
    {
        <span class="hljs-keyword">base</span>.ModifyMetadata(metadata, attributes);

        <span class="hljs-comment">//Get owner content</span>
        <span class="hljs-keyword">dynamic</span> data = metadata;
        <span class="hljs-keyword">var</span> ownerContent = data.OwnerContent;

        <span class="hljs-comment">//Only hide the property for content types inheriting from SettingsBase</span>
        <span class="hljs-keyword">if</span> (metadata.PropertyName == <span class="hljs-string">"PageVisibleInMenu"</span> &amp;&amp; ownerContent <span class="hljs-keyword">is</span> TagPage)
        {
            metadata.ShowForEdit = <span class="hljs-literal">false</span>;
        }
    }
}
</code></pre>
<p>The result is that the checkbox is hidden on all of our tag pages.
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1649226757554/4l6yCucOY.png" alt="image.png" /></p>
<h3 id="heading-hiding-the-category-field">Hiding the category field</h3>
<p>In the next example the Category field gets hidden for content types inheriting from <code>SettingsBase</code>. It's very similar to the snippet above, the main difference is that the target type is set to <code>CategoryList</code>.</p>
<pre><code class="lang-C#">[<span class="hljs-meta">EditorDescriptorRegistration(TargetType = typeof(CategoryList), EditorDescriptorBehavior = EditorDescriptorBehavior.Default)</span>]
<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">CategoryEditorDescriptor</span> : <span class="hljs-title">EditorDescriptor</span>
{
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">override</span> <span class="hljs-keyword">void</span> <span class="hljs-title">ModifyMetadata</span>(<span class="hljs-params">
        ExtendedMetadata metadata,
        IEnumerable&lt;Attribute&gt; attributes</span>)</span>
    {
        <span class="hljs-keyword">base</span>.ModifyMetadata(metadata, attributes);

        <span class="hljs-comment">//Get owner content</span>
        <span class="hljs-keyword">dynamic</span> mayQuack = metadata;
        <span class="hljs-keyword">var</span> ownerContent = mayQuack.OwnerContent;

        <span class="hljs-comment">//Only hide category for content types inheriting from SettingsBase</span>
        <span class="hljs-keyword">if</span> (metadata.PropertyName == <span class="hljs-string">"icategorizable_category"</span> &amp;&amp; ownerContent <span class="hljs-keyword">is</span> TagPage)
        {
            <span class="hljs-comment">//Hide property</span>
            metadata.ShowForDisplay = <span class="hljs-literal">false</span>;
            metadata.ShowForEdit = <span class="hljs-literal">false</span>;
        }
    }
}
</code></pre>
<h3 id="heading-move-category-to-the-page-header">Move category to the page header</h3>
<p>With an editor descriptor it's also possible to move the category field. By default it is placed within the content tab, but as shown in the Optimizely Foundation project you can also move it into the page header. Using the same descriptor that we created earlier we can move the category by setting the group name.</p>
<pre><code class="lang-C#">metadata.GroupName = SystemTabNames.PageHeader;
metadata.Order = <span class="hljs-number">10000</span>;
</code></pre>
<h4 id="heading-adding-a-custom-property-to-the-page-header">Adding a custom property to the page header</h4>
<p>This technique can also be used for custom properties. All you have to do is set the group name using the <code>SystemTabNames.PageHeader</code> constant.</p>
<pre><code class="lang-C#">[<span class="hljs-meta">SelectOne(SelectionFactoryType = typeof(ThemeSelectionFactory))</span>]
[<span class="hljs-meta">Display(Name = <span class="hljs-meta-string">"Theme"</span>, Order = 2, GroupName = SystemTabNames.PageHeader)</span>]
<span class="hljs-keyword">public</span> <span class="hljs-keyword">virtual</span> <span class="hljs-keyword">string</span> SiteTheme { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
</code></pre>
<p>The result is that we now have a 'Theme' dropdown in the header section.
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1649228185107/I6ocHloDa.png" alt="image.png" /></p>
<p>These snippets are all just examples and can be used in many different ways depending on your content approach. The default properties are there for a reason so I recommend reconsidering all the options before hiding any of them. Over the years I've had to deal with requirements to make specific adjustments to the page header so that's why I'm sharing this knowledge.</p>
]]></content:encoded></item><item><title><![CDATA[Solving the 'For This {0}' issue in Optimizely]]></title><description><![CDATA[In Optimizely you can create custom content that is not inheriting from one of the standard types like a block or page. You do this by inheriting from one of the base classes like StandardContentBase, ContentBase or even BasicContent, depending on ho...]]></description><link>https://blog.ynzen.com/solving-the-for-this-0-issue-in-optimizely</link><guid isPermaLink="true">https://blog.ynzen.com/solving-the-for-this-0-issue-in-optimizely</guid><category><![CDATA[cms]]></category><category><![CDATA[xml]]></category><category><![CDATA[C#]]></category><dc:creator><![CDATA[Ynze Nunnink]]></dc:creator><pubDate>Sun, 06 Mar 2022 02:19:19 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1646530772741/bNzsY6kXT.PNG" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In Optimizely you can create custom content that is not inheriting from one of the standard types like a block or page. You do this by inheriting from one of the base classes like <code>StandardContentBase</code>, <code>ContentBase</code> or even <code>BasicContent</code>, depending on how many features you need like versioning and languages. There are some <a target="_blank" href="https://www.cdisol.blog/2019/11/22/episerver-creation-and-management-of-non-common-content-types/">good blog posts</a> out there that explain how to create such custom content and add things like UI elements — so I will not go into detail about it here. The problem I've had for a long time is that when you edit your custom content, the folder that usually states 'For This Page' or 'For This Block' will now be called 'For This {0}'. I finally took the time to investigate the issue and come up with a solution. </p>
<p>The assumption I've always had is that the label is missing for my custom content type and therefore the '{0}' part of the label is not formatted correctly. The question was how do I add the label? After some digging I found the XML localization resource that is responsible for this label. </p>
<pre><code class="lang-XML"><span class="hljs-meta">&lt;?xml version="1.0" encoding="utf-8" standalone="yes"?&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">languages</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">language</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"English"</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"en"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">contenttypes</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">blockdata</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">name</span>&gt;</span>Block<span class="hljs-tag">&lt;/<span class="hljs-name">name</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">blockdata</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">pagedata</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">name</span>&gt;</span>Page<span class="hljs-tag">&lt;/<span class="hljs-name">name</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">pagedata</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">contenttypes</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">language</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">languages</span>&gt;</span>
</code></pre>
<p>For each base content type there is a <code>name</code> variable that is used to format the folder name. What I needed now is to set this variable for my example type called <code>CustomContent</code>. Adding localization to the CMS is done by creating an XML file and putting it in the <code>/lang</code> folder of the project. I used the following XML containing a new section using the name of my example type.</p>
<pre><code class="lang-XML"><span class="hljs-meta">&lt;?xml version="1.0" encoding="utf-8" standalone="yes"?&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">languages</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">language</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"English"</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"en"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">contenttypes</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">customcontent</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">name</span>&gt;</span>Custom Content<span class="hljs-tag">&lt;/<span class="hljs-name">name</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">customcontent</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">contenttypes</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">language</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">languages</span>&gt;</span>
</code></pre>
<p>Unfortunately this did not work initially and I later found out that it only works for <strong>primary types</strong>.  Those other types such as <code>BlockData</code> and <code>PageData</code> are marked by a UI descriptor as primary types using the <code>IsPrimaryType</code> variable. The variable is used internally to identify which one of base types of a content type contains the information related to the UI elements like the folder name. In my case I'm not inheriting from any primary types and therefore there is no label.</p>
<p>To solve this I can turn my type, or even a custom base type, into a primary type by creating a UI descriptor. Note that you should use this at your own risk, because there is not much information available online about primary types and how to use them.</p>
<pre><code class="lang-C#">[<span class="hljs-meta">UIDescriptorRegistration</span>]
<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">CustomContentDescriptor</span> : <span class="hljs-title">UIDescriptor</span>&lt;<span class="hljs-title">CustomContent</span>&gt;
{
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">CustomContentDescriptor</span>(<span class="hljs-params"></span>)</span>
    {
        IsPrimaryType = <span class="hljs-literal">true</span>;
    }
}
</code></pre>
<p>The result is that the label is now displayed correctly.
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1646532553578/FmyEKcqef.png" alt="image.png" />
As a final note — I hope that Optimizely will expand on the folder name localization so to make it more flexible and have a default value like 'Content'. It would be great if you could set the name for every content type, not just primary types, so that the folder name could be 'For This Article Page' for example. It would even make it more clear to the user what content is located in the folder. Cheers! </p>
]]></content:encoded></item><item><title><![CDATA[Extending the HyperLink editor in Optimizely 11]]></title><description><![CDATA[Anders Hattestad described in his blog post how you can create a custom implementation for the HyperLink editor in Optimizely CMS. However for the newer versions of the CMS an updated solution is required because the internal functionality has change...]]></description><link>https://blog.ynzen.com/extending-the-hyperlink-editor-in-optimizely-11</link><guid isPermaLink="true">https://blog.ynzen.com/extending-the-hyperlink-editor-in-optimizely-11</guid><category><![CDATA[C#]]></category><category><![CDATA[cms]]></category><dc:creator><![CDATA[Ynze Nunnink]]></dc:creator><pubDate>Mon, 21 Feb 2022 03:22:00 GMT</pubDate><content:encoded><![CDATA[<p><a target="_blank" href="https://world.optimizely.com/System/Users-and-profiles/Community-Profile-Card/?userId=6f2dbdb8-7685-dd11-a26e-0018717a8c82">Anders Hattestad</a> described in his blog post how you can create a custom implementation for the HyperLink editor in Optimizely CMS. However for the newer versions of the CMS an updated solution is required because the internal functionality has changed. It's still a popular request so I decided to share the updated code that works for CMS 11. </p>
<p>Note that I only post the code that has been updated, for the full example please refer to the blog post: https://world.optimizely.com/blogs/Anders-Hattestad/Dates/2015/2/extending-the-hyperlink-with-custom-field/</p>
<p>First here is the updated code for the custom HyperLink model.</p>
<pre><code class="lang-C#"><span class="hljs-keyword">internal</span> <span class="hljs-keyword">class</span> <span class="hljs-title">HyperLinkModel</span>
{
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> Name { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }

    <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> DisplayName { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }

    <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> Title { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }

    <span class="hljs-keyword">public</span> IEnumerable&lt;ContentReference&gt; Roots { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }

    <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> WidgetType { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }

    <span class="hljs-keyword">public</span> IDictionary&lt;<span class="hljs-keyword">string</span>, <span class="hljs-keyword">object</span>&gt; WidgetSettings { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }

    <span class="hljs-keyword">public</span> IEnumerable&lt;Type&gt; LinkableTypes { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }

    <span class="hljs-keyword">public</span> <span class="hljs-keyword">bool</span> Invisible { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }

    <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> SearchArea { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
}
</code></pre>
<p>Next is the custom editor descriptor that will initialize the existing fields followed by a new custom text field with the name <code>Other</code>. You can find the code for the custom field in the blog post linked above.</p>
<pre><code class="lang-C#">[<span class="hljs-meta">EditorDescriptorRegistration(TargetType = typeof (string), UIHint = <span class="hljs-meta-string">"HyperLink"</span>,
EditorDescriptorBehavior = EditorDescriptorBehavior.OverrideDefault)</span>]
<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">LinkEditorDescriptor</span> : <span class="hljs-title">EditorDescriptor</span>
{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> LocalizationService _localizationService;

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">LinkEditorDescriptor</span>(<span class="hljs-params"></span>) : <span class="hljs-title">this</span>(<span class="hljs-params">LocalizationService.Current</span>)</span>
    {
    }

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">LinkEditorDescriptor</span>(<span class="hljs-params">LocalizationService localizationService</span>)</span>
    {
        _localizationService = localizationService;
    }

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">override</span> <span class="hljs-keyword">void</span> <span class="hljs-title">ModifyMetadata</span>(<span class="hljs-params">ExtendedMetadata metadata, IEnumerable&lt;Attribute&gt; attributes</span>)</span>
    {
        <span class="hljs-keyword">base</span>.ModifyMetadata(metadata, attributes);
        IEnumerable&lt;IContentRepositoryDescriptor&gt; allInstances =
            ServiceLocator.Current.GetAllInstances&lt;IContentRepositoryDescriptor&gt;();
        List&lt;HyperLinkModel&gt; list = (
            <span class="hljs-keyword">from</span> r <span class="hljs-keyword">in</span> allInstances
            <span class="hljs-keyword">orderby</span> r.SortOrder
            <span class="hljs-keyword">where</span> r.LinkableTypes != <span class="hljs-literal">null</span> &amp;&amp; r.LinkableTypes.Any&lt;Type&gt;()
            <span class="hljs-keyword">select</span> <span class="hljs-keyword">new</span> HyperLinkModel
            {
                Name = (r.CustomSelectTitle ?? r.Name),
                Roots = r.Roots,
                WidgetType = <span class="hljs-string">"epi-cms/widget/ContentSelector"</span>,
                LinkableTypes = r.LinkableTypes,
                SearchArea = r.SearchArea
            }).ToList&lt;HyperLinkModel&gt;();
        list.InsertRange(list.Count, <span class="hljs-keyword">new</span> HyperLinkModel[]
        {
            <span class="hljs-keyword">new</span> HyperLinkModel
            {
                Name = <span class="hljs-string">"Email"</span>,
                Title = _localizationService.GetString(<span class="hljs-string">"/episerver/cms/widget/editlink/emailtooltip"</span>),
                DisplayName = _localizationService.GetString(<span class="hljs-string">"/episerver/cms/widget/editlink/email"</span>),
                WidgetType = <span class="hljs-string">"epi-cms/form/EmailValidationTextBox"</span>,
                WidgetSettings = <span class="hljs-keyword">new</span> Dictionary&lt;<span class="hljs-keyword">string</span>, <span class="hljs-keyword">object</span>&gt;
                {
                    {
                        <span class="hljs-string">"addMailTo"</span>,
                        <span class="hljs-literal">true</span>
                    }
                }
            },
            <span class="hljs-keyword">new</span> HyperLinkModel
            {
                Name = <span class="hljs-string">"FreeTextLink"</span>,
                Title = <span class="hljs-string">"Other links"</span>,
                DisplayName = <span class="hljs-string">"Other"</span>,
                WidgetType = <span class="hljs-string">"alloy/TextBoxMustHaveValue"</span>
            },
            <span class="hljs-keyword">new</span> HyperLinkModel
            {
                Name = <span class="hljs-string">"ExternalLink"</span>,
                Title = _localizationService.GetString(<span class="hljs-string">"/episerver/cms/widget/editlink/externallinktooltip"</span>),
                DisplayName = _localizationService.GetString(<span class="hljs-string">"/episerver/cms/widget/editlink/externallink"</span>),
                WidgetType = <span class="hljs-string">"epi-cms/form/UrlValidationTextBox"</span>
            },
            <span class="hljs-keyword">new</span> HyperLinkModel
            {
                Name = <span class="hljs-string">"Anchor"</span>,
                Title = _localizationService.GetString(<span class="hljs-string">"/episerver/cms/widget/editlink/anchortooltip"</span>),
                DisplayName = _localizationService.GetString(<span class="hljs-string">"/episerver/cms/widget/editlink/anchor"</span>),
                WidgetType = <span class="hljs-string">"epi-cms/form/AnchorSelectionEditor"</span>,
                Invisible = <span class="hljs-literal">true</span>
            }
        });
        metadata.EditorConfiguration[<span class="hljs-string">"providers"</span>] = list;
        metadata.DisplayName = <span class="hljs-keyword">string</span>.Empty;
        metadata.ClientEditingClass = <span class="hljs-string">"epi-cms/widget/HyperLinkSelector"</span>;
    }
}
</code></pre>
]]></content:encoded></item><item><title><![CDATA[Flexible page type restrictions in Optimizely CMS]]></title><description><![CDATA[In the Optimizely (formerly EPiServer) CMS the content structure is based on the restrictions that are set on every content type. You can use the AvailableContentTypes in code or configure the "Available Page Types" in the admin interface for a page ...]]></description><link>https://blog.ynzen.com/flexible-page-type-restrictions-in-optimizely-cms</link><guid isPermaLink="true">https://blog.ynzen.com/flexible-page-type-restrictions-in-optimizely-cms</guid><category><![CDATA[cms]]></category><category><![CDATA[content]]></category><category><![CDATA[dotnet]]></category><dc:creator><![CDATA[Ynze Nunnink]]></dc:creator><pubDate>Thu, 17 Feb 2022 04:11:42 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1645070650021/t9O5vazdb.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the Optimizely (formerly EPiServer) CMS the content structure is based on the restrictions that are set on every content type. You can use the <code>AvailableContentTypes</code> in code or configure the "Available Page Types" in the admin interface for a page type to specify which page types are available when creating child content. The problem with this is approach is that the restrictions are the same for every page of the specific type, even though you may not want every page of that type to have the same restrictions. In this blog post I will describe how resolve this problem by making the content restrictions editable within pages in the CMS. There are however numerous other use cases that can be built using the same techniques.</p>
<p>Reusing content types across multiple sites is another good use case that requires increased flexibility. The child content of the reused types would normally have to be the same for every site, but in most cases you need the content structure to be different per site. Implementing site specific content restrictions would be very useful in this scenario. I will not go into detail on how to achieve this — however it will be very similar to the solution described below.</p>
<h2 id="heading-the-content-type-selector">The content type selector</h2>
<p>To make the page restrictions editable we will need a property that lists all the available page types. When creating a new child page, only the selected page types should then be displayed as options. The following selection factory will retrieve all page types.</p>
<pre><code class="lang-C#"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">ContentTypeSelectionFactory</span> : <span class="hljs-title">ISelectionFactory</span>
{
    <span class="hljs-function"><span class="hljs-keyword">public</span> IEnumerable&lt;ISelectItem&gt; <span class="hljs-title">GetSelections</span>(<span class="hljs-params">ExtendedMetadata metadata</span>)</span>
    {
        <span class="hljs-keyword">var</span> contentTypeRepo = ServiceLocator.Current.GetInstance&lt;IContentTypeRepository&gt;();

        <span class="hljs-comment">//Retrieve all page types</span>
        <span class="hljs-keyword">var</span> contentTypes = contentTypeRepo.List()
            .Where(ct =&gt; ct.Base == ContentTypeBase.Page)
            .Select(x =&gt; x.CreateWritableClone() <span class="hljs-keyword">as</span> ContentType)
            .ToList();

        <span class="hljs-keyword">var</span> items = <span class="hljs-keyword">new</span> List&lt;ISelectItem&gt;();

        <span class="hljs-keyword">foreach</span> (<span class="hljs-keyword">var</span> contentType <span class="hljs-keyword">in</span> contentTypes)
        {
            items.Add(<span class="hljs-keyword">new</span> SelectItem { Text = contentType.Name, Value = contentType.ID });
        }

        <span class="hljs-keyword">return</span> items;
    }
}
</code></pre>
<p>Then to use the selection factory we can use the <code>SelectMany</code> attribute on a property, which will allow the editor to select multiple options from the list. In this case I've added the property to the <code>PageBase</code> class so that it is available for every page.</p>
<pre><code class="lang-C#">[<span class="hljs-meta">Display(
    Name = <span class="hljs-meta-string">"Allowed Content Types"</span>,
    GroupName = <span class="hljs-meta-string">"Content Availability"</span>,
    Order = 10)</span>]
[<span class="hljs-meta">SelectMany(SelectionFactoryType = typeof(ContentTypeSelectionFactory))</span>]
<span class="hljs-keyword">public</span> <span class="hljs-keyword">virtual</span> <span class="hljs-keyword">string</span>  AllowedContentTypes { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
</code></pre>
<p>The page property will look like this.
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1645070731243/tr-AdLzDR.png" alt="image.png" /></p>
<p>Note that the group name is "Content Availability", which will add the property into a separate tab in the page. You can then set the group to only be editable by administrators. This way only admins can make changes to the content type availability.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1645070756592/zBdWWXLxl.png" alt="image.png" /></p>
<h2 id="heading-the-content-type-availability-service">The content type availability service</h2>
<p>In the CMS there is a <code>DefaultContentTypeAvailablilityService</code> that is used to filter the available content types by permissions when creating content such as pages and blocks. The <code>ListAvailable</code> method of the service is called every time you create a piece new content. In our scenario we want to check the <code>AllowedContentTypes</code> property of the parent page when creating a new child page — then filter the list so that only those page types are listed as options. </p>
<p>To do this we can create a custom implementation of the service that inherits the default service. Then we override the <code>ListAvailable</code> method to filter the content types.</p>
<pre><code class="lang-C#"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">PropertyContentTypeAvailabilityService</span> : <span class="hljs-title">DefaultContentTypeAvailablilityService</span>
{
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">PropertyContentTypeAvailabilityService</span>(<span class="hljs-params">
        ServiceAccessor&lt;IContentTypeRepository&gt; contentTypeRepositoryAccessor,
        IAvailableModelSettingsRepository modelRepository,
        IAvailableSettingsRepository typeSettingsRepository,
        GroupDefinitionRepository groupDefinitionRepository,
        IContentLoader contentLoader,
        ISynchronizedObjectInstanceCache cache,
        Lazy&lt;ISettingsService&lt;SiteSettingsPage&gt;&gt; settingsService</span>)
        : <span class="hljs-title">base</span>(<span class="hljs-params">contentTypeRepositoryAccessor, modelRepository, typeSettingsRepository, groupDefinitionRepository,
            contentLoader, cache</span>)</span>
    {
    }
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">override</span> IList&lt;ContentType&gt; <span class="hljs-title">ListAvailable</span>(<span class="hljs-params">IContent content, <span class="hljs-keyword">bool</span> contentFolder, IPrincipal user</span>)</span>
    {
        <span class="hljs-comment">//Use the base service to filter by permissions and attributes</span>
        <span class="hljs-keyword">var</span> contentTypes = <span class="hljs-keyword">base</span>.ListAvailable(content, contentFolder, user);

        <span class="hljs-keyword">return</span> contentTypes.Where(ct =&gt; SelectByProperty(ct, content)).ToList();
    }

    <span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-title">Func</span>&lt;<span class="hljs-title">ContentType</span>, <span class="hljs-title">IContent</span>, <span class="hljs-title">bool</span>&gt; SelectByProperty</span> =&gt; (contentType, content) =&gt;
    {
        <span class="hljs-keyword">if</span> (contentType.ModelType == <span class="hljs-literal">null</span>) <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;

        <span class="hljs-keyword">if</span> (content <span class="hljs-keyword">is</span> PageBase page &amp;&amp; !page.AllowedContentTypes.IsNullOrEmpty())
        {
            <span class="hljs-keyword">return</span> page.AllowedContentTypes.Split(<span class="hljs-string">','</span>).Contains(contentType.ID.ToString());
        }

        <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
    };
}
</code></pre>
<p>For this to work we need to update the service context to use our custom service instead of the default implementation. We can do this in the dependency injection resolver as follows.</p>
<pre><code class="lang-C#"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">ConfigureContainer</span>(<span class="hljs-params">ServiceConfigurationContext context</span>)</span>
{
    context.ConfigurationComplete += (_, _) =&gt;
    {
        context.Services.RemoveAll(<span class="hljs-keyword">typeof</span>(ContentTypeAvailabilityService));
        context.Services.AddSingleton&lt;ContentTypeAvailabilityService, PropertyContentTypeAvailabilityService&gt;();
    };
}
</code></pre>
<h2 id="heading-trying-it-out">Trying it out</h2>
<p>On every page we can now update the available content types. As you can see here we only selected two page types.
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1645070774282/2MLBKADLK.png" alt="image.png" />
And as a result, when creating a new child page, only the selected two page types are displayed.
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1645070791043/UWJf5b75D.png" alt="image.png" /></p>
]]></content:encoded></item></channel></rss>