<?xml version="1.0" encoding="utf-8"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/"><channel><language>en</language><title>Blog posts by Prins Mark</title> <link>https://world.optimizely.com/blogs/prins-mark/</link><description></description><ttl>60</ttl><generator>Optimizely World</generator><item> <title>Calculate Tax on a Single Variation</title>            <link>http://markprins.eu/archive/calculate-tax-on-a-single-variation/</link>            <description>&lt;p&gt;On a Episerver Commerce project I&#39;m currently working on, prices are displayed primarily excluding Tax (because it is primarily targeted on B2B customers). So on the Market we set the setting &#39;&lt;span&gt;Prices Include Tax&lt;/span&gt;&#39; to false. However since B2C customers can also order from this site, the price including Tax should be displayed as well (next to the price without Tax)&lt;/p&gt;
&lt;p&gt;Normally Tax is calculated on the cart, and based on the shipping address. So how can we calculate the Tax without adding the product to the Cart. You can&#39;t, so you have to create a dummy cart, add an address, and then calculate the tax using this cart.&lt;/p&gt;
&lt;p&gt;This might sound daunting, but in fact it is not that complex at all.&lt;/p&gt;
&lt;p&gt;&#160;&lt;/p&gt;
&lt;p&gt;What I ended up with is a HtmlHelper that will get the price including Tax for a variation.&lt;/p&gt;
&lt;p&gt;&#160;&lt;/p&gt;
&lt;pre class=&quot;brush: csharp&quot;&gt;public static class VariationTaxHelper
{
    private static readonly Injected&amp;lt;ICurrentMarket&amp;gt; _currentMarket;
    private static readonly Injected&amp;lt;IOrderRepository&amp;gt; _orderRepository;
    private static readonly Injected&amp;lt;IOrderGroupFactory&amp;gt; _orderGroupFactory;
    private static readonly Injected&amp;lt;ITaxCalculator&amp;gt; _taxCalculator;
    private static readonly Injected&amp;lt;IPromotionService&amp;gt; _promotionService;
    private static readonly Injected&amp;lt;ICurrencyService&amp;gt; _currencyService;

    private static IMarket CurrentMarket =&amp;gt; _currentMarket.Service.GetCurrentMarket();

    public static Money PriceIncludingTax(this HtmlHelper helper, VariationContent variation)
    {
    	// Create a temporary cart
        ICart cart = _orderRepository.Service.LoadOrCreateCart&amp;lt;ICart&amp;gt;(
       	    CustomerContext.Current.CurrentContactId, &quot;TempCart&quot;, _currentMarket.Service);

        // Reads the selected currency from a cookie, defaults to Current Market DefaultCurrency
            cart.Currency = _currencyService.Service.GetCurrentCurrency();

	var address = _orderGroupFactory.Service.CreateOrderAddress(cart);
        address.CountryCode = CurrentMarket.Countries.FirstOrDefault();

	// Get the correct price for the currently logged in Contact
        var defaultPrice = PriceCalculationService.GetSalePrice(variation.Code, CurrentMarket.MarketId, cart.Currency);
        if (defaultPrice == null)
            return new Money(0, cart.Currency);

	// Check if any Item discounts should be applied 
        var discountedPrice = _promotionService.Service.GetDiscountPrice(
            defaultPrice.CatalogKey, CurrentMarket.MarketId, cart.Currency).UnitPrice;

	// Create a lineItem to add to the temporary Cart
        ILineItem lineItem = _orderGroupFactory.Service.CreateLineItem(variation.Code, cart);
        lineItem.Quantity = 1;
        lineItem.PlacedPrice = discountedPrice;
        lineItem.TaxCategoryId = variation.TaxCategoryId;
        cart.AddLineItem(lineItem);

	// Calculate the Tax amount using the ITaxCalculator
        var taxAmount = _taxCalculator.Service.GetSalesTax(lineItem, CurrentMarket,
            address, new Money(lineItem.PlacedPrice, cart.Currency));

	return new Money(discountedPrice.Amount + taxAmount.Amount , cart.Currency);
    }
}
&lt;/pre&gt;
&lt;p&gt;Since our site is based on &lt;a href=&quot;https://github.com/episerver/Foundation&quot;&gt;Episerver Foundation&lt;/a&gt;, any services you would be missing using this code can be found there.&lt;/p&gt;</description>            <guid>http://markprins.eu/archive/calculate-tax-on-a-single-variation/</guid>            <pubDate>Fri, 08 May 2020 12:05:14 GMT</pubDate>           <category>Blog post</category></item><item> <title>Country VisitorGroup routing in URL  domain/visitorgroup/language/  (domain.com/nl/nl/)</title>            <link>http://markprins.eu/archive/country-visitorgroup-routing-in-url-domainvisitorgrouplanguage-domaincomnlnl/</link>            <description>&lt;p&gt;Every once in a while a client mentions the request for their global website to have country specific content based on a country segment in the URL. Resulting in specific URLs like domain.com/nl/nl/ for Dutch content in Dutch, and domain.com/be/nl/ for Belgian content in Dutch.&lt;/p&gt;
&lt;p&gt;Most times we talk the client into using the region parts in cultures. So use the &lt;strong&gt;en&lt;/strong&gt; neutral culture for global English, the culture &lt;strong&gt;nl-NL&lt;/strong&gt; for Dutch content in Dutch and the &lt;strong&gt;nl-BE&lt;/strong&gt; culture for Belgian content in Dutch.&#160;However sometimes, using this approach we run into problems when a culture does not exist. Say we want the Dutch content to be available in German too, the &lt;strong&gt;de-NL&lt;/strong&gt; culture does not exist.&lt;/p&gt;
&lt;p&gt;This got me thinking about adding an URL segment to the URL for a specific country, where the country is represented by an episerver visitor group.&lt;/p&gt;
&lt;p&gt;With the help of &lt;a href=&quot;http://joelabrahamsson.com/custom-routing-for-episerver-content/&quot;&gt;this&lt;/a&gt; blog explaining custom routing for Episerver I created my custom segment and a custom visitor group.&lt;/p&gt;
&lt;p&gt;&#160;&lt;/p&gt;
&lt;pre class=&quot;brush: csharp&quot;&gt;    public class VisitorGroupSegment : SegmentBase
    {
        public VisitorGroupSegment(string name) : base(name)
        {
        }

        public override bool RouteDataMatch(SegmentContext context)
        {
            SegmentPair nextValue = context.GetNextValue(context.RemainingPath);
            
            // For the first fragment, check if it is a &#39;visitor group&#39; segment
            if (context.LastConsumedFragment is null &amp;amp;&amp;amp; IsVisitorGroupSegment(nextValue.Next))
            {
                context.RemainingPath = nextValue.Remaining;
                
                // Data Token to be used by VisitorGroup Criterion
                context.RouteData.DataTokens.Add(&quot;visitorgroup&quot;, nextValue.Next);

                return true;
            }

            return false;
        }

        private static bool IsVisitorGroupSegment(string segment)
        {
            var visitorGroupRepository = ServiceLocator.Current.GetInstance&amp;lt;IVisitorGroupRepository&amp;gt;();
            foreach (var vg in visitorGroupRepository.List())
            {
                foreach (var criterion in vg.Criteria)
                {
                    if (criterion.Model is RouteVisitorGroupSettings model &amp;amp;&amp;amp;
                        model.RouteSegment.Equals(segment, StringComparison.InvariantCultureIgnoreCase))
                    {
                        return true;
                    }
                }
            }

            return false;
        }

        public override string GetVirtualPathSegment(RequestContext requestContext, RouteValueDictionary values)
        {
            if (GetContextMode(requestContext.HttpContext, requestContext.RouteData)
                != ContextMode.Default)
            {
                return null;
            }

            return GetRoutingVisitorGroupByCurrentUser(new HttpContextWrapper(HttpContext.Current));
        }

        private static ContextMode GetContextMode(HttpContextBase httpContext, RouteData routeData)
        {
            var contextModeKey = &quot;contextmode&quot;;

            if (routeData.DataTokens.ContainsKey(contextModeKey))
            {
                return (ContextMode)routeData.DataTokens[contextModeKey];
            }

            if (httpContext?.Request == null)
            {
                return ContextMode.Default;
            }

            if (!PageEditing.PageIsInEditMode)
            {
                return ContextMode.Default;
            }

            return ContextMode.Edit;
        }


        private static string GetRoutingVisitorGroupByCurrentUser(HttpContextBase httpContext)
        {
            var visitorGroupRepository = ServiceLocator.Current.GetInstance&amp;lt;IVisitorGroupRepository&amp;gt;();
            var visitorGroupRoleRepository = ServiceLocator.Current.GetInstance&amp;lt;IVisitorGroupRoleRepository&amp;gt;();

            var user = httpContext.User;

            // Check all visitor groups and check if one is active, if one is active return the segment.
            var visitorGroups = visitorGroupRepository.List();
            foreach (var vg in visitorGroups)
            {
                foreach (var criterion in vg.Criteria)
                {
                    if (criterion.Model is RouteVisitorGroupSettings model &amp;amp;&amp;amp; visitorGroupRoleRepository.TryGetRole(vg.Name, out var virtualRoleObject))
                    {
                        if (virtualRoleObject.IsMatch(user, httpContext))
                        {
                            return model.RouteSegment;
                        }
                    }
                }
            }
            return null;
        }
    }
&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;The custom visitor group:&lt;/em&gt;&lt;/p&gt;
&lt;pre class=&quot;brush: csharp&quot;&gt;    public class RouteVisitorGroupSettings : CriterionModelBase
    {
        [Required]
        public string RouteSegment { get; set; }

        public override ICriterionModel Copy()
        {
            return ShallowCopy();
        }
    }

    [VisitorGroupCriterion(
        Category = &quot;Technical&quot;,
        DisplayName = &quot;VisitorGroup Routing&quot;,
        Description = &quot;&quot;)]
    public class RouteVisitorGroupCriterion : CriterionBase
    {
        public override bool IsMatch(IPrincipal principal, HttpContextBase httpContext)
        {
            // Check for DataToken added by RouteSegment.
            var routeDataTokens = httpContext.Request.RequestContext.RouteData.DataTokens;

            return routeDataTokens.ContainsKey(&quot;visitorgroup&quot;)
                   &amp;amp;&amp;amp; ((string)routeDataTokens[&quot;visitorgroup&quot;]).Equals(Model.RouteSegment, StringComparison.InvariantCultureIgnoreCase);
        }
    }
&lt;/pre&gt;
&lt;p&gt;Use an initializable module to make this work:&lt;/p&gt;
&lt;pre class=&quot;brush: csharp&quot;&gt;    [InitializableModule]
    [ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
    public class RouteEventInitializationModule : IInitializableModule
    {
        public void Initialize(InitializationEngine context)
        {
            var segment = new VisitorGroupSegment(&quot;visitorgroup&quot;);

            var routingParameters = new MapContentRouteParameters()
            {
                SegmentMappings = new Dictionary&amp;lt;string, ISegment&amp;gt;()
            };

            routingParameters.SegmentMappings.Add(&quot;visitorgroup&quot;, segment);

            RouteTable.Routes.MapContentRoute(
                name: &quot;visitorgroups&quot;,
                url: &quot;{visitorgroup}/{language}/{node}/{partial}/{action}&quot;,
                defaults: new { action = &quot;index&quot; },
                parameters: routingParameters);
        }

        public void Uninitialize(InitializationEngine context)
        {
        }
    }
&lt;/pre&gt;
&lt;p&gt;Now by adding this to a default Alloy site and creating two vistorgroups, one with segment ‘nl’ for Netherlands called &#39;NLD&#39;, and one with segment ‘en’ for United Kingdom called &#39;ENG&#39;. I can display different content using personalization by visitor groups.&lt;/p&gt;
&lt;p&gt;&#160;&lt;/p&gt;
&lt;p&gt;&lt;img style=&quot;width: 500px; height: 202.0497803806735px;&quot; src=&quot;http://markprins.eu/media/1038/cms.jpg?width=500&amp;amp;height=202.0497803806735&quot; alt=&quot;&quot; data-udi=&quot;umb://media/fb1a1c001ba24490a69f8392e0f85493&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&#160;&lt;/p&gt;
&lt;table border=&quot;0&quot; style=&quot;height: 63px;&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 42px;&quot;&gt;
&lt;td style=&quot;height: 42px;&quot;&gt;http://localhost:23529/&lt;/td&gt;
&lt;td style=&quot;height: 42px;&quot;&gt;http://localhost:23529/en &lt;br /&gt;http://localhost:23529/en/en&lt;/td&gt;
&lt;td style=&quot;height: 42px;&quot;&gt;http://localhost:23529/nl &lt;br /&gt;http://localhost:23529/nl/en&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;img style=&quot;width: 330px; height: 200px;&quot; src=&quot;http://markprins.eu/media/1039/gobal.jpg?width=330&amp;amp;height=200&amp;amp;mode=max&quot; alt=&quot;&quot; data-udi=&quot;umb://media/8bf98a33a1454fedb35bb1532dee2270&quot; /&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;img style=&quot;width: 330px; height: 200px;&quot; src=&quot;http://markprins.eu/media/1040/vg-en.jpg?width=330&amp;amp;height=200&amp;amp;mode=max&quot; alt=&quot;&quot; data-udi=&quot;umb://media/aa88250c67e94c84940f2ee43eca12c8&quot; /&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;img style=&quot;width: 330px; height: 200px;&quot; src=&quot;http://markprins.eu/media/1041/vg-nl.jpg?width=330&amp;amp;height=200&amp;amp;mode=max&quot; alt=&quot;&quot; data-udi=&quot;umb://media/e15899b769c14f3bb89fba77b07ac608&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;This is just a simple proof of concept, all seems to be working correctly, but I would not copy paste this into a production environment.&lt;/p&gt;
&lt;p&gt;Let me know what you think or if you have any questions!&lt;/p&gt;
&lt;p&gt;&#160;&lt;/p&gt;
&lt;p&gt;&#160;&lt;/p&gt;</description>            <guid>http://markprins.eu/archive/country-visitorgroup-routing-in-url-domainvisitorgrouplanguage-domaincomnlnl/</guid>            <pubDate>Fri, 08 Nov 2019 09:09:35 GMT</pubDate>           <category>Blog post</category></item><item> <title>Country VisitorGroup routing in URL  domain/visitorgroup/language/  (domain.com/nl/nl/)</title>            <link>https://markprins.eu/archive/country-visitorgroup-routing-in-url-domainvisitorgrouplanguage-domaincomnlnl/</link>            <description>&lt;p&gt;Every once in a while a client mentions the request for their global website to have country specific content based on a country segment in the URL. Resulting in specific URLs like domain.com/nl/nl/ for Dutch content in Dutch, and domain.com/be/nl/ for Belgian content in Dutch.&lt;/p&gt;
&lt;p&gt;Most times we talk the client into using the region parts in cultures. So use the &lt;strong&gt;en&lt;/strong&gt; neutral culture for global English, the culture &lt;strong&gt;nl-NL&lt;/strong&gt; for Dutch content in Dutch and the &lt;strong&gt;nl-BE&lt;/strong&gt; culture for Belgian content in Dutch.&#160;However sometimes, using this approach we run into problems when a culture does not exist. Say we want the Dutch content to be available in German too, the &lt;strong&gt;de-NL&lt;/strong&gt; culture does not exist.&lt;/p&gt;
&lt;p&gt;This got me thinking about adding an URL segment to the URL for a specific country, where the country is represented by an episerver visitor group.&lt;/p&gt;
&lt;p&gt;With the help of &lt;a href=&quot;http://joelabrahamsson.com/custom-routing-for-episerver-content/&quot;&gt;this&lt;/a&gt; blog explaining custom routing for Episerver I created my custom segment and a custom visitor group.&lt;/p&gt;
&lt;p&gt;&#160;&lt;/p&gt;
&lt;pre class=&quot;brush: csharp&quot;&gt;    public class VisitorGroupSegment : SegmentBase
    {
        public VisitorGroupSegment(string name) : base(name)
        {
        }

        public override bool RouteDataMatch(SegmentContext context)
        {
            SegmentPair nextValue = context.GetNextValue(context.RemainingPath);
            
            // For the first fragment, check if it is a &#39;visitor group&#39; segment
            if (context.LastConsumedFragment is null &amp;amp;&amp;amp; IsVisitorGroupSegment(nextValue.Next))
            {
                context.RemainingPath = nextValue.Remaining;
                
                // Data Token to be used by VisitorGroup Criterion
                context.RouteData.DataTokens.Add(&quot;visitorgroup&quot;, nextValue.Next);

                return true;
            }

            return false;
        }

        private static bool IsVisitorGroupSegment(string segment)
        {
            var visitorGroupRepository = ServiceLocator.Current.GetInstance&amp;lt;IVisitorGroupRepository&amp;gt;();
            foreach (var vg in visitorGroupRepository.List())
            {
                foreach (var criterion in vg.Criteria)
                {
                    if (criterion.Model is RouteVisitorGroupSettings model &amp;amp;&amp;amp;
                        model.RouteSegment.Equals(segment, StringComparison.InvariantCultureIgnoreCase))
                    {
                        return true;
                    }
                }
            }

            return false;
        }

        public override string GetVirtualPathSegment(RequestContext requestContext, RouteValueDictionary values)
        {
            if (GetContextMode(requestContext.HttpContext, requestContext.RouteData)
                != ContextMode.Default)
            {
                return null;
            }

            return GetRoutingVisitorGroupByCurrentUser(new HttpContextWrapper(HttpContext.Current));
        }

        private static ContextMode GetContextMode(HttpContextBase httpContext, RouteData routeData)
        {
            var contextModeKey = &quot;contextmode&quot;;

            if (routeData.DataTokens.ContainsKey(contextModeKey))
            {
                return (ContextMode)routeData.DataTokens[contextModeKey];
            }

            if (httpContext?.Request == null)
            {
                return ContextMode.Default;
            }

            if (!PageEditing.PageIsInEditMode)
            {
                return ContextMode.Default;
            }

            return ContextMode.Edit;
        }


        private static string GetRoutingVisitorGroupByCurrentUser(HttpContextBase httpContext)
        {
            var visitorGroupRepository = ServiceLocator.Current.GetInstance&amp;lt;IVisitorGroupRepository&amp;gt;();
            var visitorGroupRoleRepository = ServiceLocator.Current.GetInstance&amp;lt;IVisitorGroupRoleRepository&amp;gt;();

            var user = httpContext.User;

            // Check all visitor groups and check if one is active, if one is active return the segment.
            var visitorGroups = visitorGroupRepository.List();
            foreach (var vg in visitorGroups)
            {
                foreach (var criterion in vg.Criteria)
                {
                    if (criterion.Model is RouteVisitorGroupSettings model &amp;amp;&amp;amp; visitorGroupRoleRepository.TryGetRole(vg.Name, out var virtualRoleObject))
                    {
                        if (virtualRoleObject.IsMatch(user, httpContext))
                        {
                            return model.RouteSegment;
                        }
                    }
                }
            }
            return null;
        }
    }
&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;The custom visitor group:&lt;/em&gt;&lt;/p&gt;
&lt;pre class=&quot;brush: csharp&quot;&gt;    public class RouteVisitorGroupSettings : CriterionModelBase
    {
        [Required]
        public string RouteSegment { get; set; }

        public override ICriterionModel Copy()
        {
            return ShallowCopy();
        }
    }

    [VisitorGroupCriterion(
        Category = &quot;Technical&quot;,
        DisplayName = &quot;VisitorGroup Routing&quot;,
        Description = &quot;&quot;)]
    public class RouteVisitorGroupCriterion : CriterionBase
    {
        public override bool IsMatch(IPrincipal principal, HttpContextBase httpContext)
        {
            // Check for DataToken added by RouteSegment.
            var routeDataTokens = httpContext.Request.RequestContext.RouteData.DataTokens;

            return routeDataTokens.ContainsKey(&quot;visitorgroup&quot;)
                   &amp;amp;&amp;amp; ((string)routeDataTokens[&quot;visitorgroup&quot;]).Equals(Model.RouteSegment, StringComparison.InvariantCultureIgnoreCase);
        }
    }
&lt;/pre&gt;
&lt;p&gt;Use an initializable module to make this work:&lt;/p&gt;
&lt;pre class=&quot;brush: csharp&quot;&gt;    [InitializableModule]
    [ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
    public class RouteEventInitializationModule : IInitializableModule
    {
        public void Initialize(InitializationEngine context)
        {
            var segment = new VisitorGroupSegment(&quot;visitorgroup&quot;);

            var routingParameters = new MapContentRouteParameters()
            {
                SegmentMappings = new Dictionary&amp;lt;string, ISegment&amp;gt;()
            };

            routingParameters.SegmentMappings.Add(&quot;visitorgroup&quot;, segment);

            RouteTable.Routes.MapContentRoute(
                name: &quot;visitorgroups&quot;,
                url: &quot;{visitorgroup}/{language}/{node}/{partial}/{action}&quot;,
                defaults: new { action = &quot;index&quot; },
                parameters: routingParameters);
        }

        public void Uninitialize(InitializationEngine context)
        {
        }
    }
&lt;/pre&gt;
&lt;p&gt;Now by adding this to a default Alloy site and creating two vistorgroups, one with segment ‘nl’ for Netherlands called &#39;NLD&#39;, and one with segment ‘en’ for United Kingdom called &#39;ENG&#39;. I can display different content using personalization by visitor groups.&lt;/p&gt;
&lt;p&gt;&#160;&lt;/p&gt;
&lt;p&gt;&lt;img style=&quot;width: 500px; height: 202.0497803806735px;&quot; src=&quot;https://markprins.eu/media/1038/cms.jpg?width=500&amp;amp;height=202.0497803806735&quot; alt=&quot;&quot; data-udi=&quot;umb://media/fb1a1c001ba24490a69f8392e0f85493&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&#160;&lt;/p&gt;
&lt;table border=&quot;0&quot; style=&quot;height: 63px;&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 42px;&quot;&gt;
&lt;td style=&quot;height: 42px;&quot;&gt;http://localhost:23529/&lt;/td&gt;
&lt;td style=&quot;height: 42px;&quot;&gt;http://localhost:23529/en &lt;br /&gt;http://localhost:23529/en/en&lt;/td&gt;
&lt;td style=&quot;height: 42px;&quot;&gt;http://localhost:23529/nl &lt;br /&gt;http://localhost:23529/nl/en&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;img style=&quot;width: 330px; height: 200px;&quot; src=&quot;https://markprins.eu/media/1039/gobal.jpg?width=330&amp;amp;height=200&amp;amp;mode=max&quot; alt=&quot;&quot; data-udi=&quot;umb://media/8bf98a33a1454fedb35bb1532dee2270&quot; /&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;img style=&quot;width: 330px; height: 200px;&quot; src=&quot;https://markprins.eu/media/1040/vg-en.jpg?width=330&amp;amp;height=200&amp;amp;mode=max&quot; alt=&quot;&quot; data-udi=&quot;umb://media/aa88250c67e94c84940f2ee43eca12c8&quot; /&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;img style=&quot;width: 330px; height: 200px;&quot; src=&quot;https://markprins.eu/media/1041/vg-nl.jpg?width=330&amp;amp;height=200&amp;amp;mode=max&quot; alt=&quot;&quot; data-udi=&quot;umb://media/e15899b769c14f3bb89fba77b07ac608&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;This is just a simple proof of concept, all seems to be working correctly, but I would not copy paste this into a production environment.&lt;/p&gt;
&lt;p&gt;Let me know what you think or if you have any questions!&lt;/p&gt;
&lt;p&gt;&#160;&lt;/p&gt;
&lt;p&gt;&#160;&lt;/p&gt;</description>            <guid>https://markprins.eu/archive/country-visitorgroup-routing-in-url-domainvisitorgrouplanguage-domaincomnlnl/</guid>            <pubDate>Fri, 08 Nov 2019 08:09:35 GMT</pubDate>           <category>Blog post</category></item><item> <title>Country VisitorGroup routing in URL  domain/visitorgroup/language/  (domain.com/nl/nl/)</title>            <link>http://markprins.eu/archive/country-visitorgroup-routing-in-url-domainvisitorgrouplanguage-domaincomnlnl/</link>            <description>&lt;p&gt;Every once in a while a client mentions the request for their global website to have country specific content based on a country segment in the URL. Resulting in specific URLs like domain.com/nl/nl/ for Dutch content in Dutch, and domain.com/be/nl/ for Belgian content in Dutch.&lt;/p&gt;
&lt;p&gt;Most times we talk the client into using the region parts in cultures. So use the &lt;strong&gt;en&lt;/strong&gt; neutral culture for global English, the culture &lt;strong&gt;nl-NL&lt;/strong&gt; for Dutch content in Dutch and the &lt;strong&gt;nl-BE&lt;/strong&gt; culture for Belgian content in Dutch.&#160;However sometimes, using this approach we run into problems when a culture does not exist. Say we want the Dutch content to be available in German too, the &lt;strong&gt;de-NL&lt;/strong&gt; culture does not exist.&lt;/p&gt;
&lt;p&gt;This got me thinking about adding an URL segment to the URL for a specific country, where the country is represented by an episerver visitor group.&lt;/p&gt;
&lt;p&gt;With the help of &lt;a href=&quot;http://joelabrahamsson.com/custom-routing-for-episerver-content/&quot;&gt;this&lt;/a&gt; blog explaining custom routing for Episerver I created my custom segment and a custom visitor group.&lt;/p&gt;
&lt;p&gt;&#160;&lt;/p&gt;
&lt;pre class=&quot;brush: csharp&quot;&gt;    public class VisitorGroupSegment : SegmentBase
    {
        public VisitorGroupSegment(string name) : base(name)
        {
        }

        public override bool RouteDataMatch(SegmentContext context)
        {
            SegmentPair nextValue = context.GetNextValue(context.RemainingPath);
            
            // For the first fragment, check if it is a &#39;visitor group&#39; segment
            if (context.LastConsumedFragment is null &amp;amp;&amp;amp; IsVisitorGroupSegment(nextValue.Next))
            {
                context.RemainingPath = nextValue.Remaining;
                
                // Data Token to be used by VisitorGroup Criterion
                context.RouteData.DataTokens.Add(&quot;visitorgroup&quot;, nextValue.Next);

                return true;
            }

            return false;
        }

        private static bool IsVisitorGroupSegment(string segment)
        {
            var visitorGroupRepository = ServiceLocator.Current.GetInstance&amp;lt;IVisitorGroupRepository&amp;gt;();
            foreach (var vg in visitorGroupRepository.List())
            {
                foreach (var criterion in vg.Criteria)
                {
                    if (criterion.Model is RouteVisitorGroupSettings model &amp;amp;&amp;amp;
                        model.RouteSegment.Equals(segment, StringComparison.InvariantCultureIgnoreCase))
                    {
                        return true;
                    }
                }
            }

            return false;
        }

        public override string GetVirtualPathSegment(RequestContext requestContext, RouteValueDictionary values)
        {
            if (GetContextMode(requestContext.HttpContext, requestContext.RouteData)
                != ContextMode.Default)
            {
                return null;
            }

            return GetRoutingVisitorGroupByCurrentUser(new HttpContextWrapper(HttpContext.Current));
        }

        private static ContextMode GetContextMode(HttpContextBase httpContext, RouteData routeData)
        {
            var contextModeKey = &quot;contextmode&quot;;

            if (routeData.DataTokens.ContainsKey(contextModeKey))
            {
                return (ContextMode)routeData.DataTokens[contextModeKey];
            }

            if (httpContext?.Request == null)
            {
                return ContextMode.Default;
            }

            if (!PageEditing.PageIsInEditMode)
            {
                return ContextMode.Default;
            }

            return ContextMode.Edit;
        }


        private static string GetRoutingVisitorGroupByCurrentUser(HttpContextBase httpContext)
        {
            var visitorGroupRepository = ServiceLocator.Current.GetInstance&amp;lt;IVisitorGroupRepository&amp;gt;();
            var visitorGroupRoleRepository = ServiceLocator.Current.GetInstance&amp;lt;IVisitorGroupRoleRepository&amp;gt;();

            var user = httpContext.User;

            // Check all visitor groups and check if one is active, if one is active return the segment.
            var visitorGroups = visitorGroupRepository.List();
            foreach (var vg in visitorGroups)
            {
                foreach (var criterion in vg.Criteria)
                {
                    if (criterion.Model is RouteVisitorGroupSettings model &amp;amp;&amp;amp; visitorGroupRoleRepository.TryGetRole(vg.Name, out var virtualRoleObject))
                    {
                        if (virtualRoleObject.IsMatch(user, httpContext))
                        {
                            return model.RouteSegment;
                        }
                    }
                }
            }
            return null;
        }
    }
&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;The custom visitor group:&lt;/em&gt;&lt;/p&gt;
&lt;pre class=&quot;brush: csharp&quot;&gt;    public class RouteVisitorGroupSettings : CriterionModelBase
    {
        [Required]
        public string RouteSegment { get; set; }

        public override ICriterionModel Copy()
        {
            return ShallowCopy();
        }
    }

    [VisitorGroupCriterion(
        Category = &quot;Technical&quot;,
        DisplayName = &quot;VisitorGroup Routing&quot;,
        Description = &quot;&quot;)]
    public class RouteVisitorGroupCriterion : CriterionBase
    {
        public override bool IsMatch(IPrincipal principal, HttpContextBase httpContext)
        {
            // Check for DataToken added by RouteSegment.
            var routeDataTokens = httpContext.Request.RequestContext.RouteData.DataTokens;

            return routeDataTokens.ContainsKey(&quot;visitorgroup&quot;)
                   &amp;amp;&amp;amp; ((string)routeDataTokens[&quot;visitorgroup&quot;]).Equals(Model.RouteSegment, StringComparison.InvariantCultureIgnoreCase);
        }
    }
&lt;/pre&gt;
&lt;p&gt;Use an initializable module to make this work:&lt;/p&gt;
&lt;pre class=&quot;brush: csharp&quot;&gt;    [InitializableModule]
    [ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
    public class RouteEventInitializationModule : IInitializableModule
    {
        public void Initialize(InitializationEngine context)
        {
            var segment = new VisitorGroupSegment(&quot;visitorgroup&quot;);

            var routingParameters = new MapContentRouteParameters()
            {
                SegmentMappings = new Dictionary&amp;lt;string, ISegment&amp;gt;()
            };

            routingParameters.SegmentMappings.Add(&quot;visitorgroup&quot;, segment);

            RouteTable.Routes.MapContentRoute(
                name: &quot;visitorgroups&quot;,
                url: &quot;{visitorgroup}/{language}/{node}/{partial}/{action}&quot;,
                defaults: new { action = &quot;index&quot; },
                parameters: routingParameters);
        }

        public void Uninitialize(InitializationEngine context)
        {
        }
    }
&lt;/pre&gt;
&lt;p&gt;Now by adding this to a default Alloy site and creating two vistorgroups, one with segment ‘nl’ for Netherlands called &#39;NLD&#39;, and one with segment ‘en’ for United Kingdom called &#39;ENG&#39;. I can display different content using personalization by visitor groups.&lt;/p&gt;
&lt;p&gt;&#160;&lt;/p&gt;
&lt;p&gt;&lt;img style=&quot;width: 500px; height: 202.0497803806735px;&quot; src=&quot;http://markprins.eu/media/1038/cms.jpg?width=500&amp;amp;height=202.0497803806735&quot; alt=&quot;&quot; data-udi=&quot;umb://media/fb1a1c001ba24490a69f8392e0f85493&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&#160;&lt;/p&gt;
&lt;table border=&quot;0&quot; style=&quot;height: 63px;&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 42px;&quot;&gt;
&lt;td style=&quot;height: 42px;&quot;&gt;http://localhost:23529/&lt;/td&gt;
&lt;td style=&quot;height: 42px;&quot;&gt;http://localhost:23529/en &lt;br /&gt;http://localhost:23529/en/en&lt;/td&gt;
&lt;td style=&quot;height: 42px;&quot;&gt;http://localhost:23529/nl &lt;br /&gt;http://localhost:23529/nl/en&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;img style=&quot;width: 330px; height: 200px;&quot; src=&quot;http://markprins.eu/media/1039/gobal.jpg?width=330&amp;amp;height=200&amp;amp;mode=max&quot; alt=&quot;&quot; data-udi=&quot;umb://media/8bf98a33a1454fedb35bb1532dee2270&quot; /&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;img style=&quot;width: 330px; height: 200px;&quot; src=&quot;http://markprins.eu/media/1040/vg-en.jpg?width=330&amp;amp;height=200&amp;amp;mode=max&quot; alt=&quot;&quot; data-udi=&quot;umb://media/aa88250c67e94c84940f2ee43eca12c8&quot; /&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;img style=&quot;width: 330px; height: 200px;&quot; src=&quot;http://markprins.eu/media/1041/vg-nl.jpg?width=330&amp;amp;height=200&amp;amp;mode=max&quot; alt=&quot;&quot; data-udi=&quot;umb://media/e15899b769c14f3bb89fba77b07ac608&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;This is just a simple proof of concept, all seems to be working correctly, but I would not copy paste this into a production environment.&lt;/p&gt;
&lt;p&gt;Let me know what you think or if you have any questions!&lt;/p&gt;
&lt;p&gt;&#160;&lt;/p&gt;
&lt;p&gt;&#160;&lt;/p&gt;</description>            <guid>http://markprins.eu/archive/country-visitorgroup-routing-in-url-domainvisitorgrouplanguage-domaincomnlnl/</guid>            <pubDate>Fri, 08 Nov 2019 08:09:35 GMT</pubDate>           <category>Blog post</category></item><item> <title>Integrating ContentKing in Episerver</title>            <link>http://markprins.eu/archive/integrating-contentking-in-episerver/</link>            <description>&lt;p&gt;Ever since a client of the company I work at came up with the SEO tool &lt;a href=&quot;https://www.contentkingapp.com/&quot;&gt;ContentKing &lt;/a&gt;we have been using this tool to monitor the SEO of multiple sites.&lt;/p&gt;
&lt;p&gt;ContentKing uses real-time auditing and content tracking to define a SEO score for each page in a website.&lt;/p&gt;
&lt;p&gt;Recently, I found out about the ContentKing &lt;a href=&quot;https://www.contentkingapp.com/support/cms-api/&quot;&gt;CMS API&lt;/a&gt; that allows the user to trigger priority auditing of a page through an API. Basically telling ContentKing that a page has changed, and requesting re-evaluation of the SEO score.&lt;/p&gt;
&lt;p&gt;In this blog I will explain how I integrated the CMS API in an episerver project.&lt;/p&gt;
&lt;p&gt;&#160;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Publish event&#160;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Firstly I needed to hook up to the Episerver publish event. This I did with an Initializable Module.&lt;/p&gt;
&lt;p&gt;&#160;&lt;/p&gt;
&lt;pre class=&quot;brush: csharp&quot;&gt;    [InitializableModule]
    [ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
    public class PublishEventInitializationModule : IInitializableModule
    {
        public void Initialize(InitializationEngine context)
        {
            var contentEvents = ServiceLocator.Current.GetInstance&amp;lt;IContentEvents&amp;gt;();
            contentEvents.PublishingContent += contentEvents_PublishingContent;
        }

        public void Uninitialize(InitializationEngine context)
        {
            var contentEvents = ServiceLocator.Current.GetInstance&amp;lt;IContentEvents&amp;gt;();
            contentEvents.PublishingContent -= contentEvents_PublishingContent;
        }

        void contentEvents_PublishingContent(object sender, EPiServer.ContentEventArgs e)
        {
            var contentKingService = ServiceLocator.Current.GetInstance&amp;lt;IContentKingService&amp;gt;();
            contentKingService?.TriggerPageAudit(e.Content);
        }
    }
&lt;/pre&gt;
&lt;p&gt;&#160;&lt;/p&gt;
&lt;p&gt;When a page is published now the ContentKingService is called to trigger page auditing. The service reads the ContentKing API key needed using a settingsservice, and uses the UrlHelper and ISiteDefinitionResolver to retrieve the full url of the page published. Then does a POST to the ContentKing API using an HttpClient.&lt;/p&gt;
&lt;p&gt;&#160;&lt;/p&gt;
&lt;pre class=&quot;brush: csharp&quot;&gt;[ServiceConfiguration(ServiceType = typeof(IContentKingService), Lifecycle = ServiceInstanceScope.HttpContext)]
    public class ContentKingService : IContentKingService
    {
        private readonly UrlHelper _urlHelper;
        private readonly ISiteDefinitionResolver _siteDefResolver;
        private readonly ISettingsService _settingsService;

        private string EndPointUrl = &quot;https://api.contentkingapp.com/&quot;;

        public ContentKingService()
        {
            _urlHelper = ServiceLocator.Current.GetInstance&amp;lt;UrlHelper&amp;gt;();
            _siteDefResolver = ServiceLocator.Current.GetInstance&amp;lt;ISiteDefinitionResolver&amp;gt;();
            _settingsService = ServiceLocator.Current.GetInstance&amp;lt;ISettingsService&amp;gt;();
        }

        public void TriggerPageAudit(IContent content)
        {
            var settings = _settingsService.GetSettings();

            try
            {
                if (settings != null &amp;amp;&amp;amp;
                    settings.TriggerAnalyzeOnPublish &amp;amp;&amp;amp;
                    !string.IsNullOrEmpty(settings.CmsApiKey))
                {
                    var fullPageUrl = ResolveUrl(content);

                    var url = EndPointUrl + &quot;v1/check_url&quot;;
                    var requestHeaders = new NameValueCollection { { &quot;Authorization&quot;, $&quot;token {settings.CmsApiKey}&quot; } };
                    var json = JsonConvert.SerializeObject(new
                    {
                        url = fullPageUrl
                    });

                    var result = PostObject&amp;lt;object&amp;gt;(url, json, requestHeaders);
                }
            }
            catch
            {
                //ToDo Do something with exceptions..
            }
        }

        public string ResolveUrl(IContent content)
        {
            var contentUrl = _urlHelper.ContentUrl(content.ContentLink);
            if (contentUrl.Contains(&quot;//&quot;))
                return contentUrl;
            
            return new Uri(_siteDefResolver.GetByContent(content.ContentLink, true, true).SiteUrl, contentUrl).AbsoluteUri;
        }

        private static T PostObject&amp;lt;T&amp;gt;(string url, string jsonContent, NameValueCollection requestHeaders = null)
        {
            var httpContent = new StringContent(jsonContent, Encoding.UTF8, &quot;application/json&quot;);
            using (var client = new HttpClient())
            {
                if (requestHeaders != null &amp;amp;&amp;amp; requestHeaders.Count &amp;gt; 0)
                {
                    foreach (string key in requestHeaders.Keys)
                    {
                        client.DefaultRequestHeaders.Add(key, requestHeaders[key]);
                    }
                }

                client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(&quot;application/json&quot;));

                var httpResponse = client.PostAsync(url, httpContent).Result;

                if (!httpResponse.IsSuccessStatusCode)
                    throw new Exception($&quot;{httpResponse.StatusCode} | {httpResponse.ReasonPhrase}&quot;);

                return httpResponse.Content.ReadAsAsync&amp;lt;T&amp;gt;().Result;
            }
        }
    }
&lt;/pre&gt;
&lt;p&gt;&#160;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CMS Admin Page&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;To make the integration more flexible I wanted to create a page in the cms-admin section of Episerver to manage the ContentKing API key, and enable or disable the triggering. To create a page in cms-admin section I created a controller for the page and added the EPiServer.PlugIn.GuiPlugIn attribute.&lt;/p&gt;
&lt;p&gt;&#160;&lt;/p&gt;
&lt;pre class=&quot;brush: csharp&quot;&gt;    [EPiServer.PlugIn.GuiPlugIn(
        Area = EPiServer.PlugIn.PlugInArea.AdminMenu,
        Url = &quot;/ContentKingAdmin/Index&quot;,
        DisplayName = &quot;ContentKing Admin&quot;)]
    public class ContentKingAdminController : Controller
    {
        private readonly ISettingsService _settingsService;

        public ContentKingAdminController()
        {
            _settingsService = ServiceLocator.Current.GetInstance&amp;lt;ISettingsService&amp;gt;();
        }

        public ActionResult Index()
        {
            var model = _settingsService.GetSettings();

            return View(model);
        }

        [System.Web.Mvc.HttpPost]
        public ActionResult Save(SettingsModel model)
        {
            _settingsService.SaveSettings(model);

            return RedirectToAction(&quot;Index&quot;);
        }
    }
&lt;/pre&gt;
&lt;p&gt;Then I created a view to create a ui for the settings. (I stole some styling from another episerver admin page)&lt;/p&gt;
&lt;pre class=&quot;highlight: [5, 15]; html-script: true&quot;&gt;@{
    Layout = null;
}

@model AlloyContentKing.Models.SettingsModel

&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;title&amp;gt;&amp;lt;/title&amp;gt;
    &amp;lt;!-- Mimic Internet Explorer 7 --&amp;gt;
    &amp;lt;meta http-equiv=&quot;X-UA-Compatible&quot; content=&quot;IE=EmulateIE7&quot;&amp;gt;
    &amp;lt;link rel=&quot;stylesheet&quot; type=&quot;text/css&quot; href=&quot;/EPiServer/Shell/11.19.1/ClientResources/epi/themes/legacy/ShellCore.css&quot;&amp;gt;
    &amp;lt;script type=&quot;text/javascript&quot; src=&quot;/EPiServer/Shell/11.19.1/ClientResources/ShellCore.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;link rel=&quot;stylesheet&quot; type=&quot;text/css&quot; href=&quot;/EPiServer/Shell/11.19.1/ClientResources/epi/themes/legacy/ShellCoreLightTheme.css&quot;&amp;gt;
    &amp;lt;script type=&quot;text/javascript&quot; src=&quot;/EPiServer/CMS/11.19.1/ClientResources/ReportCenter/ReportCenter.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;link href=&quot;../../../App_Themes/Default/Styles/system.css&quot; type=&quot;text/css&quot; rel=&quot;stylesheet&quot;&amp;gt;
    &amp;lt;link href=&quot;../../../App_Themes/Default/Styles/ToolButton.css&quot; type=&quot;text/css&quot; rel=&quot;stylesheet&quot;&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;div class=&quot;epi-contentContainer epi-padding&quot;&amp;gt;
        &amp;lt;div class=&quot;epi-contentArea&quot;&amp;gt;
            &amp;lt;h1 class=&quot;EP-prefix&quot;&amp;gt;ContentKing Settings&amp;lt;/h1&amp;gt;
            &amp;lt;p class=&quot;EP-systemInfo&quot;&amp;gt;
                Settings for ContentKing module.
            &amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;div class=&quot;epi-formArea&quot;&amp;gt;
            &amp;lt;form id=&quot;cmsApiSettings&quot; method=&quot;POST&quot; action=&quot;\ContentKingAdmin\Save&quot;&amp;gt;
                &amp;lt;strong&amp;gt;ContentKing CMS Api&amp;lt;/strong&amp;gt;
                &amp;lt;div class=&quot;epi-size20&quot;&amp;gt;
                    &amp;lt;div&amp;gt;
                        &amp;lt;label&amp;gt;Trigger Page Analize on Publish&amp;lt;/label&amp;gt;
                        @Html.EditorFor(x =&amp;gt; Model.TriggerAnalyzeOnPublish)
                    &amp;lt;/div&amp;gt;
                    &amp;lt;div&amp;gt;
                        &amp;lt;label&amp;gt;ContentKing CMS Api Key&amp;lt;/label&amp;gt;
                        @Html.EditorFor(x =&amp;gt; Model.CmsApiKey)
                    &amp;lt;/div&amp;gt;
                &amp;lt;/div&amp;gt;
                &amp;lt;div class=&quot;epi-buttonContainer&quot;&amp;gt;
                    &amp;lt;span class=&quot;epi-cmsButton&quot;&amp;gt;
                        &amp;lt;input class=&quot;epi-cmsButton-text epi-cmsButton-tools epi-cmsButton-Save&quot;
                               type=&quot;submit&quot;
                               name=&quot;ctl00$FullRegion$MainRegion$ImportFile&quot;
                               id=&quot;FullRegion_MainRegion_ImportFile&quot;
                               value=&quot;Save&quot;
                               title=&quot;Save&quot;&amp;gt;
                    &amp;lt;/span&amp;gt;
                &amp;lt;/div&amp;gt;
            &amp;lt;/form&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/pre&gt;
&lt;p&gt;The Admin page looks like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://markprins.eu/media/1037/screencap.jpg&quot; alt=&quot;&quot; data-id=&quot;1412&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&#160;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Dynamic Data Store&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;To store the settings I use Episerver Dynamic Data Store (DDS). To store data in the DDS you first need to create a model that Implements IDynamicData&lt;/p&gt;
&lt;pre class=&quot;brush: csharp&quot;&gt;    public class SettingsModel : IDynamicData
    {
        public Identity Id { get; set; }

        public bool TriggerAnalyzeOnPublish { get; set; }

        public string CmsApiKey { get; set; }
    }
&lt;/pre&gt;
&lt;p&gt;The SettingsService reads or writes the settings from and to the DDS.&lt;/p&gt;
&lt;pre class=&quot;brush: csharp&quot;&gt;    [ServiceConfiguration(ServiceType = typeof(ISettingsService), Lifecycle = ServiceInstanceScope.Singleton)]
    public class SettingsService : ISettingsService
    {
        private Guid _settingsId = new Guid(&quot;05663fcf-9f39-4fc2-af49-bddfb76953e0&quot;);

        public SettingsModel GetSettings()
        {
            var store = DynamicDataStoreFactory.Instance.CreateStore(typeof(SettingsModel));
            return store.Items&amp;lt;SettingsModel&amp;gt;().FirstOrDefault(x =&amp;gt; x.Id.ExternalId == _settingsId);
        }

        public void SaveSettings(SettingsModel model)
        {
            model.Id = _settingsId;

            var store = DynamicDataStoreFactory.Instance.CreateStore(typeof(SettingsModel));
            store.Save(model);
        }
    }
&lt;/pre&gt;
&lt;p&gt;This concludes my first ever blog. Let me know what you think or if you have any questions!&lt;/p&gt;</description>            <guid>http://markprins.eu/archive/integrating-contentking-in-episerver/</guid>            <pubDate>Fri, 18 Oct 2019 16:21:24 GMT</pubDate>           <category>Blog post</category></item><item> <title>Integrating ContentKing in Episerver</title>            <link>https://markprins.eu/archive/integrating-contentking-in-episerver/</link>            <description>&lt;p&gt;Ever since a client of the company I work at came up with the SEO tool &lt;a href=&quot;https://www.contentkingapp.com/&quot;&gt;ContentKing &lt;/a&gt;we have been using this tool to monitor the SEO of multiple sites.&lt;/p&gt;
&lt;p&gt;ContentKing uses real-time auditing and content tracking to define a SEO score for each page in a website.&lt;/p&gt;
&lt;p&gt;Recently, I found out about the ContentKing &lt;a href=&quot;https://www.contentkingapp.com/support/cms-api/&quot;&gt;CMS API&lt;/a&gt; that allows the user to trigger priority auditing of a page through an API. Basically telling ContentKing that a page has changed, and requesting re-evaluation of the SEO score.&lt;/p&gt;
&lt;p&gt;In this blog I will explain how I integrated the CMS API in an episerver project.&lt;/p&gt;
&lt;p&gt;&#160;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Publish event&#160;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Firstly I needed to hook up to the Episerver publish event. This I did with an Initializable Module.&lt;/p&gt;
&lt;p&gt;&#160;&lt;/p&gt;
&lt;pre class=&quot;brush: csharp&quot;&gt;    [InitializableModule]
    [ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
    public class PublishEventInitializationModule : IInitializableModule
    {
        public void Initialize(InitializationEngine context)
        {
            var contentEvents = ServiceLocator.Current.GetInstance&amp;lt;IContentEvents&amp;gt;();
            contentEvents.PublishingContent += contentEvents_PublishingContent;
        }

        public void Uninitialize(InitializationEngine context)
        {
            var contentEvents = ServiceLocator.Current.GetInstance&amp;lt;IContentEvents&amp;gt;();
            contentEvents.PublishingContent -= contentEvents_PublishingContent;
        }

        void contentEvents_PublishingContent(object sender, EPiServer.ContentEventArgs e)
        {
            var contentKingService = ServiceLocator.Current.GetInstance&amp;lt;IContentKingService&amp;gt;();
            contentKingService?.TriggerPageAudit(e.Content);
        }
    }
&lt;/pre&gt;
&lt;p&gt;&#160;&lt;/p&gt;
&lt;p&gt;When a page is published now the ContentKingService is called to trigger page auditing. The service reads the ContentKing API key needed using a settingsservice, and uses the UrlHelper and ISiteDefinitionResolver to retrieve the full url of the page published. Then does a POST to the ContentKing API using an HttpClient.&lt;/p&gt;
&lt;p&gt;&#160;&lt;/p&gt;
&lt;pre class=&quot;brush: csharp&quot;&gt;[ServiceConfiguration(ServiceType = typeof(IContentKingService), Lifecycle = ServiceInstanceScope.HttpContext)]
    public class ContentKingService : IContentKingService
    {
        private readonly UrlHelper _urlHelper;
        private readonly ISiteDefinitionResolver _siteDefResolver;
        private readonly ISettingsService _settingsService;

        private string EndPointUrl = &quot;https://api.contentkingapp.com/&quot;;

        public ContentKingService()
        {
            _urlHelper = ServiceLocator.Current.GetInstance&amp;lt;UrlHelper&amp;gt;();
            _siteDefResolver = ServiceLocator.Current.GetInstance&amp;lt;ISiteDefinitionResolver&amp;gt;();
            _settingsService = ServiceLocator.Current.GetInstance&amp;lt;ISettingsService&amp;gt;();
        }

        public void TriggerPageAudit(IContent content)
        {
            var settings = _settingsService.GetSettings();

            try
            {
                if (settings != null &amp;amp;&amp;amp;
                    settings.TriggerAnalyzeOnPublish &amp;amp;&amp;amp;
                    !string.IsNullOrEmpty(settings.CmsApiKey))
                {
                    var fullPageUrl = ResolveUrl(content);

                    var url = EndPointUrl + &quot;v1/check_url&quot;;
                    var requestHeaders = new NameValueCollection { { &quot;Authorization&quot;, $&quot;token {settings.CmsApiKey}&quot; } };
                    var json = JsonConvert.SerializeObject(new
                    {
                        url = fullPageUrl
                    });

                    var result = PostObject&amp;lt;object&amp;gt;(url, json, requestHeaders);
                }
            }
            catch
            {
                //ToDo Do something with exceptions..
            }
        }

        public string ResolveUrl(IContent content)
        {
            var contentUrl = _urlHelper.ContentUrl(content.ContentLink);
            if (contentUrl.Contains(&quot;//&quot;))
                return contentUrl;
            
            return new Uri(_siteDefResolver.GetByContent(content.ContentLink, true, true).SiteUrl, contentUrl).AbsoluteUri;
        }

        private static T PostObject&amp;lt;T&amp;gt;(string url, string jsonContent, NameValueCollection requestHeaders = null)
        {
            var httpContent = new StringContent(jsonContent, Encoding.UTF8, &quot;application/json&quot;);
            using (var client = new HttpClient())
            {
                if (requestHeaders != null &amp;amp;&amp;amp; requestHeaders.Count &amp;gt; 0)
                {
                    foreach (string key in requestHeaders.Keys)
                    {
                        client.DefaultRequestHeaders.Add(key, requestHeaders[key]);
                    }
                }

                client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(&quot;application/json&quot;));

                var httpResponse = client.PostAsync(url, httpContent).Result;

                if (!httpResponse.IsSuccessStatusCode)
                    throw new Exception($&quot;{httpResponse.StatusCode} | {httpResponse.ReasonPhrase}&quot;);

                return httpResponse.Content.ReadAsAsync&amp;lt;T&amp;gt;().Result;
            }
        }
    }
&lt;/pre&gt;
&lt;p&gt;&#160;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CMS Admin Page&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;To make the integration more flexible I wanted to create a page in the cms-admin section of Episerver to manage the ContentKing API key, and enable or disable the triggering. To create a page in cms-admin section I created a controller for the page and added the EPiServer.PlugIn.GuiPlugIn attribute.&lt;/p&gt;
&lt;p&gt;&#160;&lt;/p&gt;
&lt;pre class=&quot;brush: csharp&quot;&gt;    [EPiServer.PlugIn.GuiPlugIn(
        Area = EPiServer.PlugIn.PlugInArea.AdminMenu,
        Url = &quot;/ContentKingAdmin/Index&quot;,
        DisplayName = &quot;ContentKing Admin&quot;)]
    public class ContentKingAdminController : Controller
    {
        private readonly ISettingsService _settingsService;

        public ContentKingAdminController()
        {
            _settingsService = ServiceLocator.Current.GetInstance&amp;lt;ISettingsService&amp;gt;();
        }

        public ActionResult Index()
        {
            var model = _settingsService.GetSettings();

            return View(model);
        }

        [System.Web.Mvc.HttpPost]
        public ActionResult Save(SettingsModel model)
        {
            _settingsService.SaveSettings(model);

            return RedirectToAction(&quot;Index&quot;);
        }
    }
&lt;/pre&gt;
&lt;p&gt;Then I created a view to create a ui for the settings. (I stole some styling from another episerver admin page)&lt;/p&gt;
&lt;pre class=&quot;highlight: [5, 15]; html-script: true&quot;&gt;@{
    Layout = null;
}

@model AlloyContentKing.Models.SettingsModel

&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;title&amp;gt;&amp;lt;/title&amp;gt;
    &amp;lt;!-- Mimic Internet Explorer 7 --&amp;gt;
    &amp;lt;meta http-equiv=&quot;X-UA-Compatible&quot; content=&quot;IE=EmulateIE7&quot;&amp;gt;
    &amp;lt;link rel=&quot;stylesheet&quot; type=&quot;text/css&quot; href=&quot;/EPiServer/Shell/11.19.1/ClientResources/epi/themes/legacy/ShellCore.css&quot;&amp;gt;
    &amp;lt;script type=&quot;text/javascript&quot; src=&quot;/EPiServer/Shell/11.19.1/ClientResources/ShellCore.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;link rel=&quot;stylesheet&quot; type=&quot;text/css&quot; href=&quot;/EPiServer/Shell/11.19.1/ClientResources/epi/themes/legacy/ShellCoreLightTheme.css&quot;&amp;gt;
    &amp;lt;script type=&quot;text/javascript&quot; src=&quot;/EPiServer/CMS/11.19.1/ClientResources/ReportCenter/ReportCenter.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;link href=&quot;../../../App_Themes/Default/Styles/system.css&quot; type=&quot;text/css&quot; rel=&quot;stylesheet&quot;&amp;gt;
    &amp;lt;link href=&quot;../../../App_Themes/Default/Styles/ToolButton.css&quot; type=&quot;text/css&quot; rel=&quot;stylesheet&quot;&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;div class=&quot;epi-contentContainer epi-padding&quot;&amp;gt;
        &amp;lt;div class=&quot;epi-contentArea&quot;&amp;gt;
            &amp;lt;h1 class=&quot;EP-prefix&quot;&amp;gt;ContentKing Settings&amp;lt;/h1&amp;gt;
            &amp;lt;p class=&quot;EP-systemInfo&quot;&amp;gt;
                Settings for ContentKing module.
            &amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;div class=&quot;epi-formArea&quot;&amp;gt;
            &amp;lt;form id=&quot;cmsApiSettings&quot; method=&quot;POST&quot; action=&quot;\ContentKingAdmin\Save&quot;&amp;gt;
                &amp;lt;strong&amp;gt;ContentKing CMS Api&amp;lt;/strong&amp;gt;
                &amp;lt;div class=&quot;epi-size20&quot;&amp;gt;
                    &amp;lt;div&amp;gt;
                        &amp;lt;label&amp;gt;Trigger Page Analize on Publish&amp;lt;/label&amp;gt;
                        @Html.EditorFor(x =&amp;gt; Model.TriggerAnalyzeOnPublish)
                    &amp;lt;/div&amp;gt;
                    &amp;lt;div&amp;gt;
                        &amp;lt;label&amp;gt;ContentKing CMS Api Key&amp;lt;/label&amp;gt;
                        @Html.EditorFor(x =&amp;gt; Model.CmsApiKey)
                    &amp;lt;/div&amp;gt;
                &amp;lt;/div&amp;gt;
                &amp;lt;div class=&quot;epi-buttonContainer&quot;&amp;gt;
                    &amp;lt;span class=&quot;epi-cmsButton&quot;&amp;gt;
                        &amp;lt;input class=&quot;epi-cmsButton-text epi-cmsButton-tools epi-cmsButton-Save&quot;
                               type=&quot;submit&quot;
                               name=&quot;ctl00$FullRegion$MainRegion$ImportFile&quot;
                               id=&quot;FullRegion_MainRegion_ImportFile&quot;
                               value=&quot;Save&quot;
                               title=&quot;Save&quot;&amp;gt;
                    &amp;lt;/span&amp;gt;
                &amp;lt;/div&amp;gt;
            &amp;lt;/form&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/pre&gt;
&lt;p&gt;The Admin page looks like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://markprins.eu/media/1037/screencap.jpg&quot; alt=&quot;&quot; data-id=&quot;1412&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&#160;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Dynamic Data Store&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;To store the settings I use Episerver Dynamic Data Store (DDS). To store data in the DDS you first need to create a model that Implements IDynamicData&lt;/p&gt;
&lt;pre class=&quot;brush: csharp&quot;&gt;    public class SettingsModel : IDynamicData
    {
        public Identity Id { get; set; }

        public bool TriggerAnalyzeOnPublish { get; set; }

        public string CmsApiKey { get; set; }
    }
&lt;/pre&gt;
&lt;p&gt;The SettingsService reads or writes the settings from and to the DDS.&lt;/p&gt;
&lt;pre class=&quot;brush: csharp&quot;&gt;    [ServiceConfiguration(ServiceType = typeof(ISettingsService), Lifecycle = ServiceInstanceScope.Singleton)]
    public class SettingsService : ISettingsService
    {
        private Guid _settingsId = new Guid(&quot;05663fcf-9f39-4fc2-af49-bddfb76953e0&quot;);

        public SettingsModel GetSettings()
        {
            var store = DynamicDataStoreFactory.Instance.CreateStore(typeof(SettingsModel));
            return store.Items&amp;lt;SettingsModel&amp;gt;().FirstOrDefault(x =&amp;gt; x.Id.ExternalId == _settingsId);
        }

        public void SaveSettings(SettingsModel model)
        {
            model.Id = _settingsId;

            var store = DynamicDataStoreFactory.Instance.CreateStore(typeof(SettingsModel));
            store.Save(model);
        }
    }
&lt;/pre&gt;
&lt;p&gt;This concludes my first ever blog. Let me know what you think or if you have any questions!&lt;/p&gt;</description>            <guid>https://markprins.eu/archive/integrating-contentking-in-episerver/</guid>            <pubDate>Fri, 18 Oct 2019 14:21:24 GMT</pubDate>           <category>Blog post</category></item></channel>
</rss>