The goal is to create the most dynamic E-Commerce Category pages possible with Epi Commerce & Find.
First lets clarify the terminology. By category page i am referring to the EPi Commerce “NodeContent” type used to structure the product catalog. Customers will be able to navigate the site using this category structure seeing the top relevant products at all points.
My project type is a Dynamics AX ERP to EPiServer Commerce integration using Avensia Storefront to manage the catalog synchronisation. More information on this type of integration is available here: Delivering Unified Commerce Solutions With Episerver Avensia Storefront And Dynamics For Finance & Operations
Using Avensia Storefront we can replicate the AX Product Catalog in the EpiServer Commerce Catalog. With this approach if our client publishes a new category and/or products in AX, within minutes that set up will be replicated in our EPiServer catalog.
The Category pages of an E-Commerce site are absolutely critical to get right as an optimal set up will provide the customer with an easy method of navigating the site while providing the functionality and information they need to find the product(s) they need. From an SEO perspective it’s also extremely important that crawlers can easily traverse the hierarchy and gain context.
Because categories are added dynamically, we have one category page implementation but the search faceting and product view should dynamically optimise based on the descendant products types of the current category and their attributes.
Take for example this simplistic catalog structure:
All Product Types inherit from a “BaseProduct” which contains common properties across the catalog.
Our Category Page inherits from Base Node
My requirement is:
- Category A – advanced facet filtering options for Product Type 1
- Category B – advanced facet filtering options for Product Type 2
- Category C – facets common to Base Product
Technical Set Up
The Category Controller inherits from ContentController<NodeContent> meaning that it gets hit on all page loads of Node Content. We are going to use a search service which is injected into the Category Controller and manages the communication with Find. The following sequence diagram will give you a feel for the flow.
Dynamically getting Child Content Types
My Search Service interface defines the following methods:
public interface ISearchService { SearchResultsModel SearchFromCategory(NodeContent currentContent, string query, string sort = "", int page = 1, int? pageSize = null, List facetGroups = null); SearchResultsModel Search(NodeContent currentContent, string query, string sort, int page = 1, int? pageSize = null, List facetGroups = null, bool trackSearch = false) where T : BaseProduct; }
In the SearchFromCategory method we need to find the descendant product types.
This query took a bit of figuring out and i have to thank the excellent community in Epi World for pointing me in the right direction:
The solution i arrived at is the following method which uses Find to get return all product types that are descendants of the current Node. To accomplish this “_Type” is included in the request where the indexed property will contain the entire type string.
public List GetProductTypesForCurrentCategory(NodeContent currentContent) { // use Find to execute search with the content type id set as a facet var searchQuery = _findClient.Search() .Filter(x => x.Ancestors().Match(currentContent.ContentLink.ToReferenceWithoutVersion().ToString())) .Take(0).TermsFacetFor(x => x.CommonType(), request => request.Field = "_Type") .StaticallyCacheFor(TimeSpan.FromMinutes(SearchConstants.ProductTypesCacheInMinutes)) .GetContentResult(); // extract the content type id's form the result var terms = searchQuery.TermsFacetFor(x => x.CommonType()).Terms; var productTypes = new List(); // add returned types to list foreach (var typeNamespace in terms) { var type = Type.GetType(typeNamespace.Term); productTypes.Add(type); } return productTypes; }
The Search From Category can then be implemented where it customises search to a product type where possible or otherwise using the Base Product object
// Called from category page public SearchResultsModel SearchFromCategory(BaseNode currentContent, string query, string sort = "", int page = 1, int? pageSize = null, List facetGroups = null) { var productTypes = GetProductTypesForCurrentCategory(currentContent); if (productTypes?.Count == 1) { // get product type var productType = productTypes.First(); // instantiate and invoke generic search var searchMethod = typeof(SearchService).GetMethod("Search", new [] {typeof(BaseNode), typeof(string), typeof(string), typeof(int), typeof(int?), typeof(List), typeof(bool) }); if (searchMethod != null) { var genericSearch = searchMethod.MakeGenericMethod(productType); return genericSearch.Invoke(this, new object[] {currentContent, query, sort, page, pageSize, facetGroups, false}) as SearchResultsModel; } } // search for base type return Search(currentContent, query, sort, page, pageSize, facetGroups); }
The generic Search Service in this solution is then built to customise facets and search results based on the type of product so this solution. Thats a large topic so happy to do another blog post on that!
Also note that this solution uses reflection and this might not always be the optimal implementation from a performance perspective. You could just as easily implement a factory method with a switch statement to map types to Search<T>() method invocations.