November Happy Hour will be moved to Thursday December 5th.

Reimond Checiches
Nov 14, 2017
  7653
(6 votes)

Episerver CMS and Commerce custom routing

Description

We needed to implement and Episerver Website that could be customized for multiple stakeholders. Each stakeholder has his own Personal Website URL, containing a Site Name segment. Because the number of the stakeholders was more than 10000, in order to reuse the Episerver content and do not create personalized content for each stakeholder, we decided to customize Episerver routing for both CMS and Commerce content.

Example

Consider that you have the next page tree in Episerver CMS for your stakeholders’ pages:

Image EditCmsTree.png

Please see the next table for exemplifying how Episerver does the default routing and how our custom routing needs to work. The same page should be retrieved for each stakeholder, but with that stakeholder segment in the URL.

Page Name

Episerver Default URL

Custom Routing URLs

Stakeholder Home

http://quicksilver.localtest.me/en/stakeholder-home/

http://quicksilver.localtest.me/en/stakeholder1/

 

http://quicksilver.localtest.me/en/stakeholder2/

Become A Stakeholder

http://quicksilver.localtest.me/en/stakeholder-home/become-a-stakeholder/

http://quicksilver.localtest.me/en/stakeholder1/become-a-stakeholder/

 

http://quicksilver.localtest.me/en/stakeholder2/become-a-stakeholder/

About Me

http://quicksilver.localtest.me/en/stakeholder-home/about-me/

http://quicksilver.localtest.me/en/ stakeholder1/about-me/

 

http://quicksilver.localtest.me/en/ stakeholder2/about-me/

Shop Now

http://quicksilver.localtest.me/en/stakeholder-home/shop-now/

http://quicksilver.localtest.me/en/ stakeholder1/shop-now/

 

http://quicksilver.localtest.me/en/ stakeholder2/shop-now/

Shopping Cart

http://quicksilver.localtest.me/en/stakeholder-home/shopping-cart/

http://quicksilver.localtest.me/en/ stakeholder1/shopping-cart/

 

http://quicksilver.localtest.me/en/ stakeholder2/shopping-cart/

Checkout

http://quicksilver.localtest.me/en/stakeholder-home/checkout/

http://quicksilver.localtest.me/en/ stakeholder1/checkout/

 

http://quicksilver.localtest.me/en/ stakeholder2/checkout/

For Commerce, the routing is different, because we need to insert the stakeholder segment instead of replacing an existing one, as it is for CMS. Please see below some example of routing for several Catalog items URLs:

Catalog Content Type

Episerver Default URL

Custom Routing URLs

Category

http://quicksilver.localtest.me/en/fashion/mens/

http://quicksilver.localtest.me/en/stakeholder1/fashion/mens/

 

http://quicksilver.localtest.me/en/stakeholder2/fashion/mens/

Subcategory

http://quicksilver.localtest.me/en/fashion/mens/mens-shoes/

http://quicksilver.localtest.me/en/stakeholder1/fashion/mens/mens-shoes/

http://quicksilver.localtest.me/en/stakeholder2/fashion/mens/mens-shoes/

Product

http://quicksilver.localtest.me/en/fashion/mens/mens-shoes/p-36127195/

http://quicksilver.localtest.me/en/stakeholder1/fashion/mens/mens-shoes/p-36127195/

http://quicksilver.localtest.me/en/stakeholder2/fashion/mens/mens-shoes/p-36127195/

Implementation

In this section, I will describe the approach that we choose and implementation details for both CMS and Commerce content custom routing.

  1. CMS Content custom routing

    In order to customize default Episerver content routing we need to implement a custom partial router, which implements the public interface IPartialRouter<TContent, TRoutedData>. Please see the next references for details about the IPartialRouter interface and its methods: IPartialRouter Interface, RoutePartial Method and GetPartialVirtualPath Method.

    1.	/*  This custom routing code will be called just for the pages  
    2.	    which are under StartPage into the CMS page tree  
    3.	    and have page type StakeholderBasePage*/  
    4.	public class CustomCmsContentPartialRouter : IPartialRouter<StartPage, StakeholderBasePage>  
    5.	    {  
    6.	        private readonly IContentLoader _contentLoader = ServiceLocator.Current.GetInstance<IContentLoader>();  
    7.	  
    8.	        public object RoutePartial(StartPage content, SegmentContext segmentContext)  
    9.	        {  
    10.	            string stakeholderSiteName;  
    11.	            // All the stakeholder pages will inherite from the same base page  
    12.	            StakeholderBasePage stakeholderPage = null;  
    13.	            var contentUrl = CustomRoutingHelper.GetStakeholderContentUrl(segmentContext, out stakeholderSiteName);  
    14.	            var contentReference = CustomRoutingHelper.TryGetContentRefrence(contentUrl);  
    15.	  
    16.	            if (contentReference != null)  
    17.	            {  
    18.	                if (CustomRoutingHelper.CheckIfCustomSiteName(segmentContext, stakeholderSiteName))  
    19.	                {  
    20.	                    stakeholderPage = _contentLoader.Get<StakeholderBasePage>(contentReference.ContentLink);  
    21.	  
    22.	                    if (stakeholderPage != null)  
    23.	                    {  
    24.	                        segmentContext.RemainingPath = string.Empty;  
    25.	                        segmentContext.RoutedContentLink = stakeholderPage.ContentLink;  
    26.	                    }  
    27.	                }  
    28.	            }  
    29.	  
    30.	            return stakeholderPage;  
    31.	        }  
    32.	  
    33.	        public PartialRouteData GetPartialVirtualPath(JewelerBasePage content, string language, RouteValueDictionary routeValues,  
    34.	            RequestContext requestContext)  
    35.	        {  
    36.	            string urlSegment;  
    37.	            string stakeholderSiteName;  
    38.	              
    39.	            if (CustomRoutingHelper.CheckIfCustomSiteName(requestContext, out stakeholderSiteName))  
    40.	            {  
    41.	                urlSegment = GetRoutedContentLink(content, stakeholderSiteName);  
    42.	            }  
    43.	            else  
    44.	            {  
    45.	                return null;  
    46.	            }  
    47.	  
    48.	            return new PartialRouteData  
    49.	            {  
    50.	                BasePathRoot = ContentReference.StartPage,  
    51.	                PartialVirtualPath = urlSegment  
    52.	            };  
    53.	        }  
    54.	  
    55.	        private string GetRoutedContentLink(StakeholderBasePage content, string stakeholderSiteName)  
    56.	        {  
    57.	            /*  Stop iteration if content type is StakeholderStartPage. 
    58.	                All pages with type StakeholderBasePage should be under  
    59.	                StakeholderStartPage into CMS page tree */  
    60.	            if (content is StakeholderStartPage)  
    61.	                return stakeholderSiteName;  
    62.	  
    63.	            var parentContent = _contentLoader.Get<StakeholderBasePage>(content.ParentLink);  
    64.	  
    65.	            return GetRoutedContentLink(parentContent, stakeholderSiteName) + "/" + content.URLSegment;  
    66.	        }  
    67.	    }
    
  2. Commerce/Catalog Content custom routing

    In order to customize default Episerver content routing we need to implement a custom partial router, which extends the public class HierarchicalCatalogPartialRouter. Please see the next references for details about the HierarchicalCatalogPartialRouter class and  the overrode methods: HierarchicalCatalogPartialRouter Class, FindNextContentInSegmentPair Method and GetPartialVirtualPath Method.

    1.	public class CustomCommerceCatalogRouter : HierarchicalCatalogPartialRouter  
    2.	{  
    3.	    public CustomCommerceCatalogRouter(CatalogContentBase commerceRoot, Func<ContentReference> func)  
    4.	    : base(func, commerceRoot, false) { }  
    5.	  
    6.	    protected override CatalogContentBase FindNextContentInSegmentPair(CatalogContentBase catalogContent, SegmentPair segmentPair, SegmentContext segmentContext, CultureInfo cultureInfo)  
    7.	    {  
    8.	  
    9.	        if (!CustomRoutingHelper.CheckIfCatalogContentUrl(segmentContext.RequestUrl.ToString()))  
    10.	            return null;  
    11.	  
    12.	        CustomRoutingHelper.SkipCustomSiteNameSegment(segmentPair, segmentContext.RequestUrl.ToString());  
    13.	        var currentCatalog = base.FindNextContentInSegmentPair(catalogContent, segmentPair, segmentContext, cultureInfo);  
    14.	  
    15.	        return currentCatalog;  
    16.	    }  
    17.	  
    18.	    public override PartialRouteData GetPartialVirtualPath(CatalogContentBase content, string language, RouteValueDictionary routeValues,  
    19.	        RequestContext requestContext)  
    20.	    {  
    21.	        string stakeholderSiteName;  
    22.	  
    23.	        var partialRouteData = base.GetPartialVirtualPath(content, language, routeValues, requestContext);  
    24.	  
    25.	        if (CustomRoutingHelper.CheckIfCustomSiteName(requestContext, out stakeholderSiteName))  
    26.	        {  
    27.	            partialRouteData.PartialVirtualPath = stakeholderSiteName + "/" + partialRouteData.PartialVirtualPath;  
    28.	        }  
    29.	  
    30.	        return partialRouteData;  
    31.	    }  
    32.	}  
    
  3. Common methods from Helpers

    CustomRoutingHelper.GetStakeholderContentUrl method will return the Episerver Content URL. This method will replace stakeholder site name with stakeholder Episerver home page URL, in return in the stakeholderSiteName output parameter the stakeholder custom site name.

    E.g.: For http://website/stakeholder-sitename the method will return http://website/stakeholder-home and stakeholderSiteName = "stakeholder-sitename".

    Please see below the CustomRoutingHelper class:

    1.	public class CustomRoutingHelper  
    2.	{  
    3.	    public static string GetStakeholderContentUrl(SegmentContext segmentContext, out string stakeholderSiteName)  
    4.	    {  
    5.	        var nextSegment = segmentContext.GetNextValue(segmentContext.RemainingPath);  
    6.	  
    7.	        if (CultureHelper.IsAvailableCulture(nextSegment.Next))  
    8.	        {  
    9.	            nextSegment = segmentContext.GetNextValue(nextSegment.Remaining);  
    10.	        }  
    11.	  
    12.	        stakeholderSiteName = nextSegment.Next;  
    13.	        var regex = new Regex(Regex.Escape(nextSegment.Next));  
    14.	        // replace custom site name with CMS stakeholder HomePage/StartPage URL  
    15.	        return regex.Replace(segmentContext.RequestUrl.AbsolutePath, "stakeholder-home", 1);  
    16.	    }  
    17.	  
    18.	    // check if request path cotains a valid stakeholder custom site name  
    19.	    public static bool CheckIfCustomSiteName(RequestContext requestContext, out string stakeholderSiteName)  
    20.	    {  
    21.	        stakeholderSiteName = null;  
    22.	  
    23.	        if (requestContext.HttpContext == null)  
    24.	            return false;  
    25.	  
    26.	        // ajax requests got stakeholder ID in request headers  
    27.	        if (requestContext.HttpContext.Request.IsAjaxRequest())  
    28.	            return false;  
    29.	  
    30.	        var requestPath = requestContext.HttpContext.Request.Path;  
    31.	        var segments = requestPath.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);  
    32.	  
    33.	        if (segments.Length > 0)  
    34.	        {  
    35.	            if (CultureHelper.IsAvailableCulture(segments[0]) && segments.Length > 1)  
    36.	            {  
    37.	                stakeholderSiteName = segments[1];  
    38.	            }  
    39.	            else  
    40.	            {  
    41.	                stakeholderSiteName = segments[0];  
    42.	            }  
    43.	  
    44.	            return CheckIfStakeholderSiteName(stakeholderSiteName, requestPath);  
    45.	        }  
    46.	  
    47.	        return false;  
    48.	    }  
    49.	  
    50.	    public static bool CheckIfCustomSiteName(SegmentContext segmentContext, string stakeholderSiteName)  
    51.	    {  
    52.	        return CheckIfStakeholderSiteName(stakeholderSiteName, segmentContext.RequestUrl.ToString());  
    53.	    }  
    54.	  
    55.	    public static void SkipCustomSiteNameSegment(SegmentPair segmentPair, string requestUrl)  
    56.	    {  
    57.	        var remainingSegments = segmentPair.Remaining.Split('/');  
    58.	  
    59.	        if (remainingSegments.Length <= 0   
    60.	            ||!remainingSegments[0].Equals("DefaultCatalogName", StringComparison.OrdinalIgnoreCase)   
    61.	            || !CheckIfStakeholderSiteName(segmentPair.Next, requestUrl))   
    62.	                return;  
    63.	  
    64.	        segmentPair.Next = remainingSegments[0];  
    65.	        segmentPair.Remaining = segmentPair.Remaining.Remove(0, remainingSegments[0].Length + 1);  
    66.	    }  
    67.	  
    68.	    // check if the segment is a valid stakeholder site name  
    69.	    public static bool CheckIfStakeholderSiteName(string segment, string requestUrl)  
    70.	    {  
    71.	        if (segment.IsNullOrEmpty()) return false;  
    72.	  
    73.	        var key = new Tuple<string, string>(segment.ToLower(), requestUrl);  
    74.	  
    75.	        // avoid extra checks for already checked segments  
    76.	        if (StakeholderSiteNames.ContainsKey(key))  
    77.	            return StakeholderSiteNames[key];  
    78.	  
    79.	        // check for some specific routes to avoid extra call to the database  
    80.	        if (segment.Equals("episerver", StringComparison.OrdinalIgnoreCase)  
    81.	            || segment.Equals("stakeholder-home", StringComparison.OrdinalIgnoreCase)  
    82.	            || segment.Equals("DefaultCatalogName", StringComparison.OrdinalIgnoreCase))  
    83.	            return false;  
    84.	  
    85.	        bool exists = //check if there is a custom sitename in your system equal with the segment   
    86.	  
    87.	        StakeholderSiteNames.Add(key, exists);  
    88.	  
    89.	        return exists;  
    90.	    }  
    91.	  
    92.	    // This dictionary help us to avoid extra checks in the same request  
    93.	    private static Dictionary<Tuple<string, string>, bool> StakeholderSiteNames  
    94.	    {  
    95.	        get  
    96.	        {  
    97.	            if (HttpContext.Current.Items["StakeholderSiteNames"] == null)  
    98.	                HttpContext.Current.Items["StakeholderSiteNames"] = new Dictionary<Tuple<string, string>, bool>();  
    99.	            return HttpContext.Current.Items["StakeholderSiteNames"] as Dictionary<Tuple<string, string>, bool>;  
    100.	        }  
    101.	    }  
    102.	  
    103.	    public static bool CheckIfCatalogContentUrl(string requestUrl)  
    104.	    {  
    105.	        return requestUrl.Contains("DefaultCatalogName");  
    106.	    }  
    107.	  
    108.	    public static IContent TryGetContentRefrence(string url)  
    109.	    {  
    110.	        try  
    111.	        {  
    112.	            return UrlResolver.Current.Route(new UrlBuilder(url));  
    113.	        }  
    114.	        catch  
    115.	        {  
    116.	            return null;  
    117.	        }  
    118.	    }  
    119.	}  
    


    Please see below the CultureHelper class:

    1.	public class CultureHelper  
    2.	    {  
    3.	        public static bool IsAvailableCulture(string cultureCode)  
    4.	        {  
    5.	            var availableLanguages = GetAvailableLanguages();  
    6.	  
    7.	            return availableLanguages.Contains(cultureCode);  
    8.	        }  
    9.	  
    10.	        private static IList<string> GetAvailableLanguages()  
    11.	        {  
    12.	            var languageBranchRepository = ServiceLocator.Current.GetInstance<ILanguageBranchRepository>();  
    13.	            var availableLanguages = languageBranchRepository.ListEnabled();  
    14.	  
    15.	            if (availableLanguages != null && availableLanguages.Any())  
    16.	                return availableLanguages.Select(x => x.URLSegment).ToList();  
    17.	  
    18.	            return new List<string>();  
    19.	        }  
    20.	    }  
    
Nov 14, 2017

Comments

Quan Mai
Quan Mai Nov 14, 2017 10:43 AM

Nice - I haven't jumped into detail, but I appreciate the time and effort you put into write such a long, well formatted post. Keep it up :) 

Please login to comment.
Latest blogs
Optimizely CMS Roadmap

Explore Optimizely CMS's latest roadmap, packed with developer-focused updates. From SaaS speed to Visual Builder enhancements, developer tooling...

Andy Blyth | Nov 21, 2024 | Syndicated blog

Set Default Culture in Optimizely CMS 12

Take control over culture-specific operations like date and time formatting.

Tomas Hensrud Gulla | Nov 15, 2024 | Syndicated blog

I'm running Optimizely CMS on .NET 9!

It works 🎉

Tomas Hensrud Gulla | Nov 12, 2024 | Syndicated blog

Recraft's image generation with AI-Assistant for Optimizely

Recraft V3 model is outperforming all other models in the image generation space and we are happy to share: Recraft's new model is now available fo...

Luc Gosso (MVP) | Nov 8, 2024 | Syndicated blog