<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom"><title type="text">Blog posts by Binh-Nguyen</title><link href="http://world.optimizely.com" /><updated>2026-06-24T11:26:37.0000000Z</updated><id>https://world.optimizely.com/blogs/binh-nguyen/</id> <generator uri="http://world.optimizely.com" version="2.0">Optimizely World</generator> <entry><title>Migrating from Find to Graph: Lessons Learned from a Real CMS 13 Project</title><link href="https://world.optimizely.com/blogs/binh-nguyen/dates/2026/6/some-found-issues-when-migrating-find-to-optimizely-graph/" /><id>&lt;p class=&quot;FirstParagraph&quot;&gt;While migrating a search solution from Optimizely Search &amp;amp; Navigation (Find) to Optimizely Graph in CMS 13, I encountered several issues that were not immediately obvious from the documentation.&lt;/p&gt;
&lt;p class=&quot;MsoBodyText&quot;&gt;Most of these issues were discovered through experimentation, debugging startup errors, investigating generated Graph schemas, and comparing behavior with Search &amp;amp; Navigation.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p class=&quot;MsoBodyText&quot;&gt;Here are details about them:&lt;/p&gt;
&lt;p class=&quot;SourceCode&quot;&gt;&lt;span class=&quot;OperatorTok&quot;&gt;&lt;strong&gt;1. Graph Conventions Only Existing Starting from CMS 13.1.0&lt;/strong&gt;&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;isSelectedEnd&quot;&gt;It took me quite some time to figure out how to apply Graph Conventions based on the following documentation while working on a CMS 13.0.0 project:&lt;/p&gt;
&lt;p class=&quot;isSelectedEnd&quot;&gt;&lt;a href=&quot;https://docs.developers.optimizely.com/content-management-system/v13.0.0-CMS/docs/indexing-conventions&quot;&gt;https://docs.developers.optimizely.com/content-management-system/v13.0.0-CMS/docs/indexing-conventions&lt;/a&gt;&lt;/p&gt;
&lt;p class=&quot;isSelectedEnd&quot;&gt;The documentation describes Graph Conventions using APIs such as:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services.ConfigureGraphConventions(...)&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;OR&lt;br /&gt;&lt;br /&gt;&lt;code&gt;services.AddContentGraph(configureConventions: c =&amp;gt;
{
    //add conventions here
});&lt;/code&gt;&lt;/pre&gt;
&lt;p class=&quot;SourceCode&quot;&gt;&lt;span class=&quot;OperatorTok&quot;&gt;However, I was unable to find this API in a CMS 13.0.0 implementation.&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;isSelectedEnd&quot;&gt;After further investigation, I discovered that the Graph Convention API was introduced in CMS 13.1.0 together with Optimizely.Graph.Cms 13.1.0 and is not available in earlier CMS 13 releases.&lt;/p&gt;
&lt;p&gt;This initially caused some confusion because the documentation is published under the CMS 13 documentation section without clearly indicating the minimum version required for the feature.&lt;/p&gt;
&lt;p&gt;My recommendation is to use CMS 13.1.0 or later if you plan to leverage Graph Conventions during your migration. And It would be helpful if the documentation included version-specific notes or minimum version requirements for Graph Convention APIs, as this could save developers a considerable amount of troubleshooting time during migration projects.&lt;/p&gt;
&lt;p class=&quot;MsoBodyText&quot;&gt;&lt;strong&gt;2. Extension Methods Work for Queries but Not for Facets&lt;span style=&quot;font-size: 12.0pt; font-family: &#39;Aptos&#39;,sans-serif; mso-ascii-theme-font: minor-latin; mso-fareast-font-family: Aptos; mso-fareast-theme-font: minor-latin; mso-hansi-theme-font: minor-latin; mso-bidi-font-family: &#39;Times New Roman&#39;; mso-bidi-theme-font: minor-bidi; mso-ansi-language: EN-US; mso-fareast-language: EN-US; mso-bidi-language: AR-SA;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p class=&quot;FirstParagraph&quot;&gt;One of the first surprises I encountered was that extension methods included through Graph Conventions can be queried successfully but cannot be used for faceting.&lt;br /&gt;Here is sample code:&lt;br /&gt;&lt;br /&gt;- Configuration&lt;br /&gt;&lt;code&gt;&lt;span class=&quot;NormalTok&quot;&gt;services&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;FunctionTok&quot;&gt;ConfigureGraphConventions&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt;conventions &lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;br /&gt;&lt;span class=&quot;OperatorTok&quot;&gt;{&lt;/span&gt;&lt;br /&gt;&lt;span class=&quot;NormalTok&quot;&gt;&lt;span style=&quot;mso-spacerun: yes;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;conventions&lt;/span&gt;&lt;br /&gt;&lt;span class=&quot;NormalTok&quot;&gt;&lt;span style=&quot;mso-spacerun: yes;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;FunctionTok&quot;&gt;ForInstancesOf&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt;StandardPage&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;&amp;gt;()&lt;/span&gt;&lt;br /&gt;&lt;span class=&quot;NormalTok&quot;&gt;&lt;span style=&quot;mso-spacerun: yes;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;FunctionTok&quot;&gt;IncludeField&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt;x &lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt; x&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;FunctionTok&quot;&gt;ContentTypeName&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;(),&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt; IndexingType&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;FunctionTok&quot;&gt;Searchable&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;);&lt;/span&gt;&lt;br /&gt;&lt;span class=&quot;OperatorTok&quot;&gt;});&lt;/span&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p class=&quot;SourceCode&quot;&gt;- Extension method:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static string ContentTypeName(this ISitePageData sitePageData)
{
    return sitePageData.GetOriginalType().Name;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p class=&quot;SourceCode&quot;&gt;However, when attempting to create a facet using the extension method:&lt;/p&gt;
&lt;p class=&quot;SourceCode&quot;&gt;&lt;code&gt;&lt;span class=&quot;NormalTok&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;FunctionTok&quot;&gt;Facet&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt;x &lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt; x&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;FunctionTok&quot;&gt;ContentTypeName&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;());&lt;br /&gt;&lt;/span&gt;&lt;/code&gt;&lt;span class=&quot;OperatorTok&quot;&gt;&lt;br /&gt;I saw this error&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;&lt;img src=&quot;/link/52e5237e18cf42718fe3a17a10072ee9.aspx&quot; width=&quot;1029&quot; height=&quot;664&quot; /&gt;&lt;br /&gt;&lt;br /&gt;I tried running the same facet query directly in Graph Explorer, and it worked as expected. This leads me to believe that the issue may originate from the Optimizely Graph Client API when it builds the query from a lambda expression. However, I&#39;m not sure whether this behavior is intentional or if it should be considered a bug.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;Impact:&lt;/strong&gt; Existing Find implementations that rely on calculated fields for faceting may require redesign when migrating to Graph.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;3. Search By Base Type Works Differently Than Search &amp;amp; Navigation&lt;br /&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p class=&quot;FirstParagraph&quot;&gt;In Search &amp;amp; Navigation, it is common to search across multiple content types by using a shared base class or marker interface.&lt;/p&gt;
&lt;p class=&quot;MsoBodyText&quot;&gt;Examples:&lt;/p&gt;
&lt;p class=&quot;SourceCode&quot;&gt;&lt;code&gt;&lt;span class=&quot;NormalTok&quot;&gt;Search&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt;SitePageData&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;&amp;gt;()&lt;/span&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p class=&quot;FirstParagraph&quot;&gt;or&lt;/p&gt;
&lt;p class=&quot;SourceCode&quot;&gt;&lt;code&gt;&lt;span class=&quot;NormalTok&quot;&gt;Search&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt;ISearchableContent&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;&amp;gt;()&lt;/span&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p class=&quot;FirstParagraph&quot;&gt;Many developers expect the same approach to work in Optimizely Graph. Unfortunately, Graph uses a different model and does not automatically support these patterns.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;- What does not work&lt;br /&gt;&lt;/strong&gt;&lt;br /&gt;The following approaches do not create a shared Graph schema that can be queried across all implementing content types.&lt;/p&gt;
&lt;p class=&quot;FirstParagraph&quot;&gt;&lt;span style=&quot;mso-bookmark: what-does-not-work;&quot;&gt;Simple interfaces:&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;SourceCode&quot;&gt;&lt;code&gt;&lt;span style=&quot;mso-bookmark: what-does-not-work;&quot;&gt;&lt;span class=&quot;KeywordTok&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;KeywordTok&quot;&gt;interface&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt; ISearchableContent&lt;/span&gt;&lt;br /&gt;&lt;span class=&quot;OperatorTok&quot;&gt;{&lt;/span&gt;&lt;br /&gt;&lt;span class=&quot;OperatorTok&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p class=&quot;FirstParagraph&quot;&gt;&lt;span style=&quot;mso-bookmark: what-does-not-work;&quot;&gt;Abstract base classes:&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;SourceCode&quot;&gt;&lt;code&gt;&lt;span style=&quot;mso-bookmark: what-does-not-work;&quot;&gt;&lt;span class=&quot;KeywordTok&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;KeywordTok&quot;&gt;abstract&lt;/span&gt; &lt;span class=&quot;KeywordTok&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt; SearchablePage &lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt; PageData&lt;/span&gt;&lt;br /&gt;&lt;span class=&quot;OperatorTok&quot;&gt;{&lt;/span&gt;&lt;br /&gt;&lt;span class=&quot;OperatorTok&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p class=&quot;FirstParagraph&quot;&gt;Although these patterns work well in Search &amp;amp; Navigation, Optimizely Graph does not automatically generate a queryable Graph contract from them.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;- What works&lt;/strong&gt;&lt;/p&gt;
&lt;p class=&quot;FirstParagraph&quot;&gt;To query multiple content types through a shared schema, Optimizely Graph requires a Graph Contract.&lt;/p&gt;
&lt;p class=&quot;MsoBodyText&quot;&gt;For example:&lt;/p&gt;
&lt;p class=&quot;SourceCode&quot;&gt;&lt;code&gt;&lt;span class=&quot;OperatorTok&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt;ContentType&lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;]&lt;/span&gt;&lt;/code&gt;&lt;br /&gt;&lt;code&gt;&lt;span class=&quot;KeywordTok&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;KeywordTok&quot;&gt;interface&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt; ISitePageData&lt;/span&gt;&lt;/code&gt;&lt;span class=&quot;OperatorTok&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;FirstParagraph&quot;&gt;The contract must then be implemented by each content type that should participate in the shared schema:&lt;/p&gt;
&lt;p class=&quot;FirstParagraph&quot;&gt;&lt;code&gt;&lt;span class=&quot;KeywordTok&quot;&gt;public class&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt; ArticlePage &lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt; ISitePageData&lt;/span&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p class=&quot;FirstParagraph&quot;&gt;&lt;br /&gt;&lt;code&gt;&lt;span class=&quot;KeywordTok&quot;&gt;public class&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt; NewsPage &lt;/span&gt;&lt;span class=&quot;OperatorTok&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;NormalTok&quot;&gt; ISitePageData&lt;/span&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p class=&quot;FirstParagraph&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;FirstParagraph&quot;&gt;When content is synchronized to Graph, a shared Graph type is generated for the contract, allowing queries across all implementing content types.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Content Type Management UI Behavior&lt;/strong&gt;&lt;/p&gt;
&lt;p class=&quot;isSelectedEnd&quot;&gt;One side effect of using a Graph Contract is that the interface becomes visible in the CMS Content Type Management UI.&lt;/p&gt;
&lt;p class=&quot;isSelectedEnd&quot;&gt;For example, after adding the &lt;code&gt;&lt;span class=&quot;text-token-text-primary cursor-text rounded-sm&quot;&gt;[ContentType]&lt;/span&gt;&lt;/code&gt; attribute to the interface:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&quot;text-token-text-primary cursor-text rounded-sm&quot;&gt;[ContentType]&lt;/span&gt;
public interface ISitePageData
{
}&lt;/code&gt;&lt;/pre&gt;
&lt;p class=&quot;isSelectedEnd&quot;&gt;the contract appears as a content type in the CMS administration interface as following:&lt;br /&gt;&lt;img src=&quot;/link/abee47fc9c3149f891781f28a055797b.aspx&quot; width=&quot;1437&quot; height=&quot;414&quot; /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;This behavior can be surprising at first, especially since the interface is not intended to be created or edited by content editors. However, this is currently the mechanism Optimizely Graph uses to discover and generate shared schema contracts.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Key takeaway:&lt;/strong&gt; In Optimizely Graph, interfaces annotated with &lt;code&gt;[ContentType]&lt;/code&gt; enable cross-type queries. Base class inheritance does not.&lt;br /&gt;&lt;strong&gt;Impact:&lt;/strong&gt; Search architectures based on inheritance hierarchies or marker interfaces must be redesigned using Graph Contracts. This can affect query models, schema design, and migration effort.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;4. Using the Content Type Management UI to Change a Property&#39;s Indexing Type&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;&lt;img src=&quot;/link/0c261ffa36a241f09ab67bea6b4d32e6.aspx&quot; width=&quot;1147&quot; height=&quot;535&quot; /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The indexing convention API cannot be used to exclude a field from Graph indexing. It only supports extension methods for calculated properties, not actual content type properties.&lt;/p&gt;
&lt;p&gt;I tried to add this below to disable an actual field:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services.AddContentGraph(configureConventions: c =&amp;gt;
{
    c.ForInstancesOf&amp;lt;StandardPage&amp;gt;().IncludeField(x =&amp;gt; x.ContentAreaCssClass, IndexingType.Disabled);
});&lt;br /&gt;&lt;/code&gt;&lt;br /&gt;Here is the error when I try to disable a field from indexing via convention:&lt;br /&gt;&lt;br /&gt;&lt;img src=&quot;/link/c483e294bb81491b89e2937c0d7b958e.aspx&quot; width=&quot;792&quot; height=&quot;268&quot; /&gt;&lt;/pre&gt;
&lt;p&gt;To disable indexing for a field, use the Content Type Management UI or apply the following attribute to the property in your content type:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[IndexingType(IndexingType.Disabled)]
public virtual bool HideSiteFooter { get; set; }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;5. &lt;span style=&quot;font-size: 12.0pt; font-family: &#39;Aptos&#39;,sans-serif; mso-ascii-theme-font: minor-latin; mso-fareast-font-family: Aptos; mso-fareast-theme-font: minor-latin; mso-hansi-theme-font: minor-latin; mso-bidi-font-family: &#39;Times New Roman&#39;; mso-bidi-theme-font: minor-bidi; mso-ansi-language: EN-US; mso-fareast-language: EN-US; mso-bidi-language: AR-SA;&quot;&gt;Graph Contracts and Content Types Must Use the Same IndexingType&lt;br /&gt;&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p class=&quot;FirstParagraph&quot;&gt;When working with Graph Contracts, I discovered an unexpected validation rule that can prevent the application from starting.&lt;/p&gt;
&lt;p class=&quot;MsoBodyText&quot;&gt;Optimizely Graph validates property metadata across the entire contract hierarchy during startup. If the same property exists on both a Graph Contract and a content type, the IndexingType must be identical everywhere.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;&lt;/p&gt;
&lt;p class=&quot;FirstParagraph&quot;&gt;Graph Contract:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[ContentType]
public interface ISitePageData
{    
    string ContentTypeName { get; }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p class=&quot;FirstParagraph&quot;&gt;Base content type:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public abstract class SitePageData : PageData, ISitePageData
{
    [GraphProperty(IndexingType.Queryable)]
    public virtual string ContentTypeName =&amp;gt;
    GetOriginalType().Name;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p class=&quot;FirstParagraph&quot;&gt;&lt;span class=&quot;OperatorTok&quot;&gt;&lt;br /&gt;Here is the error you will see if this happens:&lt;br /&gt;&lt;br /&gt;&lt;img src=&quot;/link/2162fb9599484f8cb164cddc1b8897a3.aspx&quot; width=&quot;849&quot; height=&quot;535&quot; /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;SourceCode&quot;&gt;&lt;span class=&quot;OperatorTok&quot;&gt;&lt;strong&gt;Impact:&lt;/strong&gt; Inconsistent indexing metadata can prevent the application from starting. This validation rule becomes especially important when multiple teams maintain shared contracts and content models.&lt;/span&gt;&lt;/p&gt;
&lt;p class=&quot;SourceCode&quot;&gt;&lt;span class=&quot;OperatorTok&quot;&gt;&lt;strong&gt;6.&amp;nbsp;&lt;/strong&gt;&lt;/span&gt;&lt;strong&gt;Getter-only/fallback properties are included in the Graph schema, but their indexed values are null&lt;br /&gt;&lt;/strong&gt;&lt;span class=&quot;OperatorTok&quot;&gt;&lt;strong&gt;&lt;br /&gt;&lt;/strong&gt;For example:&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public string ContentAreaCssClass =&amp;gt; &quot;teaserblock&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public virtual string TeaserText
{
    get
    {
        var teaserText = this.GetPropertyValue(p =&amp;gt; p.TeaserText);

        // Use explicitly set teaser text, otherwise fall back to description
        return !string.IsNullOrWhiteSpace(teaserText)
            ? teaserText
            : MetaDescription;
    }
    set =&amp;gt; this.SetPropertyValue(p =&amp;gt; p.TeaserText, value);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Here are the their value when indexing:&lt;br /&gt;&lt;br /&gt;&lt;img src=&quot;/link/f52a342aacf54f4c9cad1852c93e61dd.aspx&quot; width=&quot;935&quot; height=&quot;461&quot; /&gt;&lt;br /&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Impact: &lt;/strong&gt;Properties that appear to work correctly in CMS may produce unexpected Graph results. Search queries, filters, and facets relying on computed values may silently return incorrect data.&lt;strong&gt;&lt;br /&gt;&lt;br /&gt;7. Extension Methods Cannot Be Added to a Graph Contract as Calculated Fields&lt;/strong&gt;&lt;/p&gt;
&lt;p class=&quot;SourceCode&quot;&gt;&lt;span class=&quot;OperatorTok&quot;&gt;When using a Graph Contract, extension methods cannot be added as calculated fields on the contract itself.&lt;br /&gt;Here is the error that I got when trying to do this:&lt;br /&gt;&lt;br /&gt;&lt;img src=&quot;/link/12b8f476d6424d569475e365043348c1.aspx&quot; width=&quot;779&quot; height=&quot;264&quot; /&gt;&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;Impact:&lt;/strong&gt; Shared Graph Contracts cannot be enriched with convention-based calculated fields. Developers may need to duplicate calculated properties across content types or redesign their schema strategy.&lt;/span&gt;&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Optimizely Graph provides a powerful and flexible search platform, but it differs significantly from Search &amp;amp; Navigation in several areas.&lt;/p&gt;
&lt;p&gt;When migrating existing Find implementations, pay particular attention to:&lt;/p&gt;
&lt;p&gt;- Graph Contracts&lt;br /&gt;- Graph Convention limitations&lt;br /&gt;- Faceting behavior&lt;br /&gt;- Property indexing rules&lt;br /&gt;- Calculated fields&lt;/p&gt;
&lt;p&gt;Understanding these differences early can save significant debugging and migration effort.&lt;/p&gt;</id><updated>2026-06-24T11:26:37.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Full implementation - Fallback languages with Optimizely Graph</title><link href="https://world.optimizely.com/blogs/binh-nguyen/dates/2025/11/full-implementation---fallback-languages-with-optimizely-graph/" /><id>&lt;p&gt;Nowadays, many people choose a headless approach when developing Optimizely CMS/Commerce projects using Opti Graph.&lt;br /&gt;One challenge we may face is implementing language fallback, as it is not supported by default. There are a few tips available, and today I want to share my complete implementation. I hope it will help others who need to achieve the same thing.&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;&lt;strong&gt;1. In back-end code, add more fallback language property for Graph model:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public class FallbackLanguageContentsApiModelProperty(
    ILanguageBranchRepository languageBranchRepository,
    ICustomUrlService customUrlService,
    IContentLanguageSettingsHandler contentLanguageSettingsHandler) : IContentApiModelProperty
{
    public object GetValue(ContentApiModel contentApiModel)
    {
        if (contentApiModel.ContentLink != null)
        {
            var enabledLanguages = languageBranchRepository.ListEnabled();
            var pagesThatFallBackContentToCurrentPage = new List&amp;lt;FallbackLanguageContent&amp;gt;();
            var cRef = contentApiModel.ContentLink.ToContentReference();

            foreach (var enabledLanguage in enabledLanguages)
            {
                var fallbackLanguages = contentLanguageSettingsHandler.GetFallbackLanguages(cRef, enabledLanguage.Culture.Name);
                if (fallbackLanguages != null &amp;amp;&amp;amp; fallbackLanguages.Any() &amp;amp;&amp;amp; fallbackLanguages.Contains(contentApiModel.Language.Name))
                {
                    var lang = enabledLanguage.Culture.Name;

                    pagesThatFallBackContentToCurrentPage.Add(new FallbackLanguageContent()
                    {
                        LanguageName = lang,
                        RelativePath = !ContentReference.IsNullOrEmpty(cRef) ? customUrlService.GetRelativeUrl(cRef, lang) : string.Empty
                    });
                }
            }

            return pagesThatFallBackContentToCurrentPage;
        }
        else
        {
            return new List&amp;lt;FallbackLanguageContent&amp;gt;();
        }
    }

    public string Name =&amp;gt; &quot;FallbackLanguageContents&quot;;
}

internal class FallbackLanguageContent
{
    public required string LanguageName { get; set; }
    public required string RelativePath { get; set; }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. In the front-end code, we need to query content using a relative path and locale, including the fallback logic. Here is the query:&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre class=&quot;language-html&quot;&gt;&lt;code&gt;query getContentByPathWithinFallback($path: [String!]!, $locale: String, $siteId: String) {
  content: Content(
    where: {
      SiteId: { eq: $siteId }
      _or: [
        { _and: [{ Language: { Name: { eq: $locale, boost: 2 } } }, { RelativePath: { in: $path } }] }
        { _and: [{ FallbackLanguageContents: { LanguageName: { eq: $locale, boost: 1 } } }, { FallbackLanguageContents: { RelativePath: { in: $path } } }] }
      ]
    }
    locale: ALL
  ) {
    items: item {
      ...IContentData
      ...PageData
    }
  }
}
fragment PageData on IContent {
  ...IContentData
}
fragment IContentData on IContent {
  contentType: ContentType
  _metadata: ContentLink {
    id: Id
    version: WorkId
    key: GuidValue
  }
  locale: Language {
    name: Name
  }
  path: RelativePath
  _type: __typename
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;By using boost values in the query, we can indicate which conditions should have higher priority. As a result, the fallback content is returned only when no content exists for the exact locale.&lt;/p&gt;
&lt;p&gt;If you are using Optimizely SaaS Starter for your headless solution, you can call your custom content query in &lt;code&gt;src/app/[[...path]]/page.tsx&lt;/code&gt; by replacing the following section:&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;&lt;img src=&quot;/link/86bcc904fdcd4054aa19b44e4160c3a0.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;That&#39;s all. Hope this makes your multilingual setup a bit easier. Happy coding!&lt;/p&gt;</id><updated>2025-11-15T11:26:16.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>How to have a link plugin with extra link id attribute in TinyMce</title><link href="https://world.optimizely.com/blogs/binh-nguyen/dates/2024/7/how-to-have-a-link-plugin-with-extra-link-id-attribute-in-tinymce/" /><id>&lt;p&gt;&lt;strong&gt;Introduce&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Optimizely CMS Editing is using TinyMce for editing rich-text content. We need to use this control a lot in CMS site for kind of WYSWYG content.&lt;/p&gt;
&lt;p&gt;I feel quite happy to use it and one of reason that I like to use tinymce is easy to customization. We can add available plug-ins that we want into toolbar only via configuration in server code. We also can build new plug-ins via javascript and register them in server code to use.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Today, I want to share the way to add a new plug-in as same epi link plug-in with adding more extra id attribute.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 1: &lt;/strong&gt;Create new Link Model in server code with Link Id attribute. Currenty, Link Editor control is using kind of this model type to render to corresponding user interface.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;using EPiServer.Cms.Shell.UI.ObjectEditing.InternalMetadata;
using System.ComponentModel;

namespace Sample_Sites.Models
{
    public class CustomLinkModel : LinkModel
    {
        [DisplayName(&quot;Link Id&quot;)]
        public string LinkId { get; set; }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 2:&amp;nbsp;&lt;/strong&gt;Create TinyMce plug-in via javascript named customLink.js under folder &quot;wwwroot/ClientResources/Scripts/tinymce-plugins&quot;. In this example, I use Dojo module to do it because I want to re-use code of epi link plug-in. But&amp;nbsp; you can completely use vanilla javascript to create plug-in only with using &amp;nbsp;tinymce.PluginManager.add to add new button in TinyMce toolbar&lt;/p&gt;
&lt;p&gt;Here is the place that I change to indicate using our custom link model for Link Editor instead of default one&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt; var linkEditor = new LinkEditor({
     baseClass: &quot;epi-link-item&quot;,
     modelType: &quot;Sample_Sites.Models.CustomLinkModel&quot;,
     hiddenFields: [&quot;text&quot;] // hide text field from UI
});&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here is the place that I change to read Id value from a element&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;if (href.length) {
    linkObject.href = href;
    linkObject.targetName = dom.getAttrib(selectedLink, &quot;target&quot;);
    linkObject.title = dom.getAttrib(selectedLink, &quot;title&quot;);
    linkObject.linkId = dom.getAttrib(selectedLink, &quot;id&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here is the place that I change to set Id attribute for a element&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;  var callbackMethod = function (value) {
      if (value &amp;amp;&amp;amp; value.href) {
          var linkAttributes = {
              href: value.href,
              title: value.title,
              target: value.target ? value.target : null,
              id: value.linkId ? value.linkId : null
          };&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here is full javascript file for custom link plug-in:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;define(&quot;alloy/tinymce-plugins/customLink&quot;, [
    &quot;dojo/_base/lang&quot;,
    &quot;dojo/on&quot;,
    &quot;epi/shell/widget/dialog/Dialog&quot;,
    &quot;epi-cms/ApplicationSettings&quot;,
    &quot;epi-cms/widget/LinkEditor&quot;,
    &quot;epi-addon-tinymce/tinymce-loader&quot;,
    &quot;epi-addon-tinymce/plugins/epi-link/linkViewModel&quot;,
    &quot;epi/i18n!epi/cms/nls/episerver.cms.widget.editlink&quot;,
    &quot;epi/i18n!epi/cms/nls/episerver.cms.tinymce.plugins.epilink&quot;
], function (lang, on, Dialog, ApplicationSettings, LinkEditor, tinymce, linkViewModel, resource, pluginResource) {

    tinymce.PluginManager.add(&quot;custom-link&quot;, function (editor) {
        function mceEPiLink() {
            var href = &quot;&quot;,
                s = editor.selection,
                dom = editor.dom,
                linkObject = {};

            // CMS-20837: when users use the search function of Chrome (ctrl+f), the highlighted text will be un-highlighted
            // clone the selection here so it will not be affected by Chrome.
            var originalSelection = editor.selection.getRng().cloneRange();

            // When link is at the beginning of a paragraph, then IE (and FF?) returns the paragraph from getNode,
            // the getStart() and getEnd() however returns the anchor.
            var node = s.getStart() === s.getEnd() ? s.getStart() : s.getNode(),
                selectedLink = linkViewModel.getAnchorElement(editor, node);

            // No selection and not in link
            if (s.isCollapsed() &amp;amp;&amp;amp; !selectedLink) {
                return;
            }

            if (selectedLink) {
                href = dom.getAttrib(selectedLink, &quot;href&quot;);
            }

            if (href.length) {
                linkObject.href = href;
                linkObject.targetName = dom.getAttrib(selectedLink, &quot;target&quot;);
                linkObject.title = dom.getAttrib(selectedLink, &quot;title&quot;);
                linkObject.linkId = dom.getAttrib(selectedLink, &quot;id&quot;);
            }

            var callbackMethod = function (value) {
                if (value &amp;amp;&amp;amp; value.href) {
                    var linkAttributes = {
                        href: value.href,
                        title: value.title,
                        target: value.target ? value.target : null,
                        id: value.linkId ? value.linkId : null
                    };

                    // CMS-20837: and set the selection again if selection lost its value.
                    if (!editor.selection.getContent({ format: &quot;html&quot; })) {
                        editor.selection.setRng(originalSelection);
                    }

                    if (selectedLink) {
                        dom.setAttribs(selectedLink, linkAttributes);
                    } else {
                        if (linkViewModel._isImageFigure(node)) {
                            linkViewModel.linkImageFigure(editor, node, linkAttributes);
                        } else {
                            // When opening the link properties dialog in OPE mode an inline iframe is used rather than a popup window.
                            // When using IE clicking in this iframe causes the selection to collapse in the TinyMCE iframe which
                            // breaks the link creation immediately below. The workaround is to store the selection range before
                            // opening, and restoring it before creating the link.
                            s.setRng(s.getRng());
                            // To make sure we dont get nested links and have the same behavior as the default tiny
                            // link dialog we unlink any links in the selection before we create the new link.
                            editor.getDoc().execCommand(&quot;unlink&quot;, false, null);
                            editor.execCommand(&quot;mceInsertLink&quot;, false, &quot;#mce_temp_url#&quot;, { skip_undo: 1 });

                            var elementArray = tinymce.grep(dom.select(&quot;a&quot;), function (n) {
                                return dom.getAttrib(n, &quot;href&quot;) === &quot;#mce_temp_url#&quot;;
                            });
                            for (var i = 0; i &amp;lt; elementArray.length; i++) {
                                dom.setAttribs(elementArray[i], linkAttributes);
                            }

                            //move selection into the link content to be able to recognize it when looking at selection
                            if (elementArray.length &amp;gt; 0) {
                                var range = editor.dom.createRng();
                                range.selectNodeContents(elementArray[0]);
                                editor.selection.setRng(range);
                            }
                        }
                    }
                } else if (selectedLink) {
                    // pressed delete?
                    dom.setOuterHTML(selectedLink, selectedLink.innerHTML);
                    editor.undoManager.add();
                }
            };

            linkObject.target = linkViewModel.findFrameId(ApplicationSettings.frames, linkObject.targetName);

            var linkEditor = new LinkEditor({
                baseClass: &quot;epi-link-item&quot;,
                //TODO: hardcoded for now
                modelType: &quot;Sample_Sites.Models.CustomLinkModel&quot;,
                hiddenFields: [&quot;text&quot;] // hide text field from UI
            });

            //Find all Anchors in the document and add them to the Anchor list
            var allLinks = editor.getDoc().querySelectorAll(&quot;a[id],a[name]&quot;);

            // If the user is using IE 11 or lower we need to convert the
            // nodeList to a regular array
            // HACK: IE11
            if (tinymce.Env.ie &amp;amp;&amp;amp; tinymce.Env.ie &amp;lt; 12) {
                allLinks = Array.prototype.slice.call(allLinks);
            }

            var anchors = linkViewModel.findNamedAnchors(allLinks);

            linkEditor.on(&quot;fieldCreated&quot;, function (fieldname, widget) {
                if (fieldname === &quot;href&quot;) {
                    // in this case, widget is HyperLinkSelector
                    var hyperLinkSelector = widget;
                    var anchor = linkViewModel.getFirstAnchorWidget(hyperLinkSelector.get(&quot;wrappers&quot;));

                    if (anchor &amp;amp;&amp;amp; anchor.inputWidget) {
                        anchor.inputWidget.set(&quot;selections&quot;, anchors);
                    } else {
                        widget.on(&quot;selectorsCreated&quot;, function (hyperLinkSelector) { // when all selector have been created
                            var anchorWidget = linkViewModel.getFirstAnchorWidget(hyperLinkSelector.get(&quot;wrappers&quot;));

                            if (anchorWidget &amp;amp;&amp;amp; anchorWidget.inputWidget) {
                                anchorWidget.inputWidget.set(&quot;selections&quot;, anchors);
                                anchorWidget.domNode.style.display = &quot;block&quot;;
                            }
                        });
                    }

                    if (anchor) {
                        anchor.domNode.style.display = &quot;block&quot;;
                    }
                }
            });

            var dialogTitle = lang.replace(selectedLink ? resource.title.template.edit : resource.title.template.create, resource.title.action);

            var dialog = new Dialog({
                title: dialogTitle,
                dialogClass: &quot;epi-dialog-portrait&quot;,
                content: linkEditor,
                defaultActionsVisible: false
            });
            dialog.startup();

            //Set the value when the provider/consumer has been initialized
            linkEditor.set(&quot;value&quot;, linkObject);

            dialog.show();
            editor.fire(&quot;OpenWindow&quot;, {
                win: null
            });

            dialog.on(&quot;execute&quot;, function () {

                var value = linkEditor.get(&quot;value&quot;);
                var linkObject = lang.clone(value);

                if (linkObject &amp;amp;&amp;amp; linkObject.target) {
                    // get target frame name, instead of integer value
                    linkObject.target = linkViewModel.findFrameName(ApplicationSettings.frames, linkObject.target);
                }

                //Destroy the editor when the dialog closes
                linkEditor.destroy();
                linkEditor = null;

                callbackMethod(linkObject);
            });

            dialog.on(&quot;hide&quot;, function () {
                editor.fire(&quot;CloseWindow&quot;, {
                    win: null
                });
            });
        }

        // Register buttons
        editor.ui.registry.addToggleButton(&quot;custom-link&quot;, {
            tooltip: pluginResource.title,
            onAction: mceEPiLink,
            icon: &quot;link&quot;,
            onSetup: function (buttonApi) {
                function selectionChange(e) {
                    var anchorElement = linkViewModel.getAnchorElement(editor, e.element);
                    var invalidSelection = !linkViewModel.hasValidSelection(editor, e.element);
                    buttonApi.setEnabled(!(invalidSelection &amp;amp;&amp;amp; !anchorElement));
                    buttonApi.setActive(!editor.readonly &amp;amp;&amp;amp; !!anchorElement);
                }

                editor.on(&quot;SelectionChange&quot;, selectionChange);

                return function () {
                    editor.off(&quot;SelectionChange&quot;, selectionChange);
                };
            }
        });

        editor.shortcuts.add(&quot;ctrl+k&quot;, pluginResource.title, mceEPiLink);

        return {
            getMetadata: function () {
                return {
                    name: &quot;Link (epi)&quot;,
                    url: &quot;https://www.optimizely.com&quot;
                };
            }
        };
    });
});

dojo.require(&quot;alloy/tinymce-plugins/customLink&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Last step:&amp;nbsp;&lt;/strong&gt;Add TinyMce configuration with adding your custom plug-in in the toolbar&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;services.Configure&amp;lt;TinyMceConfiguration&amp;gt;(config =&amp;gt;
{
	config.InheritSettingsFromAncestor = true;
	config.Default()
		 .AddExternalPlugin(&quot;custom-link&quot;, &quot;/ClientResources/Scripts/tinymce-plugins/customLink.js&quot;)
		 .Toolbar(&quot;styles | bold italic underline | custom-link anchor | image epi-image-editor epi-personalized-content | bullist numlist outdent indent | epi-dnd-processor | removeformat | fullscreen code&quot;)
		 .AddPlugin(&quot;code&quot;);
});&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Finally, check if the plugin is displayed in the TinyMCE editor in Edit Mode. Thankfully, it works! :)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/af7f8e03f2ea4f36a3dd26ca1962827b.aspx&quot; width=&quot;1491&quot; height=&quot;793&quot; /&gt;&lt;/p&gt;
&lt;p&gt;You can see this link &lt;a href=&quot;https://tedgustaf.com/blog/2022/adding-custom-tinymce-plugin-to-the-html-editor-in-optimizely-cms/&quot;&gt;https://tedgustaf.com/blog/2022/adding-custom-tinymce-plugin-to-the-html-editor-in-optimizely-cms/&lt;/a&gt; to know how to add a new TinyMce plug-in in general&lt;/p&gt;</id><updated>2024-07-13T09:10:24.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Optimizely Search and Navigation - Part 2 - Filter Tips</title><link href="https://world.optimizely.com/blogs/binh-nguyen/dates/2024/6/optimizely-search-and-navigation---part-2---filter-tips/" /><id>&lt;h3&gt;Introduction&lt;/h3&gt;
&lt;p&gt;Continuing from &lt;a href=&quot;#link-to-part-1&quot;&gt;Part 1 &amp;ndash; Search Tips&lt;/a&gt;, today I will share the next part &amp;ndash; filter tips.&lt;/p&gt;
&lt;p&gt;The platform versions used for this article are Optimizely CMS 12.27.x, Optimizely Customized Commerce 14.21.x, and EpiServer.Find 16.1.x.&lt;/p&gt;
&lt;h3&gt;How to Add a New Custom Filter&lt;/h3&gt;
&lt;p&gt;In the built-in Optimizely Search &amp;amp; Navigation, the default filters are as follows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;And Filter&lt;/li&gt;
&lt;li&gt;Bool Filter&lt;/li&gt;
&lt;li&gt;Exists Filter&lt;/li&gt;
&lt;li&gt;Geo Distance Filter&lt;/li&gt;
&lt;li&gt;Geo Distance Range Filter&lt;/li&gt;
&lt;li&gt;Geo Polygon Filter&lt;/li&gt;
&lt;li&gt;Has Child Filter&lt;/li&gt;
&lt;li&gt;Ids Filter&lt;/li&gt;
&lt;li&gt;Kilometers Filter&lt;/li&gt;
&lt;li&gt;Nested Filter&lt;/li&gt;
&lt;li&gt;Not Filter&lt;/li&gt;
&lt;li&gt;Or Filter&lt;/li&gt;
&lt;li&gt;Prefix Filter&lt;/li&gt;
&lt;li&gt;Query Filter&lt;/li&gt;
&lt;li&gt;Range Filter&lt;/li&gt;
&lt;li&gt;Script Filter&lt;/li&gt;
&lt;li&gt;Term Filter&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These filters generate corresponding body text when sending requests to Elasticsearch through the Search &amp;amp; Navigation framework using JSON converters.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What if a filter exists in Elasticsearch but not in Search &amp;amp; Navigation, or an existing filter lacks required parameters? Can you create a new filter?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Yes, you can add new custom filters in Search &amp;amp; Navigation. Here&#39;s how to build a new Regular Expression filter.&lt;/p&gt;
&lt;h4&gt;Step 1: Create a New DTO Based on Filter Class&lt;/h4&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;[JsonConverter(typeof(RegularExpressionFilterConverter))]
public class RegularExpressionFilter : Filter
{
    [JsonIgnore]
    public string Field { get; set; }

    [JsonProperty(&quot;value&quot;)]
    public string Value { get; set; }

    public RegularExpressionFilter(string field, string value)
    { 
        Field = field;
        Value = value;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Step 2: Create a New JSON Converter for the New Filter&lt;/h4&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;internal class RegularExpressionFilterConverter : CustomWriteConverterBase&amp;lt;RegularExpressionFilter&amp;gt;
{
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        if (value is not RegularExpressionFilter)
        {
            writer.WriteNull();
            return;
        }
        var regularExpressionFilter = (RegularExpressionFilter)value;
        writer.WriteStartObject();
        writer.WritePropertyName(&quot;regexp&quot;);
        writer.WriteStartObject();
        writer.WritePropertyName(regularExpressionFilter.Field);
        serializer.Serialize(writer, regularExpressionFilter.Value);
        writer.WriteEndObject();
        writer.WriteEndObject();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Step 3: Create a New Filter Method for Your Search&lt;/h4&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public static DelegateFilterBuilder MatchRegularExpression(this string value, string input)
{ 
   return new DelegateFilterBuilder((string field) =&amp;gt; new RegularExpressionFilter(field, input));
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Step 4: Apply the New Filter to Your Search&lt;/h4&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;if (!string.IsNullOrEmpty(filterOptions.Q)){
    query = query.Filter(x =&amp;gt; x.Name.MatchRegularExpression(filterOptions.Q));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The keyword for search in this example could be text within regular expression syntax. You can find syntax for ElasticSearch &lt;a href=&quot;https://www.elastic.co/guide/en/elasticsearch/reference/current/regexp-syntax.html&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;How to Apply AND/OR for a Group of Sub Filters&lt;/h3&gt;
&lt;p&gt;Consider the following example:&lt;/p&gt;
&lt;p&gt;You have a product content type with these properties:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;[Display(Name = &quot;On sale&quot;, GroupName = SystemTabNames.Content, Order = 50)]
public virtual bool OnSale { get; set; }

[Display(Name = &quot;New arrival&quot;, GroupName = SystemTabNames.Content, Order = 55)]
public virtual bool NewArrival { get; set; }

[Display(Name = &quot;Best seller&quot;, GroupName = SystemTabNames.Content, Order = 58)]
public virtual bool BestSeller { get; set; }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To find products that are on sale and/or new arrivals, you can:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Use AND/OR Operator in FilterExpression:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;AND operator&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;query = query.Filter(x =&amp;gt; x.OnSale.Match(true) &amp;amp; x.NewArrival.Match(true));&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt; &lt;/strong&gt;&lt;strong&gt;OR operator&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;query = query.Filter(x =&amp;gt; x.OnSale.Match(true) | x.NewArrival.Match(true));&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Use AND/OR Filter:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;AND filter&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;var andFilter = new AndFilter();
andFilter.Filters.Add(new TermFilter(searchClient.GetFullFieldName(&quot;OnSale&quot;, typeof(bool)), true));
andFilter.Filters.Add(new TermFilter(searchClient.GetFullFieldName(&quot;NewArrival&quot;, typeof(bool)), true));
query = query.Filter(andFilter);&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt; &lt;/strong&gt;&lt;strong&gt;OR Filter &lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;var orFilter = new OrFilter();
orFilter.Filters.Add(new TermFilter(searchClient.GetFullFieldName(&quot;OnSale&quot;, typeof(bool)), true));
orFilter.Filters.Add(new TermFilter(searchClient.GetFullFieldName(&quot;NewArrival&quot;, typeof(bool)), true));
query = query.Filter(orFilter);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note that the field name understood by Elasticsearch is not the same as the field name in the Optimizely Content Type Model. Use the following method to get the indexed field name:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public static string GetFullFieldName(this IClient searchClient, string fieldName, Type type)
{
    if (type != null)
        return fieldName + searchClient.Conventions.FieldNameConvention.GetFieldName(Expression.Variable(type, fieldName));
    return fieldName;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Pros and Cons:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Using AND/OR Operator:&lt;/strong&gt; Short and easy to use but not suitable for dynamically building filters.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Using AND/OR Filter:&lt;/strong&gt; Longer code but allows for flexible filter building based on field names and input values.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;How to Sort Search Results Based on a Set of Conditions&lt;/h3&gt;
&lt;p&gt;Search &amp;amp; Navigation allows sorting based on one or more fields:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;query = query.OrderBy(x =&amp;gt; x.Name).ThenByDescending(x =&amp;gt; x.StartPublish);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Example Requirement:&lt;/strong&gt; Display best seller products at the top, followed by new arrivals, and then on sale products.&lt;/p&gt;
&lt;p&gt;You can achieve this by using boost matching:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;query = query.BoostMatching(x =&amp;gt; (x as GenericProduct).NewArrival.Match(true), 2);
query = query.BoostMatching(x =&amp;gt; (x as GenericProduct).OnSale.Match(true), 3);
query = query.BoostMatching(x =&amp;gt; (x as GenericProduct).BestSeller.Match(true), 4);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Issue:&lt;/strong&gt; If a product is both on sale and a new arrival, it appears at the top even if it is not a best seller. The score of this product (5) is higher than that of a best seller (4).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; Ensure best sellers are always on top by setting Boost value as following rule: Next boost value = total of all previous boost values + 1:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;query = query.BoostMatching(x =&amp;gt; (x as GenericProduct).NewArrival.Match(true), 2);
query = query.BoostMatching(x =&amp;gt; (x as GenericProduct).OnSale.Match(true), 3);
query = query.BoostMatching(x =&amp;gt; (x as GenericProduct).BestSeller.Match(true), 6);&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;These tips are based on my experience with Search &amp;amp; Navigation. I hope they help you implement similar features.&lt;/p&gt;
&lt;p&gt;Enjoy coding!&lt;/p&gt;</id><updated>2024-07-01T08:44:17.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Optimizely Search and Navigation – Part 1 – Search Tips</title><link href="https://world.optimizely.com/blogs/binh-nguyen/dates/2024/5/optimizely-search-and-navigation--part-1--search-tips/" /><id>&lt;p&gt;&lt;span style=&quot;font-size: 18pt;&quot;&gt;&lt;strong&gt;Introduction&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Search and Navigation is a cloud service provided by Optimizely to support building search functionality for both Optimizely CMS and Optimizely Commerce sites. This platform uses Elasticsearch behind the scenes to serve search, filter, facet, and indexing functionalities with the strong capabilities of Elasticsearch.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;I have experience applying Search &amp;amp; Navigation to both CMS and Commerce projects. So today, I will share some tips for searching with you.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-size: 18pt;&quot;&gt;&lt;strong&gt;Which version of Optimizely CMS and Search &amp;amp; Navigation&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;The platform versions that I am using for this article are Optimizely CMS 12.27.x and EpiServer.Find 16.1.x&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-size: 18pt;&quot;&gt;&lt;strong&gt;Difference between search and filter&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Search Concept:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;The search concept in Elasticsearch refers to the process of querying the index to retrieve documents that match certain criteria.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;When you perform a search, Elasticsearch calculates a relevance score for each document based on how well it matches the query.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Search queries can be performed using various query DSL (Domain Specific Language) constructs such as match queries, term queries, range queries, etc.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Search results are typically sorted by relevance, ut you can also specify sorting criteria based on specific fields.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Filter Concept:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Filters, on the other hand, are used to narrow down the set of documents returned by a query without affecting the relevance scores.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Filters are typically used for exact matches or ranges and are applied to the documents after the initial search.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Filters can significantly improve the performance of queries, especially for frequently used and static criteria.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Unlike search queries, filters are not scored, so they are faster but less flexible in terms of matching documents.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;In summary, while both searches and filters are used to retrieve documents from Elasticsearch, searches are used to find documents based on relevance scores calculated against the query, while filters are used to narrow down the set of documents without affecting relevance scoring.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-size: 18pt;&quot;&gt;&lt;strong&gt;How to apply free-text search with contains operator&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;We can apply the contains operator for free-text search by using wildcard as follows:&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;query = query.For(searchTerm, options =&amp;gt; {
    options.Query = $&quot;*{searchTerm}*&quot;;
    options.AllowLeadingWildcard = true;
    options.AnalyzeWildcard = true;
    options.RawQuery = searchTerm;
});&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;&lt;em&gt;AllowLeadingWildcard&lt;/em&gt;:&lt;/span&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt; If&amp;nbsp;true, the wildcard characters&amp;nbsp;*&amp;nbsp;and&amp;nbsp;?&amp;nbsp;are allowed as the first character of the query string. Defaults to&amp;nbsp;true&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;&lt;em&gt;AnalyzeWildcard&lt;/em&gt;:&lt;/span&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt; If true, the query attempts to analyze wildcard terms in the query string. Defaults to false.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;The below image&lt;/span&gt; is the result when I find products via free-text search - contains operator - In Name field:&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;&lt;img src=&quot;/link/c9616cd10f8f4d138ee31c9017392333.aspx&quot; /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-size: 18pt;&quot;&gt;&lt;strong&gt;How free-text search works if a search term contains multiple words&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Actually, when we do a free-text search with For method by default then your search term will be analyzed to separated words and do search by separated words with OR operator by default. You can change the operator to AND by this setting:&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;query = query.For(searchTerm, options =&amp;gt; {
    options.DefaultOperator = EPiServer.Find.Api.Querying.Queries.BooleanOperator.And;
})});&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;OR&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;query = query.For(searchTerm) }).WithAndAsDefaultOperator();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;The below image&lt;/span&gt; is the result when I find products via free-text search - AND operator - In Name field:&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;&lt;img src=&quot;/link/10be03209e0f417780dcca3ce698c49a.aspx&quot; /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;&lt;strong&gt;Note&lt;/strong&gt;: The AND operator here means that search results should contain all words in the search term without order respect.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-size: 18pt;&quot;&gt;&lt;strong&gt;How to apply the free-text search for the whole phrase&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Sometimes you want to search contents by the exact word or phrase that you input. So in order to apply the free-text search for the whole phrase, you can wrap your phrase with this format &amp;ldquo;{words}&amp;rdquo; like this:&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;query = query.For($&amp;rdquo;\&amp;rdquo;{searchTerm}\&amp;rdquo;&amp;rdquo;);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;The below image&lt;/span&gt; is the result when I find products via free-text search - whole phrase matching - In Name field:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/69d7239382aa43bab9c3d3a5e489b6de.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-size: 18pt;&quot;&gt;&lt;strong&gt;How to apply fuzzy search&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Fuzzy search can be used to search for content even when the user makes typographical errors, or when you want to return all search results that match the exact keyword as well as the approximate keyword. However, you need to consider whether you really want to use fuzzy search or not because it does not perform as well as normal free-text search.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Here is a way that I used to apply fuzzy search:&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;First, adding an extension method to build a fuzzy query&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public static ITypeSearch&amp;lt;TSource&amp;gt; FuzzySearch&amp;lt;TSource&amp;gt;(this ITypeSearch&amp;lt;TSource&amp;gt; search, string query, Expression&amp;lt;Func&amp;lt;TSource, string&amp;gt;&amp;gt; fieldSelector, double minSimilarity, int? prefixLength)
{
    return new Search&amp;lt;TSource, QueryStringQuery&amp;gt;(
        search,
        (ISearchContext context) =&amp;gt;
        {
            if (minSimilarity &amp;lt;= 0 || minSimilarity &amp;gt;= 1)
            {
                return;
            }

            var boolQuery = new BoolQuery();

            boolQuery.MinimumNumberShouldMatch = 1;

            boolQuery.Should.Add(context.RequestBody.Query);

            var fieldNameForSearch = search.Client.Conventions.FieldNameConvention
    .GetFieldNameForAnalyzed(fieldSelector);

            var fuzzyQuery = new FuzzyQuery(fieldNameForSearch, query)
            {
                MinSimilarity = minSimilarity,
                PrefixLength = prefixLength,
                Boost = 0.5,
            };

            boolQuery.Should.Add(fuzzyQuery);

            context.RequestBody.Query = boolQuery;
        });
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;MinSimilarity&lt;/span&gt;&lt;/em&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;: It means the minimum similarity is acceptable to fit the result&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;PrefixLength&lt;/span&gt;&lt;/em&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;: Number of beginning characters left unchanged for fuzzy matching. Defaults to&amp;nbsp;0&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Boost&lt;/span&gt;&lt;/em&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;: Factor for relevance score of fuzzy query. The default is 1. But we can change it based on our purpose to make sure the default result order based on the score as your expectation.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Second, you can use this method for your search object as follows:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;query = query.FuzzySearch(searchTerm, x =&amp;gt; x.Name, 0.5, 0);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;The below image&lt;/span&gt; is the result when I find products via fuzzy search - Similarity is 0.5 - In Name field:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/76b46e1d5b0e4e11990814555958113d.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;&lt;strong&gt;Note&lt;/strong&gt;: I tried to apply fuzzy search by this guide &lt;/span&gt;&lt;a href=&quot;https://docs.developers.optimizely.com/digital-experience-platform/v1.1.0-search-and-navigation/docs/fuzzy-search&quot;&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;https://docs.developers.optimizely.com/digital-experience-platform/v1.1.0-search-and-navigation/docs/fuzzy-search&lt;/span&gt;&lt;/a&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt; but it does not work with me. I will spend time to dig deeply to find a reason later.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-size: 18pt;&quot;&gt;&lt;strong&gt;How to apply multiple search queries with AND/OR operator&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;If you have more than 1 query and need to define AND/OR operator for them then you can do by using BoolQuery.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Here is the way to use BoolQuery for OR:&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;var boolQuery = new BoolQuery();

boolQuery.MinimumNumberShouldMatch = 1;

boolQuery.Should.Add(subQuery);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Here is the way to use BoolQuery for AND:&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;var boolQuery = new BoolQuery();

boolQuery.Must.Add(subQuery);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or AND for negative queries&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;var boolQuery = new BoolQuery();

boolQuery.MustNot.Add(subQuery);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span style=&quot;font-size: 18pt;&quot;&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;In my opinion, using Search &amp;amp; Navigation is still a good choice when you need a search solution for your application. It uses Elasticsearch behind the scenes with a lot of available support for search, filtering, paging, faceting, and statistics, among other features. I hope that the search tips above can help you when applying Search &amp;amp; Navigation to your project.&lt;/span&gt;&lt;/p&gt;</id><updated>2024-05-22T03:59:03.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Optimizely Forms - How to add extra data automatically into submission</title><link href="https://world.optimizely.com/blogs/binh-nguyen/dates/2024/3/optimizely-form---add-more-fields-into-submission-data-automatically/" /><id>&lt;p&gt;&lt;strong&gt;Some words about Optimizely Forms&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Optimizely Forms is a built-in add-on by Optimizely development team that enables to create forms dynamically via block-based approach. Developers could install Optimizely Forms to any Optimizely CMS or Customized Commerce site via installing nuget package.&lt;/p&gt;
&lt;p&gt;It allows content editors to create forms dynamically as a normal block, dragging and dropping available form elements such as text box, text area, date time, file upload, image...etc into form container block. So editors can design easily any form with needed controls and re-use it at any pages for vary contexts such as user survey, user registration, user feedback, user support.&lt;/p&gt;
&lt;p&gt;Once users submit form then data will be saved into database permanently by default. Moreover, we also can send submission data to third parties via connectors for auto-marketing purpose.&lt;/p&gt;
&lt;p&gt;Here is illustrated image about creating form in Edit User Interface&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/aa60c69aa37b46d2b4d4a8afbf9a2f8f.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;After creating form then placing form in pages then users can see it when viewing page like this below&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/1ad7ee167fcf4ef48f5df54c65e912ca.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How Optimizely Forms works&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The below image illustrates about how to apply Optimizely Forms and how it works&lt;/p&gt;
&lt;p&gt;&lt;strong&gt; &lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&amp;nbsp;&lt;img src=&quot;/link/e4783c35984a41b881f65fbdb61f26b0.aspx&quot; /&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;So as a developer, I realized that we can do a lot of customization around Optimizely Forms such as creating new form elements, adding more extra post submission actors, implementing custom connector.&lt;/p&gt;
&lt;p&gt;Recently, I got a requirement that the client want to have some extra data in all submission data such as page url, page category. These extra fields must be stored in database, showed in form submissions view and exporting files as well.&lt;/p&gt;
&lt;p&gt;This below image shows how to submission data is displayed in backend user interface.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/6d6ee420f4ed4343a3d6aa2077e34a98.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt; &lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;You can see data in blue border is filled data by users, data in orange border is system data &amp;ndash; they are not data filled by user. Is it possible to add more data as same as default system data? Yes. We can do customization for it and here is sample code that I want to share&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How I do to add extra data to submission&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;First, we need a new post submission actor to add extra data to submission data. This actor should be run first by setting order to make sure that extra data is always added to submission data before all other actors.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public class MoreExtraInformationPostSubmissionActor : PostSubmissionActorBase, ISyncOrderedSubmissionActor
 {
     private readonly IUrlResolver _urlResolver;

     public MoreExtraInformationPostSubmissionActor(IUrlResolver urlResolver)
     {
         _urlResolver = urlResolver;
     }

     public int Order =&amp;gt; 1;

     public override object Run(object input)
     {
         var hostedPageId = SubmissionData.Data[&quot;SYSTEMCOLUMN_HostedPage&quot;] as string;

         if (!string.IsNullOrEmpty(hostedPageId))
         {
             var hostedPageUrl = _urlResolver.GetUrl(new ContentReference(hostedPageId));
             SubmissionData.Data.Add(&quot;PageUrl&quot;, hostedPageUrl);
         }
         return new SubmissionActorResult();
     }
 }&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;In order to display extra data in view and export file here is the thing that we need to add.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public class CustomFormRepository : FormRepository
{
    public override IEnumerable&amp;lt;FriendlyNameInfo&amp;gt; GetFriendlyNameInfos(FormIdentity formIden, params Type[] excludedElementBlockTypes)
    {
        var friendlyNameInfos = new List&amp;lt;FriendlyNameInfo&amp;gt;(base.GetFriendlyNameInfos(formIden, excludedElementBlockTypes));
        friendlyNameInfos.Add(new FriendlyNameInfo() { FormatType = FormatType.String, FriendlyName = &quot;Page Url&quot;, ElementId = &quot;PageUrl&quot; });
        return friendlyNameInfos;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Finally, need to override default FormRepository by new one by adding this below code in Startup.cs&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;  services.AddSingleton&amp;lt;IFormRepository, CustomFormRepository&amp;gt;();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;The result after applying all above code changes&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;In Form submissions UI&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/link/bd9ca8388e104ad5a00eee57f50fc559.aspx&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;In exported file&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;img src=&quot;/link/e21224a4289946cda0d695148eb3e8a9.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;That is all thing that I want to share in this article. I hope that it is helpful for someones. Happy coding!&lt;/p&gt;</id><updated>2024-04-29T03:49:42.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>How to add more Content Area Context Menu Item in Optimizely CMS 12</title><link href="https://world.optimizely.com/blogs/binh-nguyen/dates/2024/2/how-to-add-a-new-menu-item-for-content-area-context-menu/" /><id>&lt;p&gt;Hey folks, today I will share something related to Context Menu customization in the Content Area of Optimizely CMS.&lt;/p&gt;
&lt;p&gt;As you know, the content area is a crucial property in Optimizely CMS. It enables editors to place blocks onto a page, allowing for flexible rendering based on the chosen blocks.&lt;/p&gt;
&lt;p&gt;You can see the Content Area Context Menu when editing a page in on-page editing mode or in properties editing mode.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/3676d5be4bc94caa813225a85cfff68d.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;With the Content Area Context Menu, you can see the following available actions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Edit: Navigate to the selected block for editing in the current view.&lt;/li&gt;
&lt;li&gt;Quick Edit: Allows editing of the block within a modal popup in the current view.&lt;/li&gt;
&lt;li&gt;Personalize: Enables customization of block visibility, specifying who can view the block&#39;s content.&lt;/li&gt;
&lt;li&gt;Move Outside Group: Moves the block out from a personalized group.&lt;/li&gt;
&lt;li&gt;Display Option: This action is displayed if configurations for display tags are available.&lt;/li&gt;
&lt;li&gt;Move Up: Shifts the block upwards.&lt;/li&gt;
&lt;li&gt;Move Down: Shifts the block downwards.&lt;/li&gt;
&lt;li&gt;Remove: Deletes a block from the Content Area.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;span style=&quot;font-size: 14pt;&quot;&gt;So question:&lt;strong&gt;&lt;span style=&quot;font-size: 14pt;&quot;&gt; &lt;/span&gt;Is it possible if you want to add more customized action for block in Content Area as known as menu item in context menu?&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-size: 14pt;&quot;&gt;My answer is:&lt;strong&gt;&lt;span style=&quot;font-size: 14pt;&quot;&gt; &lt;/span&gt;Yes. We could&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;Today, I will show you my way to do it based on my experience in Dojo framework. It may be not the best solution but it works with me.&lt;/p&gt;
&lt;p&gt;Here are all steps that you can do to add more menu item to Content Area Context Menu.&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-size: 18pt;&quot;&gt;&lt;strong&gt;1. &lt;/strong&gt;&lt;/span&gt;First step, you need to do is creating a content area command because each menu item in the context menu is currently matched to a content area command.&lt;/p&gt;
&lt;p&gt;Here is the code example for creating a content area command:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;define(&quot;alloy/contentediting/command/CustomOption&quot;, [
    // General application modules
    &quot;dojo/_base/declare&quot;,
    &quot;dojo/_base/lang&quot;,
    &quot;dojo/when&quot;,

    &quot;epi/dependency&quot;,

    &quot;epi-cms/contentediting/command/_ContentAreaCommand&quot;,
    &quot;epi-cms/contentediting/viewmodel/ContentBlockViewModel&quot;
], function (declare, lang, when, dependency, _ContentAreaCommand, ContentBlockViewModel) {

    return declare([_ContentAreaCommand], {        
        // label: [public] String
        //      The action text of the command to be used in visual elements.
        label: &quot;Custom action&quot;,
        // iconClass: [readonly] String
        //      The icon class of the command to be used in visual elements.
        iconClass: &quot;epi-iconStar&quot;,

        constructor: function () {
        },       
        _execute: function () {            
            //Add your logic here when clicking on this action
        },
        _onModelValueChange: function () {
            // summary:
            //      Updates canExecute after the model value has changed.
            // tags:
            //      protected
            var item = this.model;
           
            this.set(&quot;isAvailable&quot;, true);

            this.set(&quot;canExecute&quot;, false);

            if (item &amp;amp;&amp;amp; item.contentLink) {
                this.set(&quot;canExecute&quot;, true);
                return;
            }           
        }

    });
});&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span style=&quot;font-size: 14pt;&quot;&gt;&lt;strong&gt;2. &lt;/strong&gt;&lt;/span&gt;Next step is creating new dojo component one for ContentAreaCommands. This component is used to declare context menu in on-page editting mode.&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;define(&quot;epi-cms/contentediting/command/ContentAreaCommands&quot;, [
    &quot;dojo/_base/array&quot;,
    &quot;dojo/_base/declare&quot;,
    &quot;dojo/Stateful&quot;,
    &quot;dojo/when&quot;,
    &quot;dijit/Destroyable&quot;,
    &quot;epi-cms/ApplicationSettings&quot;,
    &quot;epi-cms/contentediting/command/BlockRemove&quot;,
    &quot;epi-cms/contentediting/command/BlockEdit&quot;,
    &quot;epi-cms/contentediting/command/ContentAreaItemBlockEdit&quot;,
    &quot;epi-cms/contentediting/command/BlockInlineEdit&quot;,
    &quot;epi-cms/contentediting/command/MoveVisibleToPrevious&quot;,
    &quot;epi-cms/contentediting/command/MoveVisibleToNext&quot;,
    &quot;epi-cms/contentediting/command/Personalize&quot;,
    &quot;epi-cms/contentediting/command/SelectDisplayOption&quot;,
    &quot;epi-cms/contentediting/command/MoveOutsideGroup&quot;,
    &quot;alloy/contentediting/command/CustomOption&quot;
], function (array, declare, Stateful, when, Destroyable, ApplicationSettings, Remove, Edit, ContentAreaItemBlockEdit, InlineEdit, MoveVisibleToPrevious, MoveVisibleToNext, Personalize, SelectDisplayOption, MoveOutsideGroup, CustomOption) {

    return declare([Stateful, Destroyable], {
        // tags:
        //      internal

        commands: null,

        constructor: function () {
            this._commandSpliter = this._commandSpliter || new Stateful({
                category: &quot;menuWithSeparator&quot;
            });
            this.contentAreaItemBlockEdit = new ContentAreaItemBlockEdit({ category: null });
            this.blockInlineEdit = new InlineEdit();
            this.moveVisibleToPrevious = new MoveVisibleToPrevious();
            this.moveVisibleToNext = new MoveVisibleToNext();
            this.customOption = new CustomOption();
            this.commands = [
                new Edit({ category: null }),
                this.contentAreaItemBlockEdit,
                this.blockInlineEdit,
                this.customOption,
                this._commandSpliter,
                new SelectDisplayOption(),
                this.moveVisibleToPrevious,
                this.moveVisibleToNext,
                new Remove()
            ];
            var sectionsVisibility = Object.assign({}, ApplicationSettings.sectionsVisibility);
            // Only add personalize command if the ui is not limited
            if (!ApplicationSettings.limitUI &amp;amp;&amp;amp; (sectionsVisibility.visitorGroups !== false)) {
                this.moveOutsideGroup = new MoveOutsideGroup();
                this.personalize = new Personalize({ category: null });
                this.commands.splice(5, 0, this.personalize, this.moveOutsideGroup);
            }

            this.commands.forEach(function (command) {
                this.own(command);
            }, this);
        },

        handleDoubleClick: function (itemModel) {
            if (itemModel.inlineBlockData) {
                when(this.contentAreaItemBlockEdit.updateModel(itemModel)).then(function () {
                    this.contentAreaItemBlockEdit.execute();
                }.bind(this));
            } else {
                when(this.blockInlineEdit.updateModel(itemModel)).then(function () {
                    this.blockInlineEdit.execute();
                }.bind(this));
            }
        },

        _modelSetter: function (model) {
            this.model = model;

            array.forEach(this.commands, function (command) {
                command.set(&quot;model&quot;, model);
            });
        }
    });
});&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span style=&quot;font-size: 14pt;&quot;&gt;&lt;strong&gt;3. &lt;/strong&gt;&lt;/span&gt;Next step is creating new dojo component one for ContentAreaEditor. This component is used to declare the context menu in properties editting mode&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;require({
    cache: {
        &#39;url:epi-cms/contentediting/editors/templates/ContentAreaEditor.html&#39;: &quot;&amp;lt;div class=\&quot;dijitInline\&quot; tabindex=\&quot;-1\&quot; role=\&quot;presentation\&quot;&amp;gt;\r\n    &amp;lt;div class=\&quot;epi-content-area-header-block\&quot;&amp;gt;\r\n        &amp;lt;div data-dojo-type=\&quot;epi-cms/contentediting/AllowedTypesList\&quot;\r\n            data-dojo-props=\&quot;allowedTypes: this.allowedTypes, restrictedTypes: this.restrictedTypes\&quot;\r\n            data-dojo-attach-point=\&quot;allowedTypesHeader\&quot;&amp;gt;&amp;lt;/div&amp;gt;\r\n    &amp;lt;/div&amp;gt;\r\n    &amp;lt;div class=\&quot;epi-content-area-editor--wide epi-content-area-editor\&quot;&amp;gt;\r\n        &amp;lt;div data-dojo-attach-point=\&quot;treeNode\&quot;&amp;gt;&amp;lt;/div&amp;gt;\r\n        &amp;lt;div data-dojo-attach-point=\&quot;actionsContainer\&quot; class=\&quot;epi-content-area-actionscontainer\&quot;&amp;gt;&amp;lt;/div&amp;gt;\r\n    &amp;lt;/div&amp;gt;\r\n&amp;lt;/div&amp;gt;\r\n&quot;
    }
});
define(&quot;epi-cms/contentediting/editors/ContentAreaEditor&quot;, [
    // Dojo
    &quot;dojo/_base/declare&quot;,
    &quot;dojo/aspect&quot;,
    &quot;dojo/dom-class&quot;,
    &quot;dojo/dom-style&quot;,
    &quot;dojo/on&quot;,
    &quot;dojo/topic&quot;,
    &quot;dojo/when&quot;,
    &quot;dojo/Stateful&quot;,

    //Dijit
    &quot;dijit/registry&quot;,
    &quot;dijit/_WidgetBase&quot;,
    &quot;dijit/_TemplatedMixin&quot;,
    &quot;dijit/_CssStateMixin&quot;,
    &quot;dijit/_WidgetsInTemplateMixin&quot;,

    // EPi Framework
    &quot;epi/dependency&quot;,
    &quot;epi/shell/dnd/Target&quot;,
    &quot;epi/shell/command/_CommandProviderMixin&quot;,
    &quot;epi/shell/command/_Command&quot;,
    &quot;epi/shell/applicationSettings&quot;,

    //EPi CMS
    &quot;epi-cms/contentediting/editors/_ContentAreaTree&quot;,
    &quot;epi-cms/contentediting/editors/_ContentAreaTreeModel&quot;,
    &quot;epi-cms/contentediting/viewmodel/PersonalizedGroupViewModel&quot;,
    &quot;epi-cms/_ContentContextMixin&quot;,
    &quot;epi-cms/ApplicationSettings&quot;,
    &quot;epi-cms/contentediting/viewmodel/ContentAreaViewModel&quot;,
    &quot;epi-cms/core/ContentReference&quot;,
    &quot;epi/shell/widget/ContextMenu&quot;,
    &quot;epi/shell/widget/_ValueRequiredMixin&quot;,
    &quot;epi-cms/widget/overlay/Block&quot;,
    &quot;epi-cms/widget/command/CreateContentFromContentArea&quot;,
    &quot;epi-cms/widget/command/CreateContentFromSelector&quot;,

    &quot;epi-cms/widget/_HasChildDialogMixin&quot;,

    &quot;epi-cms/contentediting/command/BlockRemove&quot;,
    &quot;epi-cms/contentediting/command/BlockConvert&quot;,
    &quot;epi-cms/contentediting/command/BlockEdit&quot;,
    &quot;epi-cms/contentediting/command/ContentAreaItemBlockEdit&quot;,
    &quot;epi-cms/contentediting/command/BlockInlineEdit&quot;,
    &quot;epi-cms/contentediting/command/MoveToPrevious&quot;,
    &quot;epi-cms/contentediting/command/MoveToNext&quot;,
    &quot;epi-cms/contentediting/command/MoveOutsideGroup&quot;,
    &quot;epi-cms/contentediting/command/Personalize&quot;,
    &quot;epi-cms/contentediting/command/SelectDisplayOption&quot;,
    &quot;alloy/contentediting/command/CustomOption&quot;,
    &quot;epi-cms/contentediting/AllowedTypesList&quot;,
    &quot;epi-cms/contentediting/editors/_TextWithActionsMixin&quot;,

    // Resources
    &quot;dojo/text!epi-cms/contentediting/editors/templates/ContentAreaEditor.html&quot;,
    &quot;epi/i18n!epi/cms/nls/episerver.cms.contentediting.editors.contentarea&quot;,
    &quot;epi/i18n!epi/cms/nls/episerver.cms.widget.overlay.blockarea&quot;
], function (

    // Dojo
    declare,
    aspect,
    domClass,
    domStyle,
    on,
    topic,
    when,
    Stateful,

    // Dijit
    registry,
    _WidgetBase,
    _TemplatedMixin,
    _CssStateMixin,
    _WidgetsInTemplateMixin,

    // EPi Framework
    dependency,
    Target,
    _CommandProviderMixin,
    _Command,
    shellApplicationSettings,

    // CMS
    _ContentAreaTree,
    _ContentAreaTreeModel,
    PersonalizedGroupViewModel,

    _ContentContextMixin,
    ApplicationSettings,
    ContentAreaViewModel,

    ContentReference,
    ContextMenu,
    _ValueRequiredMixin,
    BlockOverlay,
    CreateContentFromContentArea,
    CreateContentFromSelector,

    _HasChildDialogMixin,

    RemoveCommand,
    BlockConvertCommand,
    EditCommand,
    ContentAreaItemBlockEdit,
    BlockInlineEdit,
    MoveToPrevious,
    MoveToNext,
    MoveOutsideGroup,
    Personalize,
    SelectDisplayOption,
    CustomOption,
    AllowedTypesList, // used in template
    _TextWithActionsMixin,

    // Resources
    template,
    resources,
    blockAreaRes
) {

    return declare([
        _WidgetBase,
        _TemplatedMixin,
        _WidgetsInTemplateMixin,
        _CssStateMixin,
        _ValueRequiredMixin,
        _ContentContextMixin,
        _CommandProviderMixin,
        _HasChildDialogMixin,
        _TextWithActionsMixin
    ], {
        // summary:
        //      Editor for ContentArea to be able to edit my content area property in forms mode
        //      This should be a simple, non WYSIWYG listing of the inner content blocks with possibilities to add, remove and rearrange the content.
        //
        // tags:
        //      internal

        // baseClass: [public] String
        //    The widget&#39;s base CSS class.
        baseClass: &quot;epi-content-area-wrapper&quot;,

        emptyClass: &quot;epi-content-area-wrapper--empty&quot;,

        // res: Json object
        //      Language resource
        res: resources,

        // templateString: String
        //      UI template for content area editor
        templateString: template,

        // value: String
        //      Value of the content area
        value: null,

        // multiple: Boolean
        //  Value must be true, otherwise dijit/Form will trea the value as an object instead of an array
        multiple: true,

        // parent: Object
        //      Editor wrapper object containe the editor
        parent: null,

        // overlayItem: Object
        //      Source overlay of the content area in on page edit mode
        overlayItem: null,

        // model: Object
        //      Content area editor view model
        model: null,

        // intermediateChanges: Boolean
        //      Inherited from editor interface
        intermediateChanges: true,

        // editMode: String
        //      Flags to detect page edit mode (On page edit or Form edit mode or Create content mode)
        editMode: &quot;onpageedit&quot;,

        // _preventOnBlur: [private] Boolean
        //      When set, the onBlur event is prevented.
        _preventOnBlur: false,

        _dndTarget: null,

        // allowedTypes: [public] Array
        //      The types which are allowed. i.e used for filtering based on AllowedTypesAttribute
        allowedTypes: null,

        // restrictedTypes: [public] Array
        //      The types which are restricted.
        restrictedTypes: null,

        // actionsResource: [Object]
        //      The resource of actions link
        actionsResource: blockAreaRes,

        // actionsResource: [Object]
        //      Name of constructor function of ContentAreaTree class
        treeClass: _ContentAreaTree,

        // allowMultipleItems: [Boolean]
        //      Allow dnd multiple items at once
        allowMultipleItems: true,

        constructor: function () {
            this.allowedDndTypes = [];
        },

        onChange: function (value) {
            // summary:
            //    Called when the value in the widget changes.
            // tags:
            //    public callback
        },

        onForceChange: function (value) {
            this.onChange(value);
        },

        _handleModelChange: function (value) {
            // summary:
            //    Called when the value in the model changes.
            // tags:
            //    public callback

            this.validate();
            this.onForceChange(value);

            this._toggleEmptyClass();
            if (this.model.selectedItem) {
                this.updateCommandModel(this.model.selectedItem);
            }

            this._toggleActionsContainer();
        },

        _toggleClass: function (node, className, condition) {
            (node || this.domNode).classList[condition ? &quot;add&quot; : &quot;remove&quot;](className);
        },

        _toggleEmptyClass: function () {
            this._toggleClass(this.domNode, this.emptyClass, !this.value || this.value.length &amp;lt;= 0);
        },

        _onBlur: function () {
            // summary:
            //      Override base to prevent the onBlur from being called when the _preventOnBlur flag is set.
            // tags:
            //      protected override

            if (this._preventOnBlur) {
                return;
            }
            this.inherited(arguments);
        },

        focus: function () {
            // summary:
            //    Focus the tree if there is a value, else focus the create block text.
            // tags:
            //    public
            if (this.tree &amp;amp;&amp;amp; this.model.get(&quot;value&quot;).length &amp;gt; 0) {
                this._focusManager.focus(this.tree.domNode);
            } else {
                if (this.textWithLinks) {
                    this.textWithLinks.focus();
                }
            }
        },

        postMixInProperties: function () {

            this.inherited(arguments);

            this._commandSpliter = this._commandSpliter || new Stateful({
                category: &quot;menuWithSeparator&quot;
            });

            // NOTE: Check for this._commands to allow for mocking the commands without breaking _CommandProviderMixin.
            this.contentAreaItemBlockEdit = new ContentAreaItemBlockEdit({ category: null, isContentAreaReadonly: this.get(&quot;readOnly&quot;) });
            this.blockInlineEdit = new BlockInlineEdit();
            this.movePrevious = new MoveToPrevious();
            this.moveNext = new MoveToNext();
            this.customOption = new CustomOption();

            this.commands = this._commands || [
                new EditCommand({ category: null }),
                this.contentAreaItemBlockEdit,
                this.blockInlineEdit,
                this.customOption,
                this._commandSpliter,
                new SelectDisplayOption(),
                this.movePrevious,
                this.moveNext,
                new RemoveCommand(),
                new BlockConvertCommand()
            ];

            this.own(on(this.contentAreaItemBlockEdit, &quot;save&quot;, function (inlineBlockData, name) {
                this._preventOnBlur = false;

                var value = [];
                (this.model.getChildren() || []).forEach(function (child) {
                    if (child instanceof PersonalizedGroupViewModel) {
                        (child.getChildren() || []).forEach(function (child) {
                            if (child.id === this.model.selectedItem.id) {
                                child.inlineBlockData = inlineBlockData;
                                child.name = name;
                            }
                            value.push(child);
                        }.bind(this));
                    } else {
                        if (child.id === this.model.selectedItem.id) {
                            child.inlineBlockData = inlineBlockData;
                            child.name = name;
                        }
                        value.push(child);
                    }
                }.bind(this));
                value = value.map(function (v) {
                    return v.serialize();
                });

                this.set(&quot;value&quot;, value);

                // In order to be able to add a block when creating it from a floating editor
                // we need to set the editing parameter on the editors parent wrapper to true
                // since it has been set to false while being suspended when switching to
                // the secondaryView.
                this.parent = this.parent || this.getParent();
                this.parent.set(&quot;editing&quot;, true);
                this.onForceChange(value);

                // Now call onBlur since it&#39;s been prevented using the _preventOnBlur flag.
                this.onBlur();
            }.bind(this)));

            // Only add personalize command if the ui is not limited
            if (this._isPersonalizationEnabled()) {
                this.commands.splice(5, 0,
                    new Personalize({ category: null }),
                    new MoveOutsideGroup()
                );
            }
            this.commands.forEach(function (command) {
                this.own(command);
            }, this);

            this.own(
                //Create the view model
                this.model = this.model || new ContentAreaViewModel({
                    maxLength: this.maxLength,
                    minLength: this.minLength
                }),
                this.treeModel = this.treeModel || new _ContentAreaTreeModel({ model: this.model }),
                this.model.watch(&quot;selectedItem&quot;, function (name, oldValue, newValue) {
                    //Update the commands with the selected block
                    this.updateCommandModel(newValue);
                }.bind(this)),
                on(this.model, &quot;changed&quot;, function () {
                    if (!this._started || this._supressValueChanged) {
                        return;
                    }

                    this._set(&quot;value&quot;, this.model.get(&quot;value&quot;));

                    //Call to the handle model change with the new value
                    this._handleModelChange(this.value);
                }.bind(this))
            );
            // personalizationarea isn&#39;t an actual type so it needs to be hardcoded like in _ContentAreaTree
            this.allowedDndTypes.push(&quot;personalizationarea&quot;);
            this.allowedDndTypes.push(this._getInlineBlockDndKey());

            if (!this.contentDataStore) {
                var registry = dependency.resolve(&quot;epi.storeregistry&quot;);
                this.contentDataStore = registry.get(&quot;epi.cms.contentdata&quot;);
            }
        },

        buildRendering: function () {
            this.inherited(arguments);

            this.contextMenu = new ContextMenu();
            this.contextMenu.addProvider(this);

            this.own(this.contextMenu);

            this._setupActions(this.actionsContainer);

            this.own(this._dndTarget = new Target(this.actionsContainer, {
                accept: this.allowedDndTypes,
                reject: this.restrictedDndTypes,
                isSource: false,
                alwaysCopy: false,
                allowMultipleItems: this.allowMultipleItems,
                insertNodes: function () { }
            }));

            this.own(aspect.after(this._dndTarget, &quot;onDropData&quot;, function (dndData, source, nodes, copy) {
                dndData.forEach(function (dndData) {
                    this.model.modify(function () {
                        this.model.addChild(dndData.data);
                    }.bind(this));
                }, this);

                if (!this.tree) {
                    this._createTree();
                }

            }.bind(this), true));

            // Handle focus after dropping on the tree or the drop area. We set focus to ourselves so that
            // it is not left where the drag originated.
            this.own(aspect.after(this._dndTarget, &quot;onDrop&quot;, this.focus.bind(this)));
        },

        postCreate: function () {
            this.inherited(arguments);

            this.set(&quot;emptyMessage&quot;, resources.emptymessage);
            this._toggleEmptyClass();

            if (this.parent &amp;amp;&amp;amp; this.overlayItem) {
                this.own(aspect.after(this.parent, &quot;onStartEdit&quot;, function () {
                    this._selectFromOverlay(this.overlayItem.model);
                }.bind(this)));
            }

            this.own(topic.subscribe(&quot;/dnd/start&quot;, this._startDrag.bind(this)));
        },

        startup: function () {

            if (this._started) {
                return;
            }

            this.inherited(arguments);

            this.tree &amp;amp;&amp;amp; this.tree.startup();
            this.contextMenu.startup();
            this._updateStyle();

            this.own(
                this.allowedTypesHeader.watch(&quot;hasRestriction&quot;, this._updateStyle.bind(this))
            );
        },

        destroy: function () {
            this.tree &amp;amp;&amp;amp; this.tree.destroyRecursive();

            this.inherited(arguments);
        },

        isCreateLinkVisible: function () {
            // summary:
            //      Overridden mixin class, depend on currentMode will show/not create link
            // tags:
            //      protected

            return this.model.canCreateBlock(this.allowedTypes, this.restrictedTypes);
        },

        onDialogExecute: function (selectedContent) {
            if (selectedContent) {
                this._saveValueAndFireOnChange({
                    contentLink: new ContentReference(selectedContent.contentLink).createVersionUnspecificReference().toString(),
                    name: selectedContent.name,
                    typeIdentifier: selectedContent.typeIdentifier
                });
            }
        },

        _saveValueAndFireOnChange: function (block) {
            this._preventOnBlur = false;
            var value = Object.assign([], this.model.get(&quot;value&quot;), true);
            value.push(block);
            this.set(&quot;value&quot;, value);

            // In order to be able to add a block when creating it from a floating editor
            // we need to set the editing parameter on the editors parent wrapper to true
            // since it has been set to false while being suspended when switching to
            // the secondaryView.
            this.parent = this.parent || this.getParent();
            this.parent.set(&quot;editing&quot;, true);
            this.validate();
            this.onForceChange(value);

            // Now call onBlur since it&#39;s been prevented using the _preventOnBlur flag.
            this.onBlur();
        },

        executeAction: function (actionName) {
            // summary:
            //      Overridden mixin class executing click actions from textWithLinks widget
            // actionName: [String]
            //      Action name of link on content area
            // tags:
            //      public

            if (actionName === &quot;createnewblock&quot;) {
                // HACK: Preventing the onBlur from being executed so the editor wrapper keeps this editor in editing state
                this._preventOnBlur = true;

                // since we&#39;re going to create a block, we need to hide all validation tooltips because onBlur is prevented here
                this.validate(false);

                var command = shellApplicationSettings.inlineBlocksInContentAreaEnabled ? new CreateContentFromContentArea({
                    allowedTypes: this.allowedTypes,
                    restrictedTypes: this.restrictedTypes
                }) : new CreateContentFromSelector({
                    creatingTypeIdentifier: &quot;episerver.core.blockdata&quot;,
                    createAsLocalAsset: true,
                    isInQuickEditMode: this.isInQuickEditMode,
                    quickEditBlockId: this.quickEditBlockId,
                    autoPublish: true,
                    allowedTypes: this.allowedTypes,
                    restrictedTypes: this.restrictedTypes
                });

                command.set(&quot;model&quot;, {
                    save: this._saveValueAndFireOnChange.bind(this),
                    cancel: function () {
                        this._preventOnBlur = false;
                        this.onBlur();
                    }.bind(this)
                });
                command.execute();
            }
        },

        isValid: function (isFocused) {
            // summary:
            //    Check if widget&#39;s value is valid.
            // isFocused:
            //    Indicate that the widget is being focused.
            // tags:
            //    protected

            // When create block screen is visible, we need to hide all validation messages since onBlur is prevented.
            return (this._preventOnBlur || !this.required || this.model.get(&quot;value&quot;).length &amp;gt; 0);
        },

        _setReadOnlyAttr: function (readOnly) {
            this._set(&quot;readOnly&quot;, readOnly);
            this._toggleActionsContainer();

            if (this._source) {
                this._source.isSource = !this.readOnly;
            }

            if (this.model) {
                this.model.set(&quot;readOnly&quot;, readOnly);
            }

            this.tree &amp;amp;&amp;amp; this.tree.set(&quot;readOnly&quot;, readOnly);
        },

        _toggleActionsContainer: function () {
            // summary:
            //    Hide actions when readonly or editor has reached the items limit
            // tags:
            //    private

            var visible = !this.model.hasReachedItemsLimit() &amp;amp;&amp;amp; !this.get(&quot;readOnly&quot;);
            domStyle.set(this.actionsContainer, &quot;display&quot;, visible ? &quot;&quot; : &quot;none&quot;);
        },

        _checkAcceptance: function (source, nodes) {
            // summary:
            //      Customize checkAcceptance func
            // source: Object
            //      The source which provides items
            // nodes: Array
            //      The list of transferred items

            return this.readOnly ? false : this._source.defaultCheckAcceptance(source, nodes) &amp;amp;&amp;amp; !this.model.hasReachedItemsLimit();
        },

        _createTree: function () {
            // summary:
            //    Creates the tree widget
            // tags:
            //    private

            //Create the tree
            this.tree = new this.treeClass({
                accept: this.allowedDndTypes,
                reject: this.restrictedDndTypes,
                contextMenu: this.contextMenu,
                model: this.treeModel,
                readOnly: this.readOnly,
                inlineBlockDndKey: this._getInlineBlockDndKey(),
                inlineBlockNameProperties: this.inlineBlockNameProperties
            }).placeAt(this.treeNode);

            this.tree.own(on(this.tree, &quot;dblclick&quot;, function (itemModel) {
                if (itemModel.inlineBlockData) {
                    when(this.contentAreaItemBlockEdit.updateModel(itemModel)).then(function () {
                        this.contentAreaItemBlockEdit.execute();
                    }.bind(this));
                } else {
                    when(this.blockInlineEdit.updateModel(itemModel)).then(function () {
                        this.blockInlineEdit.execute();
                    }.bind(this));
                }
            }.bind(this)));

            this.tree.own(
                aspect.after(this.tree.dndController, &quot;onDndEnd&quot;, this.focus.bind(this))
            );
        },

        _selectFromOverlay: function (overlayModel) {
            var child = overlayModel &amp;amp;&amp;amp; overlayModel.selectedItem &amp;amp;&amp;amp; overlayModel.selectedItem.serialize(),
                model = this.model,
                path = [&quot;root&quot;];

            // exit if there is no overlay model to select item
            if (!child) {
                return;
            }

            if (child.contentGroup) {
                model = model.getChild({ name: child.contentGroup });
                path.push(model.id);
            }
            model = model.getChild(child);
            if (!model) {
                return;
            }

            path.push(model.id);

            model.set(&quot;selected&quot;, true);
            model.set(&quot;ensurePersonalization&quot;, overlayModel.selectedItem.ensurePersonalization);

            // TODO: move this selection into tree instead
            this.tree &amp;amp;&amp;amp; this.tree.set(&quot;path&quot;, path);

        },

        _startDrag: function (source, nodes, copy) {
            var accepted = this._dndTarget.accept &amp;amp;&amp;amp; this._dndTarget.checkAcceptance(source, nodes);
            domClass.toggle(this.domNode, &quot;dojoDndTargetDisabled&quot;, !accepted);

            // close the editor when user start draging Block from BlockArea
            //TODO: This widget should not call a method on the parent
            var widget = registry.getEnclosingWidget(nodes[0]);
            if (widget &amp;amp;&amp;amp; widget.isInstanceOf(BlockOverlay) &amp;amp;&amp;amp; this.parent &amp;amp;&amp;amp; this.parent.cancel) {
                // We set isModified to false (default value) because always synchronize value
                // between OPE and Editor
                this.parent.set(&quot;isModified&quot;, false);
                this.parent.cancel();
            }
        },

        _ensureNonBrokenContentAreaItems: function (value) {
            var itemsList = value || [];
            itemsList.forEach(function (item) {
                if (!item || (!item.contentLink &amp;amp;&amp;amp; !item.inlineBlockData)) {
                    item.isBrokenLink = true;
                    item.name = resources.brokenlink;
                }
            });
            return itemsList;
        },

        _setValueAttr: function (value) {
            value = this._ensureNonBrokenContentAreaItems(value);
            // Destroy the tree since that is the fastest way to remove all items
            this.tree &amp;amp;&amp;amp; this.tree.destroyRecursive();

            this._set(&quot;value&quot;, value);

            this._supressValueChanged = true;
            this.model.set(&quot;value&quot;, value);
            this._supressValueChanged = false;

            // Create the tree again after the value has been set so
            // all tree nodes are created in one go
            this._createTree();

            this._toggleEmptyClass();
            this._toggleActionsContainer();
        },

        _updateStyle: function () {
            // summary:
            //      Handle the widget style depending on the allowedTypesList visibility

            if (!this.domNode) {
                return;
            }

            if (this.allowedTypesHeader.get(&quot;hasRestriction&quot;)) {
                domClass.remove(this.domNode, &quot;allowed-types-list-hidden&quot;);
            } else {
                domClass.add(this.domNode, &quot;allowed-types-list-hidden&quot;);
            }
        },

        _isPersonalizationEnabled: function () {
            var sectionsVisibility = Object.assign({}, ApplicationSettings.sectionsVisibility);
            if (sectionsVisibility.visitorGroups === false) {
                return false;
            }
            return !ApplicationSettings.limitUI;
        },

        _getInlineBlockDndKey: function () {
            return this.name + &quot;_contentarea-inline-block&quot;;
        }
    });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span style=&quot;font-size: 14pt;&quot;&gt;&lt;strong&gt;4. &lt;/strong&gt;&lt;/span&gt;Last one, add overrided dojo components into module config as epi base resource to load customized resources once loading epi base resource&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;false&quot; clientResourceRelativePath=&quot;&quot; viewEngine=&quot;Razor&quot;  moduleJsonSerializerType=&quot;None&quot; preferredUiJsonSerializerType=&quot;Net&quot;&amp;gt;	
	&amp;lt;dojo&amp;gt;
		&amp;lt;paths&amp;gt;
			&amp;lt;add name=&quot;alloy&quot; path=&quot;ClientResources/Scripts&quot; /&amp;gt;
		&amp;lt;/paths&amp;gt;
	&amp;lt;/dojo&amp;gt;
	&amp;lt;clientResources&amp;gt;		
		&amp;lt;add name=&quot;epi-cms.widgets.base&quot; path=&quot;ClientResources/Scripts/contentediting/command/ContentAreaCommands.js&quot; resourceType=&quot;Script&quot; /&amp;gt;
		&amp;lt;add name=&quot;epi-cms.widgets.base&quot; path=&quot;ClientResources/Scripts/editors/ContentAreaEditor.js&quot; resourceType=&quot;Script&quot; /&amp;gt;
	&amp;lt;/clientResources&amp;gt;
&amp;lt;/module&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I hope this article will help some of you somehow. Enjoy reading!&lt;/p&gt;</id><updated>2024-02-25T09:37:28.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>How to apply output cache to Optimizely CMS 12</title><link href="https://world.optimizely.com/blogs/binh-nguyen/dates/2023/3/how-to-apply-output-cache-to-optimizely-cms-12/" /><id>&lt;p&gt;&lt;strong&gt;Introduce&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Optimizely CMS 12 is a platform for content management systems. It is written by .NET 6.0. Actually, the output caching concept has not been yet made it in .NET 6.0 but there is a response caching concept in .NET6.0.&lt;/p&gt;
&lt;p&gt;You can use response cache to cache output for MVC controllers, MVC action methods, RAZOR pages. Response caching reduces the amount of work the web server performs to generate a response by returning result immediately from cache if it exists instead of running methods again and again. By this way, the performance is improved and server resources are optimized.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step to apply response cache in Optimizely CMS 12&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Step 1: Add [ResponseCache] attribute to the controller/action/razor page that you want:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;    public class StartPageController : PageControllerBase&amp;lt;StartPage&amp;gt;
    {
        [ResponseCache(Duration =30, Location = ResponseCacheLocation.Any)]
        public IActionResult Index(StartPage currentPage)
        {
            var model = PageViewModel.Create(currentPage);

            // Check if it is the StartPage or just a page of the StartPage type.
            if (SiteDefinition.Current.StartPage.CompareToIgnoreWorkID(currentPage.ContentLink))
            {
                // Connect the view models logotype property to the start page&#39;s to make it editable
                var editHints = ViewData.GetEditHints&amp;lt;PageViewModel&amp;lt;StartPage&amp;gt;, StartPage&amp;gt;();
               editHints.AddConnection(m =&amp;gt; m.Layout.Logotype, p =&amp;gt; p.SiteLogotype);
               editHints.AddConnection(m =&amp;gt; m.Layout.ProductPages, p =&amp;gt; p.ProductPageLinks);
               editHints.AddConnection(m =&amp;gt; m.Layout.CompanyInformationPages, p =&amp;gt; p.CompanyInformationPageLinks);
               editHints.AddConnection(m =&amp;gt; m.Layout.NewsPages, p =&amp;gt; p.NewsPageLinks);
               editHints.AddConnection(m =&amp;gt; m.Layout.CustomerZonePages, p =&amp;gt; p.CustomerZonePageLinks);
           }

           return View(model);
       }
   }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Duration=30&lt;/strong&gt; will cache the page for 30 seconds&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Location=ResponseCacheLocation.Any&lt;/strong&gt; will cache the page in both proxies and client.&lt;/p&gt;
&lt;p&gt;If you do not apply response cache for certain situation then you can use &lt;strong&gt;Location= ResponseCacheLocation.None&lt;/strong&gt; and &lt;strong&gt;NoStore=true&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Step 2: Add Response Cache Middleware services to service collection with AddResponseCaching extension method:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;    public void ConfigureServices(IServiceCollection services)
    {
        if (_webHostingEnvironment.IsDevelopment())
        {
            AppDomain.CurrentDomain.SetData(&quot;DataDirectory&quot;, Path.Combine(_webHostingEnvironment.ContentRootPath, &quot;App_Data&quot;));

            services.Configure&amp;lt;SchedulerOptions&amp;gt;(options =&amp;gt; options.Enabled = false);
        }

        services
            .AddCmsAspNetIdentity&amp;lt;ApplicationUser&amp;gt;()
            .AddCms()
            .AddCmsTagHelpers()
            .AddAlloy()
            .AddAdminUserRegistration()
            .AddEmbeddedLocalization&amp;lt;Startup&amp;gt;();

        // Required by Wangkanai.Detection
        services.AddDetection();

        services.AddSession(options =&amp;gt;
        {
            options.IdleTimeout = TimeSpan.FromSeconds(10);
            options.Cookie.HttpOnly = true;
            options.Cookie.IsEssential = true;
        });
        services.AddControllers();
        services.AddResponseCaching();
    }&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Step 3: Configure the app to use the middleware with the&amp;nbsp;UseResponseCaching&amp;nbsp;extension method:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        // Required by Wangkanai.Detection
        app.UseDetection();
        app.UseSession();

        app.UseResponseCaching();

        app.UseStaticFiles();
        app.UseRouting();
        app.UseAuthentication();
        app.UseAuthorization();

        app.UseEndpoints(endpoints =&amp;gt;
        {
            endpoints.MapContent();
            endpoints.MapControllers();
        });
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Please note that if you load a page by pressing F5 key in the browser then the cache for this page is refreshed by adding Cache-Control header to request is &amp;ldquo;max-age=0&amp;rdquo;. In order to prevent that, you can add the following middleware before response cache middleware:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;   app.Use(async (context, next) =&amp;gt;
   {
        const string cc = &quot;Cache-Control&quot;;

        if (context.Request.Headers.ContainsKey(cc) &amp;amp;&amp;amp; string.Equals(context.Request.Headers[cc], &quot;max-age=0&quot;, StringComparison.InvariantCultureIgnoreCase))
        {
              context.Request.Headers.Remove(cc);
        }
        await next();
   });&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;How to invalidate cache when the content is changed&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Actually, the response cache middleware uses MemoryResponseCache by default and this implementation does not support clearing cache. So you can do quickly and dirty to get a new cache by using the content cache version as a query key to vary cache. The content cache version is increased once any content is changed.&lt;/p&gt;
&lt;p&gt;Here are the steps that you can take to do that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Add ContentCacheVersion to vary by query keys attribute&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;   [ResponseCache(Duration =30, Location = ResponseCacheLocation.Any, VaryByQueryKeys = new string[] { &quot;ContentCacheVersion&quot;})]
   public IActionResult Index(StartPage currentPage)
   {
       var model = PageViewModel.Create(currentPage);
       // Check if it is the StartPage or just a page of the StartPage type.
       if (SiteDefinition.Current.StartPage.CompareToIgnoreWorkID(currentPage.ContentLink))
       {&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Add a middleware before Response Caching Middleware to add content cache version to the query string and add a middleware after Response Caching Middleware to remove this from the query string.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;   app.Use(async (context, next) =&amp;gt;
        {
            var contentCacheVersion = ServiceLocator.Current.GetInstance&amp;lt;IContentCacheVersion&amp;gt;();

            context.Request.QueryString = context.Request.QueryString.Add(&quot;ContentCacheVersion&quot;, contentCacheVersion.Version.ToString());
           
            await next();
        });

        app.UseResponseCaching();

        app.Use(async (context, next) =&amp;gt;
        {
            var nameValueCollection = System.Web.HttpUtility.ParseQueryString(context.Request.QueryString.ToString());
            nameValueCollection.Remove(&quot;ContentCacheVersion&quot;);
            context.Request.QueryString = new QueryString($&quot;?{nameValueCollection}&quot;);
            
            await next();
        });&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;How to vary response cache by visitor group&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;It is the same as in the case of changed content, you can do quickly and dirty to add visitor group as a vary by query key like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Add VisitorGroup to vary by query keys attribute&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;    [ResponseCache(Duration =30, Location = ResponseCacheLocation.Any, VaryByQueryKeys = new string[] { &quot;VisitorGroup&quot;})]
    public IActionResult Index(StartPage currentPage)
    {
        var model = PageViewModel.Create(currentPage);

        // Check if it is the StartPage or just a page of the StartPage type.
        if (SiteDefinition.Current.StartPage.CompareToIgnoreWorkID(currentPage.ContentLink))
        {&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Add a middleware before Response Caching Middleware to add the current visitor group to the query string and add a middleware after Response Caching Middleware to remove it from the query string.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;   app.Use(async (context, next) =&amp;gt;
        {
            context.Request.QueryString = context.Request.QueryString.Add(&quot;VisitorGroup&quot;, GetCurrentVisitorGroups(context));
           
            await next();
        });
        app.UseResponseCaching();
        app.Use(async (context, next) =&amp;gt;
        {
            var nameValueCollection = System.Web.HttpUtility.ParseQueryString(context.Request.QueryString.ToString());
            nameValueCollection.Remove(&quot;VisitorGroup&quot;);
            context.Request.QueryString = new QueryString($&quot;?{nameValueCollection}&quot;);
            await next();
        });&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Add GetCurrentVisitorGroups method to get visitor groups based on current context&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;    private string GetCurrentVisitorGroups(HttpContext context)
    {
        var  principalAccessor = ServiceLocator.Current.GetInstance&amp;lt;IPrincipalAccessor&amp;gt;();
        var  visitorGroupRepository = ServiceLocator.Current.GetInstance&amp;lt;IVisitorGroupRepository&amp;gt;();
        var  visitorGroupRoleRepository = ServiceLocator.Current.GetInstance&amp;lt;IVisitorGroupRoleRepository&amp;gt;();

        var roleNames = visitorGroupRepository.List().Select(x =&amp;gt; x.Name);

        var currentGroups = new List&amp;lt;string&amp;gt;();
        foreach (var roleName in roleNames)
        {
            if (visitorGroupRoleRepository.TryGetRole(roleName, out var role))
            {
                if (role.IsMatch(principalAccessor.Principal, context))
                {
                    currentGroups.Add(roleName);
                }
            }
        }
        return string.Join(&quot;|&quot;, currentGroups);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is it. Using this approach, you can apply built-in response cache quickly into Optimizely CMS 12 without customizing too much or using third-party packages. You can see another topic about using third-party output caching package here &lt;a href=&quot;https://www.gulla.net/en/blog/quick-and-dirty-output-cache-in-optimizely-cms12/&quot;&gt;https://www.gulla.net/en/blog/quick-and-dirty-output-cache-in-optimizely-cms12/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;ddict_btn&quot;&gt;&lt;img src=&quot;chrome-extension://bpggmmljdiliancllaapiggllnkbjocb/logo/48.png&quot; /&gt;&lt;/div&gt;
&lt;div class=&quot;ddict_btn&quot;&gt;&lt;img src=&quot;chrome-extension://bpggmmljdiliancllaapiggllnkbjocb/logo/48.png&quot; /&gt;&lt;/div&gt;
&lt;div class=&quot;ddict_btn&quot;&gt;&lt;img src=&quot;chrome-extension://bpggmmljdiliancllaapiggllnkbjocb/logo/48.png&quot; /&gt;&lt;/div&gt;</id><updated>2023-03-17T04:57:59.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>How to route a simple url address within action name</title><link href="https://world.optimizely.com/blogs/binh-nguyen/dates/2020/8/how-to-route--a-content-with-simple-url-and-action/" /><id>&lt;p&gt;&lt;strong&gt;What is the simple address in the EpiServer?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The simple address is a single segment uniquely identifying&amp;nbsp;content and its language.&amp;nbsp;&lt;span&gt;The simple address can be used as a direct address without parent nodes included in URL.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/fc5212912a6a475498cd1a4e00f76984.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The situation here&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;You are using simple url address for your CMS pages and there are some pages you want to proceed data that sent from the client.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;p&gt;You are in the contact page &quot;{base_url}/contact-page-seo-url&quot;, you have a submit form to allow user input data and send back to our system by POST endpoint&amp;nbsp;&quot;{base_url}/contact-page-seo-url/subcription&quot;.&lt;/p&gt;
&lt;p&gt;Actually, you cannot use the endpoint like this, you will get 404 message immediately.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I realized that the default simple address route of Episerver always considers that all path after {base_url} is SIMPLE ADDRESS PATH includes ACTION name.&lt;/p&gt;
&lt;p&gt;So the system cannot route any content that matchs to the simple address &quot;contact-page-seo-url/subcription&quot;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How can we solve this problem?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Here is it. You need to add more a route to allow that. And we do not need to create any custom route, just do like that&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;    [InitializableModule]
    [ModuleDependency(typeof(EPiServer.Commerce.Initialization.InitializationModule))]
    public partial class SiteInitializationModule : IConfigurableModule
    {

        public void Initialize(InitializationEngine context)
        {
            RegisterCustomSimpleAddressRoute(context, RouteTable.Routes);
        }

        public void ConfigureContainer(ServiceConfigurationContext context)
        {
        }

        public void Uninitialize(InitializationEngine context)
        {
        }

        private static void RegisterCustomSimpleAddressRoute(InitializationEngine context, RouteCollection routes)
        {
            var rewriteExtension = Settings.Instance.UrlRewriteExtension;

            var urlResolver = context.Locate.Advanced.GetInstance&amp;lt;UrlResolver&amp;gt;();
            var contentLoader = context.Locate.Advanced.GetInstance&amp;lt;IContentLoader&amp;gt;();

            var contentLanguageSettingsHandler = context.Locate.Advanced.GetInstance&amp;lt;IContentLanguageSettingsHandler&amp;gt;();

            var basePathResolver = context.Locate.Advanced.GetInstance&amp;lt;IBasePathResolver&amp;gt;();

            var addressSegmentRouter = new SimpleAddressSegmentRouter(sd =&amp;gt; sd.StartPage);

            var parameters = new MapContentRouteParameters()
            {
                UrlSegmentRouter = addressSegmentRouter,
                BasePathResolver = basePathResolver.Resolve,
                Direction = SupportedDirection.Incoming,
                SegmentMappings = new Dictionary&amp;lt;string, ISegment&amp;gt;()
                {
                    {
                        RoutingConstants.SimpleAddressKey,
                        new SimpleAddressSegment(RoutingConstants.SimpleAddressKey, rewriteExtension, addressSegmentRouter, contentLoader, urlResolver, contentLanguageSettingsHandler)
                        {
                            MatchOneSegment = true
                        }
                    }
                }
            };


            routes.MapContentRoute(&quot;CustomSimpleAddress&quot;, &quot;{simpleaddresslanguage}/{simpleaddress}/{partial}/{action}&quot;, (object)new
            {
                action = &quot;index&quot;
            }, parameters);

        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Tada, it is not much complicated. You just need to register more another simple address route with parameter &quot;MatchOneSegment = true&quot;.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;This parameter indicates that only one segment is matching to a certain simple address instead of all segments as default.&lt;/p&gt;
&lt;p&gt;Hope this help some of you.&lt;/p&gt;</id><updated>2020-10-15T02:16:09.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Lock and Unlock account using AspNet Identity</title><link href="https://world.optimizely.com/blogs/binh-nguyen/dates/2020/4/lock-and-unlock-account-using-aspnet-identity/" /><id>&lt;p&gt;You are using AspNet Identity for authentication and want to configure to block user if he/she inputs wrong password over a certainly allowed login attempts. I have had an experience to implement this function in EpiServer version 11 and&amp;nbsp;Microsoft.AspNet.Identity 2.2&lt;/p&gt;
&lt;p&gt;Here are steps:&lt;/p&gt;
&lt;p&gt;1. Configure user lockout in your&amp;nbsp;ApplicationUserManager as mentioned in&amp;nbsp;&lt;a href=&quot;/link/5da1a6cc4d7f4f95a9daf3b5bb726748.aspx&quot;&gt;https://world.episerver.com/documentation/developer-guides/CMS/security/episerver-aspnetidentity/&lt;/a&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;// Configure user lockout defaults
manager.UserLockoutEnabledByDefault = true; //This flag is true it means will enable lockout when users are created. Noticed that a user is locked if LockEnable flag is true and LockoutEndDateUtc is set and greater than now
manager.DefaultAccountLockoutTimeSpan = TimeSpan.FromMinutes(60); //User will be locked in 60 minutes
manager.MaxFailedAccessAttemptsBeforeLockout = 5; //User will be locked after 5 continuesly failed attempts&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2. Pass shouldLockout is true when you call to validate user for login&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;  var signInStatus = await _signInManager.PasswordSignInAsync(username, password, isPersistent, shouldLockout:true);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3. If there are a lot of existed users that created before turning on user lockout functionality then you should migrate all existed user to enable lockout for them if you want to apply user lockout for all existed users too. You can create an Episerver migration step to do that like this:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;    [ServiceConfiguration(typeof(IMigrationStep))]
    public class EnableUserLockOutMigrationStep : IMigrationStep
    {
        private readonly IConnectionStringHandler _connectionHandler;

        public EnableUserLockOutMigrationStep(IConnectionStringHandler connectionHandler)
        {
            this._connectionHandler = connectionHandler;
        }

        public bool Execute(IProgressMessenger progressMessenger)
        {
            progressMessenger.AddProgressMessageText(&quot;Enabling user lockout...&quot;, false, 0);
            try
            {
              
                using (SqlConnection connection = new SqlConnection(this._connectionHandler.Commerce.ConnectionString))
                {
                    connection.Open();
                    using (SqlTransaction transaction = connection.BeginTransaction())
                    {
                        try
                        {
                            this.CreateCommand(transaction, @&quot;UPDATE [dbo].[AspNetUsers] SET [LockoutEnabled] = 1&quot;, 300).ExecuteNonQuery();
                            transaction.Commit();
                        }
                        catch (Exception ex)
                        {
                            transaction.Rollback();
                            connection.Close();

                            throw new Exception((string)null, ex);
                        }
                    }
                    connection.Close();
                }
                return true;
            }
            catch (Exception ex)
            {
                progressMessenger.AddProgressMessageText(string.Format((IFormatProvider)CultureInfo.InvariantCulture, &quot;Enable user lockout has failed with exception &#39;{0}&#39;.&quot;, (object)ex), true, 0);
            }
            return false;
        }

        public int Order =&amp;gt; 1000;
        public string Name =&amp;gt; &quot;Enable User Lockout&quot;;
        public string Description =&amp;gt; &quot;This is used to turn on Enable User Lockout for existed users&quot;;

        private SqlCommand CreateCommand(
            SqlTransaction transaction,
            string query,
            int timeout = 30)
        {
            return new SqlCommand
            {
                Connection = transaction.Connection,
                Transaction = transaction,
                CommandType = CommandType.Text,
                CommandText = query,
                CommandTimeout = timeout
            };
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Tada, it is not too complicated to enable lockout account, right? So what about if you want to unblock account somewhere? I see that we can do that in editing user view in admin mode like that:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/a7efb247c6df4eaa896112d856e63ec9.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;But it seems this function works well if we use Membership Provider for authentication. It does not works if I use Aspnet Identity.&lt;/p&gt;
&lt;p&gt;I found that the episerver is using IsLockedOut to check lockout status and unblock user by changing IsLockedOut to false. But currently Aspnet Identity uses the LockEnable flag and&amp;nbsp;LockoutEndDateUtc to check lockout status. So the solution that I use to unblock user in Aspnet Identity is creating a custom user that inherited from Application and over IsLockedOut property like this:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;        public override bool IsLockedOut
        {
            get =&amp;gt; LockoutEnabled &amp;amp;&amp;amp; LockoutEndDateUtc != null &amp;amp;&amp;amp; LockoutEndDateUtc &amp;gt;= DateTime.UtcNow;
            set
            {
                if (!LockoutEnabled || value) return;

                if (LockoutEndDateUtc != null &amp;amp;&amp;amp; LockoutEndDateUtc &amp;gt; DateTime.UtcNow)
                {
                    LastLockoutDate = LockoutEndDateUtc = DateTime.UtcNow;
                }
                AccessFailedCount = 0;
            }
        }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is all. Now you can unblock account in Episerver admin mode as usual.&lt;/p&gt;</id><updated>2020-04-24T02:41:33.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Workaround for supporting multilingual promotion</title><link href="https://world.optimizely.com/blogs/binh-nguyen/dates/2019/4/workaround-for-supporting-multilingual-discount/" /><id>&lt;p&gt;Did you use to be bored with old marketing system? The performance when running the old promotion engine or customizing a promotion used to be a big problem. But since the new marketing system was launched, the hard time has been ended.&lt;/p&gt;
&lt;p&gt;But one good day, you have a customer request that we need to show our promotion name/description in the front view by multiple different languages. For example: there are 2 available languages in our site are English and Sweden.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How to do that?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;We are aware that we could not do that with OOTB&#39;s Episerver until now. Although discount data has already been based on IContent but it has some limitations compared to other Episerver content like pages, blocks or catalog contents.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;So is there any way to workaround with this requirement?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The answer here is YES but it is not simple way. Here is my way:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Install package EPiServer.VisitorGroupsCriteriaPack. We will use SelectedLanguage criterion for filtering promotions based on the current language&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Setup 2 visitor groups for English and Sweden&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/670809ee95dd4ce9869b00a1840846a1.aspx&quot; width=&quot;1311&quot; height=&quot;584&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/d74ea300f07f4e928cd8411f84766ce0.aspx&quot; width=&quot;1296&quot; height=&quot;151&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Create 2 different campaigns for these two languages and appropriated promotions in these campaigns&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/172e9b3655d84d31b74d3693bf3f4e7e.aspx&quot; width=&quot;1313&quot; height=&quot;527&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/7be7aa1592d24793ab5132c42b784a19.aspx&quot; width=&quot;1323&quot; height=&quot;541&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. Modiy&amp;nbsp;MarketContentLoader class in QuickSilver sample code a bit to see proper promotions based on the current language in the home page: &lt;/strong&gt;Using&amp;nbsp;CampaignVisitorGroupFilter to filter campaign based on visitor group&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt; private readonly CampaignVisitorGroupFilter _campaignVisitorGroupFilter;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public virtual IList&amp;lt;PromotionData&amp;gt; GetEvaluablePromotionsInPriorityOrderForMarket(IMarket market)
{
   var result = _campaignVisitorGroupFilter.Filter(
                new PromotionFilterContext(GetPromotions().Where(x =&amp;gt; IsValid(x, market)),
                    RequestFulfillmentStatus.All));
   return result.IncludedPromotions.OrderBy(x =&amp;gt; x.Priority).ToList();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;5. Finally, let&#39;s see the result in the front view&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;In English&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/link/04073edc2e6c47ebaf737a7ddde05837.aspx&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;In Sweden&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/link/37e6bcda0e5e4461b97a1a89ac6c68d3.aspx&quot; width=&quot;826&quot; height=&quot;830&quot; /&gt;&lt;/p&gt;</id><updated>2019-04-04T03:51:56.0000000Z</updated><summary type="html">Blog post</summary></entry></feed>