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.
Most times we talk the client into using the region parts in cultures. So use the en neutral culture for global English, the culture nl-NL for Dutch content in Dutch and the nl-BE 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 de-NL culture does not exist.
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.
With the help of this blog explaining custom routing for Episerver I created my custom segment and a custom visitor group.
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; } }
The custom visitor group:
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); } }
Use an initializable module to make this work:
[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) { } }
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.
http://localhost:23529/ | http://localhost:23529/en http://localhost:23529/en/en |
http://localhost:23529/nl http://localhost:23529/nl/en |
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.