Blog posts by Prins Mark2020-05-08T12:05:14.0000000Z/blogs/prins-mark/Optimizely WorldCalculate Tax on a Single Variationhttp://markprins.eu/archive/calculate-tax-on-a-single-variation/2020-05-08T12:05:14.0000000Z<p>On a Episerver Commerce project I'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 '<span>Prices Include Tax</span>' 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)</p>
<p>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't, so you have to create a dummy cart, add an address, and then calculate the tax using this cart.</p>
<p>This might sound daunting, but in fact it is not that complex at all.</p>
<p> </p>
<p>What I ended up with is a HtmlHelper that will get the price including Tax for a variation.</p>
<p> </p>
<pre class="brush: csharp">public static class VariationTaxHelper
{
private static readonly Injected<ICurrentMarket> _currentMarket;
private static readonly Injected<IOrderRepository> _orderRepository;
private static readonly Injected<IOrderGroupFactory> _orderGroupFactory;
private static readonly Injected<ITaxCalculator> _taxCalculator;
private static readonly Injected<IPromotionService> _promotionService;
private static readonly Injected<ICurrencyService> _currencyService;
private static IMarket CurrentMarket => _currentMarket.Service.GetCurrentMarket();
public static Money PriceIncludingTax(this HtmlHelper helper, VariationContent variation)
{
// Create a temporary cart
ICart cart = _orderRepository.Service.LoadOrCreateCart<ICart>(
CustomerContext.Current.CurrentContactId, "TempCart", _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);
}
}
</pre>
<p>Since our site is based on <a href="https://github.com/episerver/Foundation">Episerver Foundation</a>, any services you would be missing using this code can be found there.</p>Country VisitorGroup routing in URL domain/visitorgroup/language/ (domain.com/nl/nl/)http://markprins.eu/archive/country-visitorgroup-routing-in-url-domainvisitorgrouplanguage-domaincomnlnl/2019-11-08T09:09:35.0000000Z<p>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.</p>
<p>Most times we talk the client into using the region parts in cultures. So use the <strong>en</strong> neutral culture for global English, the culture <strong>nl-NL</strong> for Dutch content in Dutch and the <strong>nl-BE</strong> culture for Belgian content in Dutch. 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 <strong>de-NL</strong> culture does not exist.</p>
<p>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.</p>
<p>With the help of <a href="http://joelabrahamsson.com/custom-routing-for-episerver-content/">this</a> blog explaining custom routing for Episerver I created my custom segment and a custom visitor group.</p>
<p> </p>
<pre class="brush: csharp"> 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 'visitor group' segment
if (context.LastConsumedFragment is null && IsVisitorGroupSegment(nextValue.Next))
{
context.RemainingPath = nextValue.Remaining;
// Data Token to be used by VisitorGroup Criterion
context.RouteData.DataTokens.Add("visitorgroup", nextValue.Next);
return true;
}
return false;
}
private static bool IsVisitorGroupSegment(string segment)
{
var visitorGroupRepository = ServiceLocator.Current.GetInstance<IVisitorGroupRepository>();
foreach (var vg in visitorGroupRepository.List())
{
foreach (var criterion in vg.Criteria)
{
if (criterion.Model is RouteVisitorGroupSettings model &&
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 = "contextmode";
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<IVisitorGroupRepository>();
var visitorGroupRoleRepository = ServiceLocator.Current.GetInstance<IVisitorGroupRoleRepository>();
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 && visitorGroupRoleRepository.TryGetRole(vg.Name, out var virtualRoleObject))
{
if (virtualRoleObject.IsMatch(user, httpContext))
{
return model.RouteSegment;
}
}
}
}
return null;
}
}
</pre>
<p><em>The custom visitor group:</em></p>
<pre class="brush: csharp"> public class RouteVisitorGroupSettings : CriterionModelBase
{
[Required]
public string RouteSegment { get; set; }
public override ICriterionModel Copy()
{
return ShallowCopy();
}
}
[VisitorGroupCriterion(
Category = "Technical",
DisplayName = "VisitorGroup Routing",
Description = "")]
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("visitorgroup")
&& ((string)routeDataTokens["visitorgroup"]).Equals(Model.RouteSegment, StringComparison.InvariantCultureIgnoreCase);
}
}
</pre>
<p>Use an initializable module to make this work:</p>
<pre class="brush: csharp"> [InitializableModule]
[ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
public class RouteEventInitializationModule : IInitializableModule
{
public void Initialize(InitializationEngine context)
{
var segment = new VisitorGroupSegment("visitorgroup");
var routingParameters = new MapContentRouteParameters()
{
SegmentMappings = new Dictionary<string, ISegment>()
};
routingParameters.SegmentMappings.Add("visitorgroup", segment);
RouteTable.Routes.MapContentRoute(
name: "visitorgroups",
url: "{visitorgroup}/{language}/{node}/{partial}/{action}",
defaults: new { action = "index" },
parameters: routingParameters);
}
public void Uninitialize(InitializationEngine context)
{
}
}
</pre>
<p>Now by adding this to a default Alloy site and creating two vistorgroups, one with segment ‘nl’ for Netherlands called 'NLD', and one with segment ‘en’ for United Kingdom called 'ENG'. I can display different content using personalization by visitor groups.</p>
<p> </p>
<p><img style="width: 500px; height: 202.0497803806735px;" src="http://markprins.eu/media/1038/cms.jpg?width=500&height=202.0497803806735" alt="" data-udi="umb://media/fb1a1c001ba24490a69f8392e0f85493" /></p>
<p> </p>
<table border="0" style="height: 63px;">
<tbody>
<tr style="height: 42px;">
<td style="height: 42px;">http://localhost:23529/</td>
<td style="height: 42px;">http://localhost:23529/en <br />http://localhost:23529/en/en</td>
<td style="height: 42px;">http://localhost:23529/nl <br />http://localhost:23529/nl/en</td>
</tr>
<tr style="height: 21px;">
<td style="height: 21px;"><img style="width: 330px; height: 200px;" src="http://markprins.eu/media/1039/gobal.jpg?width=330&height=200&mode=max" alt="" data-udi="umb://media/8bf98a33a1454fedb35bb1532dee2270" /></td>
<td style="height: 21px;"><img style="width: 330px; height: 200px;" src="http://markprins.eu/media/1040/vg-en.jpg?width=330&height=200&mode=max" alt="" data-udi="umb://media/aa88250c67e94c84940f2ee43eca12c8" /></td>
<td style="height: 21px;"><img style="width: 330px; height: 200px;" src="http://markprins.eu/media/1041/vg-nl.jpg?width=330&height=200&mode=max" alt="" data-udi="umb://media/e15899b769c14f3bb89fba77b07ac608" /></td>
</tr>
</tbody>
</table>
<p>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.</p>
<p>Let me know what you think or if you have any questions!</p>
<p> </p>
<p> </p>Country VisitorGroup routing in URL domain/visitorgroup/language/ (domain.com/nl/nl/)https://markprins.eu/archive/country-visitorgroup-routing-in-url-domainvisitorgrouplanguage-domaincomnlnl/2019-11-08T08:09:35.0000000Z<p>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.</p>
<p>Most times we talk the client into using the region parts in cultures. So use the <strong>en</strong> neutral culture for global English, the culture <strong>nl-NL</strong> for Dutch content in Dutch and the <strong>nl-BE</strong> culture for Belgian content in Dutch. 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 <strong>de-NL</strong> culture does not exist.</p>
<p>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.</p>
<p>With the help of <a href="http://joelabrahamsson.com/custom-routing-for-episerver-content/">this</a> blog explaining custom routing for Episerver I created my custom segment and a custom visitor group.</p>
<p> </p>
<pre class="brush: csharp"> 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 'visitor group' segment
if (context.LastConsumedFragment is null && IsVisitorGroupSegment(nextValue.Next))
{
context.RemainingPath = nextValue.Remaining;
// Data Token to be used by VisitorGroup Criterion
context.RouteData.DataTokens.Add("visitorgroup", nextValue.Next);
return true;
}
return false;
}
private static bool IsVisitorGroupSegment(string segment)
{
var visitorGroupRepository = ServiceLocator.Current.GetInstance<IVisitorGroupRepository>();
foreach (var vg in visitorGroupRepository.List())
{
foreach (var criterion in vg.Criteria)
{
if (criterion.Model is RouteVisitorGroupSettings model &&
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 = "contextmode";
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<IVisitorGroupRepository>();
var visitorGroupRoleRepository = ServiceLocator.Current.GetInstance<IVisitorGroupRoleRepository>();
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 && visitorGroupRoleRepository.TryGetRole(vg.Name, out var virtualRoleObject))
{
if (virtualRoleObject.IsMatch(user, httpContext))
{
return model.RouteSegment;
}
}
}
}
return null;
}
}
</pre>
<p><em>The custom visitor group:</em></p>
<pre class="brush: csharp"> public class RouteVisitorGroupSettings : CriterionModelBase
{
[Required]
public string RouteSegment { get; set; }
public override ICriterionModel Copy()
{
return ShallowCopy();
}
}
[VisitorGroupCriterion(
Category = "Technical",
DisplayName = "VisitorGroup Routing",
Description = "")]
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("visitorgroup")
&& ((string)routeDataTokens["visitorgroup"]).Equals(Model.RouteSegment, StringComparison.InvariantCultureIgnoreCase);
}
}
</pre>
<p>Use an initializable module to make this work:</p>
<pre class="brush: csharp"> [InitializableModule]
[ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
public class RouteEventInitializationModule : IInitializableModule
{
public void Initialize(InitializationEngine context)
{
var segment = new VisitorGroupSegment("visitorgroup");
var routingParameters = new MapContentRouteParameters()
{
SegmentMappings = new Dictionary<string, ISegment>()
};
routingParameters.SegmentMappings.Add("visitorgroup", segment);
RouteTable.Routes.MapContentRoute(
name: "visitorgroups",
url: "{visitorgroup}/{language}/{node}/{partial}/{action}",
defaults: new { action = "index" },
parameters: routingParameters);
}
public void Uninitialize(InitializationEngine context)
{
}
}
</pre>
<p>Now by adding this to a default Alloy site and creating two vistorgroups, one with segment ‘nl’ for Netherlands called 'NLD', and one with segment ‘en’ for United Kingdom called 'ENG'. I can display different content using personalization by visitor groups.</p>
<p> </p>
<p><img style="width: 500px; height: 202.0497803806735px;" src="https://markprins.eu/media/1038/cms.jpg?width=500&height=202.0497803806735" alt="" data-udi="umb://media/fb1a1c001ba24490a69f8392e0f85493" /></p>
<p> </p>
<table border="0" style="height: 63px;">
<tbody>
<tr style="height: 42px;">
<td style="height: 42px;">http://localhost:23529/</td>
<td style="height: 42px;">http://localhost:23529/en <br />http://localhost:23529/en/en</td>
<td style="height: 42px;">http://localhost:23529/nl <br />http://localhost:23529/nl/en</td>
</tr>
<tr style="height: 21px;">
<td style="height: 21px;"><img style="width: 330px; height: 200px;" src="https://markprins.eu/media/1039/gobal.jpg?width=330&height=200&mode=max" alt="" data-udi="umb://media/8bf98a33a1454fedb35bb1532dee2270" /></td>
<td style="height: 21px;"><img style="width: 330px; height: 200px;" src="https://markprins.eu/media/1040/vg-en.jpg?width=330&height=200&mode=max" alt="" data-udi="umb://media/aa88250c67e94c84940f2ee43eca12c8" /></td>
<td style="height: 21px;"><img style="width: 330px; height: 200px;" src="https://markprins.eu/media/1041/vg-nl.jpg?width=330&height=200&mode=max" alt="" data-udi="umb://media/e15899b769c14f3bb89fba77b07ac608" /></td>
</tr>
</tbody>
</table>
<p>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.</p>
<p>Let me know what you think or if you have any questions!</p>
<p> </p>
<p> </p>Country VisitorGroup routing in URL domain/visitorgroup/language/ (domain.com/nl/nl/)http://markprins.eu/archive/country-visitorgroup-routing-in-url-domainvisitorgrouplanguage-domaincomnlnl/2019-11-08T08:09:35.0000000Z<p>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.</p>
<p>Most times we talk the client into using the region parts in cultures. So use the <strong>en</strong> neutral culture for global English, the culture <strong>nl-NL</strong> for Dutch content in Dutch and the <strong>nl-BE</strong> culture for Belgian content in Dutch. 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 <strong>de-NL</strong> culture does not exist.</p>
<p>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.</p>
<p>With the help of <a href="http://joelabrahamsson.com/custom-routing-for-episerver-content/">this</a> blog explaining custom routing for Episerver I created my custom segment and a custom visitor group.</p>
<p> </p>
<pre class="brush: csharp"> 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 'visitor group' segment
if (context.LastConsumedFragment is null && IsVisitorGroupSegment(nextValue.Next))
{
context.RemainingPath = nextValue.Remaining;
// Data Token to be used by VisitorGroup Criterion
context.RouteData.DataTokens.Add("visitorgroup", nextValue.Next);
return true;
}
return false;
}
private static bool IsVisitorGroupSegment(string segment)
{
var visitorGroupRepository = ServiceLocator.Current.GetInstance<IVisitorGroupRepository>();
foreach (var vg in visitorGroupRepository.List())
{
foreach (var criterion in vg.Criteria)
{
if (criterion.Model is RouteVisitorGroupSettings model &&
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 = "contextmode";
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<IVisitorGroupRepository>();
var visitorGroupRoleRepository = ServiceLocator.Current.GetInstance<IVisitorGroupRoleRepository>();
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 && visitorGroupRoleRepository.TryGetRole(vg.Name, out var virtualRoleObject))
{
if (virtualRoleObject.IsMatch(user, httpContext))
{
return model.RouteSegment;
}
}
}
}
return null;
}
}
</pre>
<p><em>The custom visitor group:</em></p>
<pre class="brush: csharp"> public class RouteVisitorGroupSettings : CriterionModelBase
{
[Required]
public string RouteSegment { get; set; }
public override ICriterionModel Copy()
{
return ShallowCopy();
}
}
[VisitorGroupCriterion(
Category = "Technical",
DisplayName = "VisitorGroup Routing",
Description = "")]
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("visitorgroup")
&& ((string)routeDataTokens["visitorgroup"]).Equals(Model.RouteSegment, StringComparison.InvariantCultureIgnoreCase);
}
}
</pre>
<p>Use an initializable module to make this work:</p>
<pre class="brush: csharp"> [InitializableModule]
[ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
public class RouteEventInitializationModule : IInitializableModule
{
public void Initialize(InitializationEngine context)
{
var segment = new VisitorGroupSegment("visitorgroup");
var routingParameters = new MapContentRouteParameters()
{
SegmentMappings = new Dictionary<string, ISegment>()
};
routingParameters.SegmentMappings.Add("visitorgroup", segment);
RouteTable.Routes.MapContentRoute(
name: "visitorgroups",
url: "{visitorgroup}/{language}/{node}/{partial}/{action}",
defaults: new { action = "index" },
parameters: routingParameters);
}
public void Uninitialize(InitializationEngine context)
{
}
}
</pre>
<p>Now by adding this to a default Alloy site and creating two vistorgroups, one with segment ‘nl’ for Netherlands called 'NLD', and one with segment ‘en’ for United Kingdom called 'ENG'. I can display different content using personalization by visitor groups.</p>
<p> </p>
<p><img style="width: 500px; height: 202.0497803806735px;" src="http://markprins.eu/media/1038/cms.jpg?width=500&height=202.0497803806735" alt="" data-udi="umb://media/fb1a1c001ba24490a69f8392e0f85493" /></p>
<p> </p>
<table border="0" style="height: 63px;">
<tbody>
<tr style="height: 42px;">
<td style="height: 42px;">http://localhost:23529/</td>
<td style="height: 42px;">http://localhost:23529/en <br />http://localhost:23529/en/en</td>
<td style="height: 42px;">http://localhost:23529/nl <br />http://localhost:23529/nl/en</td>
</tr>
<tr style="height: 21px;">
<td style="height: 21px;"><img style="width: 330px; height: 200px;" src="http://markprins.eu/media/1039/gobal.jpg?width=330&height=200&mode=max" alt="" data-udi="umb://media/8bf98a33a1454fedb35bb1532dee2270" /></td>
<td style="height: 21px;"><img style="width: 330px; height: 200px;" src="http://markprins.eu/media/1040/vg-en.jpg?width=330&height=200&mode=max" alt="" data-udi="umb://media/aa88250c67e94c84940f2ee43eca12c8" /></td>
<td style="height: 21px;"><img style="width: 330px; height: 200px;" src="http://markprins.eu/media/1041/vg-nl.jpg?width=330&height=200&mode=max" alt="" data-udi="umb://media/e15899b769c14f3bb89fba77b07ac608" /></td>
</tr>
</tbody>
</table>
<p>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.</p>
<p>Let me know what you think or if you have any questions!</p>
<p> </p>
<p> </p>Integrating ContentKing in Episerverhttp://markprins.eu/archive/integrating-contentking-in-episerver/2019-10-18T16:21:24.0000000Z<p>Ever since a client of the company I work at came up with the SEO tool <a href="https://www.contentkingapp.com/">ContentKing </a>we have been using this tool to monitor the SEO of multiple sites.</p>
<p>ContentKing uses real-time auditing and content tracking to define a SEO score for each page in a website.</p>
<p>Recently, I found out about the ContentKing <a href="https://www.contentkingapp.com/support/cms-api/">CMS API</a> 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.</p>
<p>In this blog I will explain how I integrated the CMS API in an episerver project.</p>
<p> </p>
<p><strong>Publish event </strong></p>
<p>Firstly I needed to hook up to the Episerver publish event. This I did with an Initializable Module.</p>
<p> </p>
<pre class="brush: csharp"> [InitializableModule]
[ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
public class PublishEventInitializationModule : IInitializableModule
{
public void Initialize(InitializationEngine context)
{
var contentEvents = ServiceLocator.Current.GetInstance<IContentEvents>();
contentEvents.PublishingContent += contentEvents_PublishingContent;
}
public void Uninitialize(InitializationEngine context)
{
var contentEvents = ServiceLocator.Current.GetInstance<IContentEvents>();
contentEvents.PublishingContent -= contentEvents_PublishingContent;
}
void contentEvents_PublishingContent(object sender, EPiServer.ContentEventArgs e)
{
var contentKingService = ServiceLocator.Current.GetInstance<IContentKingService>();
contentKingService?.TriggerPageAudit(e.Content);
}
}
</pre>
<p> </p>
<p>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.</p>
<p> </p>
<pre class="brush: csharp">[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 = "https://api.contentkingapp.com/";
public ContentKingService()
{
_urlHelper = ServiceLocator.Current.GetInstance<UrlHelper>();
_siteDefResolver = ServiceLocator.Current.GetInstance<ISiteDefinitionResolver>();
_settingsService = ServiceLocator.Current.GetInstance<ISettingsService>();
}
public void TriggerPageAudit(IContent content)
{
var settings = _settingsService.GetSettings();
try
{
if (settings != null &&
settings.TriggerAnalyzeOnPublish &&
!string.IsNullOrEmpty(settings.CmsApiKey))
{
var fullPageUrl = ResolveUrl(content);
var url = EndPointUrl + "v1/check_url";
var requestHeaders = new NameValueCollection { { "Authorization", $"token {settings.CmsApiKey}" } };
var json = JsonConvert.SerializeObject(new
{
url = fullPageUrl
});
var result = PostObject<object>(url, json, requestHeaders);
}
}
catch
{
//ToDo Do something with exceptions..
}
}
public string ResolveUrl(IContent content)
{
var contentUrl = _urlHelper.ContentUrl(content.ContentLink);
if (contentUrl.Contains("//"))
return contentUrl;
return new Uri(_siteDefResolver.GetByContent(content.ContentLink, true, true).SiteUrl, contentUrl).AbsoluteUri;
}
private static T PostObject<T>(string url, string jsonContent, NameValueCollection requestHeaders = null)
{
var httpContent = new StringContent(jsonContent, Encoding.UTF8, "application/json");
using (var client = new HttpClient())
{
if (requestHeaders != null && requestHeaders.Count > 0)
{
foreach (string key in requestHeaders.Keys)
{
client.DefaultRequestHeaders.Add(key, requestHeaders[key]);
}
}
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var httpResponse = client.PostAsync(url, httpContent).Result;
if (!httpResponse.IsSuccessStatusCode)
throw new Exception($"{httpResponse.StatusCode} | {httpResponse.ReasonPhrase}");
return httpResponse.Content.ReadAsAsync<T>().Result;
}
}
}
</pre>
<p> </p>
<p><strong>CMS Admin Page</strong></p>
<p>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.</p>
<p> </p>
<pre class="brush: csharp"> [EPiServer.PlugIn.GuiPlugIn(
Area = EPiServer.PlugIn.PlugInArea.AdminMenu,
Url = "/ContentKingAdmin/Index",
DisplayName = "ContentKing Admin")]
public class ContentKingAdminController : Controller
{
private readonly ISettingsService _settingsService;
public ContentKingAdminController()
{
_settingsService = ServiceLocator.Current.GetInstance<ISettingsService>();
}
public ActionResult Index()
{
var model = _settingsService.GetSettings();
return View(model);
}
[System.Web.Mvc.HttpPost]
public ActionResult Save(SettingsModel model)
{
_settingsService.SaveSettings(model);
return RedirectToAction("Index");
}
}
</pre>
<p>Then I created a view to create a ui for the settings. (I stole some styling from another episerver admin page)</p>
<pre class="highlight: [5, 15]; html-script: true">@{
Layout = null;
}
@model AlloyContentKing.Models.SettingsModel
<!DOCTYPE html>
<html>
<head>
<title></title>
<!-- Mimic Internet Explorer 7 -->
<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7">
<link rel="stylesheet" type="text/css" href="/EPiServer/Shell/11.19.1/ClientResources/epi/themes/legacy/ShellCore.css">
<script type="text/javascript" src="/EPiServer/Shell/11.19.1/ClientResources/ShellCore.js"></script>
<link rel="stylesheet" type="text/css" href="/EPiServer/Shell/11.19.1/ClientResources/epi/themes/legacy/ShellCoreLightTheme.css">
<script type="text/javascript" src="/EPiServer/CMS/11.19.1/ClientResources/ReportCenter/ReportCenter.js"></script>
<link href="../../../App_Themes/Default/Styles/system.css" type="text/css" rel="stylesheet">
<link href="../../../App_Themes/Default/Styles/ToolButton.css" type="text/css" rel="stylesheet">
</head>
<body>
<div class="epi-contentContainer epi-padding">
<div class="epi-contentArea">
<h1 class="EP-prefix">ContentKing Settings</h1>
<p class="EP-systemInfo">
Settings for ContentKing module.
</p>
</div>
<div class="epi-formArea">
<form id="cmsApiSettings" method="POST" action="\ContentKingAdmin\Save">
<strong>ContentKing CMS Api</strong>
<div class="epi-size20">
<div>
<label>Trigger Page Analize on Publish</label>
@Html.EditorFor(x => Model.TriggerAnalyzeOnPublish)
</div>
<div>
<label>ContentKing CMS Api Key</label>
@Html.EditorFor(x => Model.CmsApiKey)
</div>
</div>
<div class="epi-buttonContainer">
<span class="epi-cmsButton">
<input class="epi-cmsButton-text epi-cmsButton-tools epi-cmsButton-Save"
type="submit"
name="ctl00$FullRegion$MainRegion$ImportFile"
id="FullRegion_MainRegion_ImportFile"
value="Save"
title="Save">
</span>
</div>
</form>
</div>
</div>
</body>
</html>
</pre>
<p>The Admin page looks like this:</p>
<p><img src="http://markprins.eu/media/1037/screencap.jpg" alt="" data-id="1412" /></p>
<p> </p>
<p><strong>Dynamic Data Store</strong></p>
<p>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</p>
<pre class="brush: csharp"> public class SettingsModel : IDynamicData
{
public Identity Id { get; set; }
public bool TriggerAnalyzeOnPublish { get; set; }
public string CmsApiKey { get; set; }
}
</pre>
<p>The SettingsService reads or writes the settings from and to the DDS.</p>
<pre class="brush: csharp"> [ServiceConfiguration(ServiceType = typeof(ISettingsService), Lifecycle = ServiceInstanceScope.Singleton)]
public class SettingsService : ISettingsService
{
private Guid _settingsId = new Guid("05663fcf-9f39-4fc2-af49-bddfb76953e0");
public SettingsModel GetSettings()
{
var store = DynamicDataStoreFactory.Instance.CreateStore(typeof(SettingsModel));
return store.Items<SettingsModel>().FirstOrDefault(x => x.Id.ExternalId == _settingsId);
}
public void SaveSettings(SettingsModel model)
{
model.Id = _settingsId;
var store = DynamicDataStoreFactory.Instance.CreateStore(typeof(SettingsModel));
store.Save(model);
}
}
</pre>
<p>This concludes my first ever blog. Let me know what you think or if you have any questions!</p>Integrating ContentKing in Episerverhttps://markprins.eu/archive/integrating-contentking-in-episerver/2019-10-18T14:21:24.0000000Z<p>Ever since a client of the company I work at came up with the SEO tool <a href="https://www.contentkingapp.com/">ContentKing </a>we have been using this tool to monitor the SEO of multiple sites.</p>
<p>ContentKing uses real-time auditing and content tracking to define a SEO score for each page in a website.</p>
<p>Recently, I found out about the ContentKing <a href="https://www.contentkingapp.com/support/cms-api/">CMS API</a> 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.</p>
<p>In this blog I will explain how I integrated the CMS API in an episerver project.</p>
<p> </p>
<p><strong>Publish event </strong></p>
<p>Firstly I needed to hook up to the Episerver publish event. This I did with an Initializable Module.</p>
<p> </p>
<pre class="brush: csharp"> [InitializableModule]
[ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
public class PublishEventInitializationModule : IInitializableModule
{
public void Initialize(InitializationEngine context)
{
var contentEvents = ServiceLocator.Current.GetInstance<IContentEvents>();
contentEvents.PublishingContent += contentEvents_PublishingContent;
}
public void Uninitialize(InitializationEngine context)
{
var contentEvents = ServiceLocator.Current.GetInstance<IContentEvents>();
contentEvents.PublishingContent -= contentEvents_PublishingContent;
}
void contentEvents_PublishingContent(object sender, EPiServer.ContentEventArgs e)
{
var contentKingService = ServiceLocator.Current.GetInstance<IContentKingService>();
contentKingService?.TriggerPageAudit(e.Content);
}
}
</pre>
<p> </p>
<p>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.</p>
<p> </p>
<pre class="brush: csharp">[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 = "https://api.contentkingapp.com/";
public ContentKingService()
{
_urlHelper = ServiceLocator.Current.GetInstance<UrlHelper>();
_siteDefResolver = ServiceLocator.Current.GetInstance<ISiteDefinitionResolver>();
_settingsService = ServiceLocator.Current.GetInstance<ISettingsService>();
}
public void TriggerPageAudit(IContent content)
{
var settings = _settingsService.GetSettings();
try
{
if (settings != null &&
settings.TriggerAnalyzeOnPublish &&
!string.IsNullOrEmpty(settings.CmsApiKey))
{
var fullPageUrl = ResolveUrl(content);
var url = EndPointUrl + "v1/check_url";
var requestHeaders = new NameValueCollection { { "Authorization", $"token {settings.CmsApiKey}" } };
var json = JsonConvert.SerializeObject(new
{
url = fullPageUrl
});
var result = PostObject<object>(url, json, requestHeaders);
}
}
catch
{
//ToDo Do something with exceptions..
}
}
public string ResolveUrl(IContent content)
{
var contentUrl = _urlHelper.ContentUrl(content.ContentLink);
if (contentUrl.Contains("//"))
return contentUrl;
return new Uri(_siteDefResolver.GetByContent(content.ContentLink, true, true).SiteUrl, contentUrl).AbsoluteUri;
}
private static T PostObject<T>(string url, string jsonContent, NameValueCollection requestHeaders = null)
{
var httpContent = new StringContent(jsonContent, Encoding.UTF8, "application/json");
using (var client = new HttpClient())
{
if (requestHeaders != null && requestHeaders.Count > 0)
{
foreach (string key in requestHeaders.Keys)
{
client.DefaultRequestHeaders.Add(key, requestHeaders[key]);
}
}
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var httpResponse = client.PostAsync(url, httpContent).Result;
if (!httpResponse.IsSuccessStatusCode)
throw new Exception($"{httpResponse.StatusCode} | {httpResponse.ReasonPhrase}");
return httpResponse.Content.ReadAsAsync<T>().Result;
}
}
}
</pre>
<p> </p>
<p><strong>CMS Admin Page</strong></p>
<p>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.</p>
<p> </p>
<pre class="brush: csharp"> [EPiServer.PlugIn.GuiPlugIn(
Area = EPiServer.PlugIn.PlugInArea.AdminMenu,
Url = "/ContentKingAdmin/Index",
DisplayName = "ContentKing Admin")]
public class ContentKingAdminController : Controller
{
private readonly ISettingsService _settingsService;
public ContentKingAdminController()
{
_settingsService = ServiceLocator.Current.GetInstance<ISettingsService>();
}
public ActionResult Index()
{
var model = _settingsService.GetSettings();
return View(model);
}
[System.Web.Mvc.HttpPost]
public ActionResult Save(SettingsModel model)
{
_settingsService.SaveSettings(model);
return RedirectToAction("Index");
}
}
</pre>
<p>Then I created a view to create a ui for the settings. (I stole some styling from another episerver admin page)</p>
<pre class="highlight: [5, 15]; html-script: true">@{
Layout = null;
}
@model AlloyContentKing.Models.SettingsModel
<!DOCTYPE html>
<html>
<head>
<title></title>
<!-- Mimic Internet Explorer 7 -->
<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7">
<link rel="stylesheet" type="text/css" href="/EPiServer/Shell/11.19.1/ClientResources/epi/themes/legacy/ShellCore.css">
<script type="text/javascript" src="/EPiServer/Shell/11.19.1/ClientResources/ShellCore.js"></script>
<link rel="stylesheet" type="text/css" href="/EPiServer/Shell/11.19.1/ClientResources/epi/themes/legacy/ShellCoreLightTheme.css">
<script type="text/javascript" src="/EPiServer/CMS/11.19.1/ClientResources/ReportCenter/ReportCenter.js"></script>
<link href="../../../App_Themes/Default/Styles/system.css" type="text/css" rel="stylesheet">
<link href="../../../App_Themes/Default/Styles/ToolButton.css" type="text/css" rel="stylesheet">
</head>
<body>
<div class="epi-contentContainer epi-padding">
<div class="epi-contentArea">
<h1 class="EP-prefix">ContentKing Settings</h1>
<p class="EP-systemInfo">
Settings for ContentKing module.
</p>
</div>
<div class="epi-formArea">
<form id="cmsApiSettings" method="POST" action="\ContentKingAdmin\Save">
<strong>ContentKing CMS Api</strong>
<div class="epi-size20">
<div>
<label>Trigger Page Analize on Publish</label>
@Html.EditorFor(x => Model.TriggerAnalyzeOnPublish)
</div>
<div>
<label>ContentKing CMS Api Key</label>
@Html.EditorFor(x => Model.CmsApiKey)
</div>
</div>
<div class="epi-buttonContainer">
<span class="epi-cmsButton">
<input class="epi-cmsButton-text epi-cmsButton-tools epi-cmsButton-Save"
type="submit"
name="ctl00$FullRegion$MainRegion$ImportFile"
id="FullRegion_MainRegion_ImportFile"
value="Save"
title="Save">
</span>
</div>
</form>
</div>
</div>
</body>
</html>
</pre>
<p>The Admin page looks like this:</p>
<p><img src="https://markprins.eu/media/1037/screencap.jpg" alt="" data-id="1412" /></p>
<p> </p>
<p><strong>Dynamic Data Store</strong></p>
<p>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</p>
<pre class="brush: csharp"> public class SettingsModel : IDynamicData
{
public Identity Id { get; set; }
public bool TriggerAnalyzeOnPublish { get; set; }
public string CmsApiKey { get; set; }
}
</pre>
<p>The SettingsService reads or writes the settings from and to the DDS.</p>
<pre class="brush: csharp"> [ServiceConfiguration(ServiceType = typeof(ISettingsService), Lifecycle = ServiceInstanceScope.Singleton)]
public class SettingsService : ISettingsService
{
private Guid _settingsId = new Guid("05663fcf-9f39-4fc2-af49-bddfb76953e0");
public SettingsModel GetSettings()
{
var store = DynamicDataStoreFactory.Instance.CreateStore(typeof(SettingsModel));
return store.Items<SettingsModel>().FirstOrDefault(x => x.Id.ExternalId == _settingsId);
}
public void SaveSettings(SettingsModel model)
{
model.Id = _settingsId;
var store = DynamicDataStoreFactory.Instance.CreateStore(typeof(SettingsModel));
store.Save(model);
}
}
</pre>
<p>This concludes my first ever blog. Let me know what you think or if you have any questions!</p>