Categories:

Customizing Optimizely/EPiServer Commerce Partial Routing for a Second Start Root in Catalog Routing

In a recent consulting engagement, I reviewed a team’s implementation of Optimizely Commerce Connect 14, where they faced a unique requirement: support SEO-friendly, domain-aligned URLs like:

/programs/series-name/product-name  

These URLs needed to resolve to catalog content under a separate content structure rooted at a custom _programsRoot, while preserving default routing and editing behaviors for all other content—allowing fully headless link generation.

During the review, it was noted that the team had registered two customized instances of HierarchicalCatalogPartialRouter to handle different catalog roots (_programsRoot and the default catalog root). While this might seem like a straightforward approach, it leads to significant issues:

  • Routing Conflicts: The route table becomes ambiguous, causing unpredictable matches and routing behavior.
  • URL Generation Problems: Outgoing URLs, especially those generated by XhtmlString properties in the CMS, can end up duplicated or malformed (e.g., duplicate /programs/programs/ segments).
  • Editor Experience Impact: Editors face confusion when editing catalog links in rich-text fields, due to inconsistent URL resolutions and routing mismatches.
  • Maintenance Complexity: Managing multiple routers adds overhead and makes debugging harder.

✅ Objectives

  • Route URLs prefixed with /programs/ to a designated catalog subtree (_programsRoot).
  • Ensure GetPartialVirtualPath correctly generates outgoing links without duplicate or invalid segments.
  • Prevent any interference with CMS editing or preview mode.
  • Maintain full headless compatibility—all routes and URLs are server-generated and API-friendly.
  • Implement this in a single router setup only to avoid route table conflicts and preserve XhtmlString permalinks.

🔍 Optimizely Support for Multiple Roots

Though HierarchicalCatalogPartialRouter doesn’t officially support multiple entry points, it does allow arbitrary start nodes. In fact, Per Gunsarfs from Optimizely noted that you can register multiple router instances manually to simulate multiple root catalogs—even if it’s not officially promoted (See the reference 2).

However, using multiple routers can lead to URL generation inconsistencies, especially for UrlResolver.GetUrl() or content embedded through XhtmlString. A single, condition-based router is often more reliable.


🔧Key Implementation Details

1. RoutePartial

Intercepts incoming URL paths:

  • If the path begins with /programs/, set the catalog start to _programsRoot stored in HttpContext.Items["EP:CatalogRouterStartingPoints"].
  • Otherwise, use the default catalog root.
public override object RoutePartial(PageData content, UrlResolverContext context){
var path = context.Url.Path.Trim('/').ToLowerInvariant();
var http = _httpContextAccessor.HttpContext;var dict = http.Items["EP:CatalogRouterStartingPoints"] as Dictionary<object, ContentReference><br>               ?? new Dictionary<object, ContentReference>();
http.Items["EP:CatalogRouterStartingPoints"] = dict;
dict[this] = path.StartsWith("programs/") && !_programsRoot.IsNullOrEmpty()                  ? _programsRoot : base.RouteStartingPoint; return base.RoutePartial(content, context);}

2. GetPartialVirtualPath

Generates outgoing URLs based on context:

  • Removes duplicate /programs/ if already routed from _programsRoot.
  • Prepend /programs/ for program content only when needed.

3. GetCatalogContentRecursive

Internal Content Lookup:

  • Bypass routing in CMS edit/preview.
  • Trim /programs from paths before recursing into the program catalog subtree.
  • Handle special business logic based on route structure.

These updates guarantee that both incoming URL resolution and internal navigation correctly support the two-root catalog structure.


🧠 Best Practices

  • Only one HierarchicalCatalogPartialRouter should be registered—to avoid routing conflicts and unexpected link behavior.
  • Implement secondary root logic via HttpContext.Items, rather than multiple routers.
  • Avoid interfering with edit or preview mode; leave those paths untouched.
  • Maintain full support for CMS UI, Preview/Edit, and headless link generation.

👣 Summary Flow

ScenarioInbound (RoutePartial, GetCatalogContentRecursive)Outbound (GetPartialVirtualPath)
/programs/series/...Start from _programsRootURL starts with /programs/...
/products/... or otherDefault catalog rootDefault virtual paths generated

While HierarchicalCatalogPartialRouter wasn’t originally designed to handle multiple catalog roots, we’ve achieved a robust solution by:

  • Remapping custom routing paths,
  • Dynamically setting the catalog starting node,
  • And ensuring consistent URL generation.

All while preserving edit-mode integrity and headless link resolution.

References:

  1. https://world.optimizely.com/blogs/reimonds-blog—episerver-developer/dates/2017/11/episerver-cms-and-commerce-custom-routing
  2. https://world.optimizely.com/forum/developer-forum/Commerce/Thread-Container/2017/1/multiple-catalog-roots-and-cms-roots-/