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.