London Dev Meetup Rescheduled! Due to unavoidable reasons, the event has been moved to 21st May. Speakers remain the same—any changes will be communicated. Seats are limited—register here to secure your spot!

Custom catalog routing

Vote:
 

I'm trying to create some custom routing for the commerce catalog.
I would like all variations to have the following url structure, regardles of where they are in the catalog:

https://sitename/products/variationcode

The products page is the route starting point for the catalog.

To do this I created a custom HierarchicalCatalogPartialRouter:

    public class CustomHierarchicalCatalogPartialRouter : HierarchicalCatalogPartialRouter
    {
        public CustomHierarchicalCatalogPartialRouter(Func routeStartingPoint, CatalogContentBase commerceRoot, bool enableOutgoingSeoUri)
            :base(routeStartingPoint, commerceRoot, enableOutgoingSeoUri)
        {
        }

        public override PartialRouteData GetPartialVirtualPath(CatalogContentBase content, string language, RouteValueDictionary routeValues, RequestContext requestContext)
        {
            var variation = content as VariationContent;
            if (variation == null)
                return base.GetPartialVirtualPath(content, language, routeValues, requestContext);

            return new PartialRouteData
            {
                BasePathRoot = RouteStartingPoint,
                PartialVirtualPath = HttpUtility.UrlPathEncode(variation.Code)
            };
        }

        public override object RoutePartial(PageData content, SegmentContext segmentContext)
        {
            if (!content.ContentLink.CompareToIgnoreWorkID(RouteStartingPoint))
                return base.RoutePartial(content, segmentContext);
            SegmentPair nextValue = segmentContext.GetNextValue(segmentContext.RemainingPath);
            if (string.IsNullOrEmpty(nextValue.Next))
                return null;
            CultureInfo cultureInfo = string.IsNullOrEmpty(segmentContext.Language) ? ContentLanguage.PreferredCulture : CultureInfo.GetCultureInfo(segmentContext.Language);
            CatalogContentBase contentByCode = GetContentByCode(CommerceRoot, nextValue, cultureInfo);
            if (contentByCode != null)
                segmentContext.RoutedContentLink = contentByCode.ContentLink;

            return contentByCode;
        }

        protected CatalogContentBase GetContentByCode(CatalogContentBase catalogContent, SegmentPair segmentPair, CultureInfo cultureInfo)
        {
            var referenceConverter = ServiceLocator.Current.GetInstance();

            var code = segmentPair.Next;
            var variantLink = referenceConverter.GetContentLink(code);

            var repo = ServiceLocator.Current.GetInstance();
            var variant = repo.Get(variantLink, cultureInfo);
            return variant;
       }

    }

The catalog partial router is registered in an initialization module

var hierarchicalCatalogPartialRouter = new CustomHierarchicalCatalogPartialRouter(() => productPage, catalogRoot, false);
RouteTable.Routes.RegisterPartialRouter(hierarchicalCatalogPartialRouter);

The GetPartialVirtualPath method correctly creates the outgoing Url, and the RoutePartial method also finds the correct variation in the catalog.

However I keep getting a 404 when I do a request on https://sitename/products/variationcode

What am I missing here? Any help is very much appreciated.

#180199
Edited, Jul 03, 2017 7:55
Vote:
 

Is there default commerce hierarchical router registered in routes table?

#180413
Jul 09, 2017 17:21
Vote:
 

Hi Valdis,

No there is no default commerce hierachical router registered. 

Only this one.

var hierarchicalCatalogPartialRouter = new CustomHierarchicalCatalogPartialRouter(() => productPage, catalogRoot, false);
RouteTable.Routes.RegisterPartialRouter(hierarchicalCatalogPartialRouter);
#180415
Jul 10, 2017 1:41
Vote:
 

I’m dealing with the same problem as you. If you solved it, can you please share your solution?

#195125
Edited, Jul 14, 2018 6:13
Vote:
 

Have a look at Quan's post: Multiple catalogs with same url

#195127
Jul 14, 2018 10:38
Vote:
 

Just wrote about it in my book https://leanpub.com/epicommercerecipes/read_sample 

Recipe Problem 1.6.5: Url without categories

#195128
Jul 14, 2018 10:38
Vote:
 

Just wrote about it in my book https://leanpub.com/epicommercerecipes/read_sample 

Recipe Problem 1.6.5: Url without categories

#195129
Jul 14, 2018 10:38
Vote:
 

Hi Jeroen and Quan Mai

I am struglling to make it work but its not working for me. Always get a 404 at all

I would have a seo-friendly url with custom segment

for instance:

Orl Url of product: www.domain/niteco/123-furnishings/12320-curtains/1232005-licensednon-branded/123200525-xx-pencil-pleats/vanguard-marco-pencil-pleat-curtains-pair/?variant=a_498676

new url should be www.domain/product/{plu}/{product-name}/?variant=a_498676 (www.domain/product/498676/vanguard-marco-pencil-pleat-curtains-pair/?variant=a_498676)

here is my code

 

public class CustomHierarchicalCatalogPartialRouter : HierarchicalCatalogPartialRouter
 {
     private readonly string CustomRouteSegment = "product/{0}/{1}";
     public CustomHierarchicalCatalogPartialRouter(
     Func<ContentReference> routeStartingPoint,
     CatalogContentBase commerceRoot,
     bool enableOutgoingSeoUri,
     IContentLoader contentLoader,
     IRelationRepository relationRepository) : base(
     routeStartingPoint,
     commerceRoot,
     enableOutgoingSeoUri)
    {
        _contentLoader = contentLoader;
        _relationRepository = relationRepository;
     }


    public override PartialRouteData GetPartialVirtualPath(CatalogContentBase content, string language, RouteValueDictionary routeValues, RequestContext requestContext)
    {
         var product = content as BaseProduct;

         if (product == null) return base.GetPartialVirtualPath(content, language, routeValues, requestContext);
         return new PartialRouteData
         {
             BasePathRoot = RouteStartingPoint,
             PartialVirtualPath =string.Format(CustomRouteSegment, product.PLU, product.Name)
         };
     }

     public override object RoutePartial(PageData content, SegmentContext segmentContext)
    {
        if (!content.ContentLink.CompareToIgnoreWorkID(RouteStartingPoint))
           return base.RoutePartial(content, segmentContext);
           var routed = base.RoutePartial(content, segmentContext);
           var product = routed as FashionProduct;

           segmentContext.RoutedContentLink = product.ContentLink;
           segmentContext.RemainingPath = string.Empty;
           segmentContext.RoutedObject = product;
           return product;
     }

     protected override bool IsValidRoutedContent(CatalogContentBase content)
    {
        //To forbid hierarchical variation url structure:
       if (content is ProductContent) return false;

       return base.IsValidRoutedContent(content);
    }
}
protected override void RegisterRoutes(RouteCollection routes)
{

    var segment = new ParameterSegment("product");
    var routingParameters = new MapContentRouteParameters()
    {
        SegmentMappings = new Dictionary<string, ISegment>()
    };
   routingParameters.SegmentMappings.Add("product", 
   segment);
   RouteTable.Routes.MapContentRoute(
   "product_node",
   "{language}/product/{plu}/{name}/{action}",
   new
   {
       controller = "Product",
       action = "Index",
       language = UrlParameter.Optional,
   },
   routingParameters);
}
#195130
Edited, Jul 14, 2018 11:54
Vote:
 

Hi Thang

Here is the solution I've used in my project. The pattern of product SEO URL is https://domain/product/product-name/product-code

using EPiServer.Commerce.Catalog.ContentTypes;
using EPiServer.Core;
using EPiServer.ServiceLocation;
using EPiServer.Web.Routing;
using EPiServer.Web.Routing.Segments;
using Mediachase.Commerce.Catalog;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Web;
using System.Web.Routing;

namespace EPiServer.Commerce.Routing
{
    public class ProductRouter : HierarchicalCatalogPartialRouter
    {
        protected readonly IContentLoader contentLoader;

        protected readonly ReferenceConverter referenceConverter;

        public ProductRouter(Func<ContentReference> routeStartingPoint, CatalogContentBase commerceRoot, bool enableOutgoingSeoUri) :
            base(routeStartingPoint, commerceRoot, enableOutgoingSeoUri)
        {
            this.contentLoader = ServiceLocator.Current.GetInstance<IContentLoader>();
            this.referenceConverter = ServiceLocator.Current.GetInstance<ReferenceConverter>();
        }

        public override PartialRouteData GetPartialVirtualPath(CatalogContentBase content, string language, RouteValueDictionary routeValues, RequestContext requestContext)
        {
            var product = content as ProductContent;
            if (product == null)
            {
                return base.GetPartialVirtualPath(content, language, routeValues, requestContext);
            }

            return new PartialRouteData
            {
                BasePathRoot = RouteStartingPoint,
                PartialVirtualPath = $"product/{product.Name.Replace(' ', '-')}/{HttpUtility.UrlPathEncode(product.Code)}" // Should move to the ultility class
            };
        }

        protected override CatalogContentBase FindNextContentInSegmentPair(CatalogContentBase catalogContent, SegmentPair segmentPair, SegmentContext segmentContext, CultureInfo cultureInfo)
        {
            while (!string.IsNullOrWhiteSpace(segmentPair.Next) )
            {
                if (catalogContent.ContentType == CatalogContentType.Root)
                {
                    var contentLink = referenceConverter.GetContentLink(segmentPair.Next, CatalogContentType.CatalogEntry);
                    if (!ContentReference.IsNullOrEmpty(contentLink))
                    {
                        return contentLoader.Get<ProductContent>(contentLink);
                    }
                }
                
                segmentPair = segmentContext.GetNextValue(segmentPair.Remaining);
            }

            segmentContext.RemainingPath = string.Empty;

            return null;
        }
    }
}

You should register the router in the initialization module.

RouteCollectionExtensions.RegisterPartialRouter<PageData, CatalogContentBase>(System.Web.Routing.RouteTable.Routes, new ProductRouter(() => SiteDefinition.Current.StartPage, commerceRootContent, false));

Hope this helps!

#195169
Edited, Jul 17, 2018 5:52
Vote:
 

Hi Quan Mai,

I have applied your solution it seems work fine but when i try run the job EPiServer Find Content Indexing Job i get an exception and all product can't index

Have you ever thought of this case yet?

2018-07-18 17:12:27,228 [96] ERROR EPiServer.Find.Cms.ContentIndexer: HC-THAOPHAM: An exception occurred while indexing content [Link 2317__CatalogContent] [GUID 7f18a65d-cf2d-40c5-a58e-040b8e06be07] [Type DiningAndEntertainingProduct] [Name Simon Gault Replacement Seals & Rings Old Fashioned 70mm]: Exception has been thrown by the target of an invocation.
System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation. ---> System.ArgumentNullException: The provided content link does not have a value.
Parameter name: contentLink
at EPiServer.Core.Internal.DefaultContentLoader.Get[T](ContentReference contentLink, LoaderOptions loaderOptions)
at EPiServer.Web.Routing.Segments.Internal.NodeSegment.GetVirtualPathSegment(RequestContext requestContext, RouteValueDictionary values)
at EPiServer.Web.Routing.Segments.Internal.NodeSegment.GetVirtualPathSegment(RequestContext requestContext, RouteValueDictionary values, HashSet`1 usedValues)
at EPiServer.Web.Routing.Internal.DefaultContentRoute.AddVirtualPathFromSegments(StringBuilder virtualPath, RequestContext requestContext, RouteValueDictionary values, HashSet`1 usedValues, Int32 lastNonDefaultIndex)
at EPiServer.Web.Routing.Internal.DefaultContentRoute.GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
at EPiServer.Web.Routing.Internal.DefaultUrlResolver.GetUrlFromRoute(ContentReference contentReference, String language, RouteValueDictionary routeValues, RequestContext requestContext)
at EPiServer.Web.Routing.Internal.DefaultUrlResolver.GetVirtualPath(ContentReference contentLink, String language, VirtualPathArguments arguments)
at EPiServer.Web.Routing.UrlResolver.GetUrl(ContentReference contentLink, String language)
at EPiServer.Find.Commerce.CommerceUnifiedSearchSetUp.GetContentUrl(ContentReference contentLink)
--- End of inner exception stack trace ---
at System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor)
at System.Reflection.RuntimeMethodInfo.UnsafeInvokeInternal(Object obj, Object[] parameters, Object[] arguments)
at System.Delegate.DynamicInvokeImpl(Object[] args)
at EPiServer.Find.UnifiedSearch.IndexProjection.GetUrl(Object o)
at EPiServer.Find.DelegateValueProvider`2.GetValue(Object target)
at Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.CalculatePropertyValues(JsonWriter writer, Object value, JsonContainerContract contract, JsonProperty member, JsonProperty property, JsonContract& memberContract, Object& memberValue)
at Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.SerializeObject(JsonWriter writer, Object value, JsonObjectContract contract, JsonProperty member, JsonContainerContract collectionContract, JsonProperty containerProperty)
at Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.Serialize(JsonWriter jsonWriter, Object value, Type objectType)
at EPiServer.Find.Api.BulkActionConverter.WriteJson(JsonWriter writer, Object value, JsonSerializer serializer)
at Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.SerializeConvertable(JsonWriter writer, JsonConverter converter, Object value, JsonContract contract, JsonContainerContract collectionContract, JsonProperty containerProperty)
at Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.Serialize(JsonWriter jsonWriter, Object value, Type objectType)
at Newtonsoft.Json.JsonSerializer.SerializeInternal(JsonWriter jsonWriter, Object value, Type objectType)
at EPiServer.Find.Json.Serializer.SerializeToTextWriter(JsonSerializer serializer, Object value, TextWriter textWriter)
at EPiServer.Find.Json.Serializer.SerializeObjectsToJsonRequest(JsonSerializer serializer, IJsonRequest jsonRequest, IEnumerable values)
at EPiServer.Find.Api.BulkCommand.Execute(List`1& serializationFailures)
at EPiServer.Find.Api.BulkCommand.Execute()
at EPiServer.Find.Cms.ContentIndexer.IndexWithRetry(IContent[] contents, Int32 maxRetries)
at EPiServer.Find.Cms.ContentIndexer.Index(IEnumerable`1 content, IndexOptions options)
at EPiServer.Find.Cms.ContentIndexer.IndexBatch(IEnumerable`1 content, Action`1 statusAction, Int32& numberOfContentErrors, Int32& indexingCount)
System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation. ---> System.ArgumentNullException: The provided content link does not have a value.
Parameter name: contentLink
at EPiServer.Core.Internal.DefaultContentLoader.Get[T](ContentReference contentLink, LoaderOptions loaderOptions)
at EPiServer.Web.Routing.Segments.Internal.NodeSegment.GetVirtualPathSegment(RequestContext requestContext, RouteValueDictionary values)
at EPiServer.Web.Routing.Segments.Internal.NodeSegment.GetVirtualPathSegment(RequestContext requestContext, RouteValueDictionary values, HashSet`1 usedValues)
at EPiServer.Web.Routing.Internal.DefaultContentRoute.AddVirtualPathFromSegments(StringBuilder virtualPath, RequestContext requestContext, RouteValueDictionary values, HashSet`1 usedValues, Int32 lastNonDefaultIndex)
at EPiServer.Web.Routing.Internal.DefaultContentRoute.GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
at EPiServer.Web.Routing.Internal.DefaultUrlResolver.GetUrlFromRoute(ContentReference contentReference, String language, RouteValueDictionary routeValues, RequestContext requestContext)
at EPiServer.Web.Routing.Internal.DefaultUrlResolver.GetVirtualPath(ContentReference contentLink, String language, VirtualPathArguments arguments)
at EPiServer.Web.Routing.UrlResolver.GetUrl(ContentReference contentLink, String language)
at EPiServer.Find.Commerce.CommerceUnifiedSearchSetUp.GetContentUrl(ContentReference contentLink)
--- End of inner exception stack trace ---
at System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor)
at System.Reflection.RuntimeMethodInfo.UnsafeInvokeInternal(Object obj, Object[] parameters, Object[] arguments)
at System.Delegate.DynamicInvokeImpl(Object[] args)
at EPiServer.Find.UnifiedSearch.IndexProjection.GetUrl(Object o)
at EPiServer.Find.DelegateValueProvider`2.GetValue(Object target)
at Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.CalculatePropertyValues(JsonWriter writer, Object value, JsonContainerContract contract, JsonProperty member, JsonProperty property, JsonContract& memberContract, Object& memberValue)
at Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.SerializeObject(JsonWriter writer, Object value, JsonObjectContract contract, JsonProperty member, JsonContainerContract collectionContract, JsonProperty containerProperty)
at Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.Serialize(JsonWriter jsonWriter, Object value, Type objectType)
at EPiServer.Find.Api.BulkActionConverter.WriteJson(JsonWriter writer, Object value, JsonSerializer serializer)
at Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.SerializeConvertable(JsonWriter writer, JsonConverter converter, Object value, JsonContract contract, JsonContainerContract collectionContract, JsonProperty containerProperty)
at Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.Serialize(JsonWriter jsonWriter, Object value, Type objectType)
at Newtonsoft.Json.JsonSerializer.SerializeInternal(JsonWriter jsonWriter, Object value, Type objectType)
at EPiServer.Find.Json.Serializer.SerializeToTextWriter(JsonSerializer serializer, Object value, TextWriter textWriter)
at EPiServer.Find.Json.Serializer.SerializeObjectsToJsonRequest(JsonSerializer serializer, IJsonRequest jsonRequest, IEnumerable values)
at EPiServer.Find.Api.BulkCommand.Execute(List`1& serializationFailures)
at EPiServer.Find.Api.BulkCommand.Execute()
at EPiServer.Find.Cms.ContentIndexer.IndexWithRetry(IContent[] contents, Int32 maxRetries)
at EPiServer.Find.Cms.ContentIndexer.Index(IEnumerable`1 content, IndexOptions options)
at EPiServer.Find.Cms.ContentIndexer.IndexBatch(IEnumerable`1 content, Action`1 statusAction, Int32& numberOfContentErrors, Int32& indexingCount)

#195235
Jul 18, 2018 14:24
* You are NOT allowed to include any hyperlinks in the post because your account hasn't associated to your company. User profile should be updated.