Take the community feedback survey now.

Jonas Bergqvist
Jan 7, 2016
  3368
(0 votes)

Automatic landing page

An “Automatic landing page” is content (for example an page type instance) which renders differently depending on user input. A form of personalized content-type. A google hit can, for example, point to an automatic landing page, which shows content related to the google search.

This is the first of a blog series about automatic landing pages. This first blog post is an overview over what automatic landing pages are about. The next blog will show how you can implement automatic landing pages in Quicksilver (Commerce demo site) with the search provider system. Another one will show how to use native Find in Quicksilver to accomplish the same thing.

Partial route

The challenge in building an automatic landing page is to make a search engine accept dynamic rendered content as different search hits. That is easiest accomplished by creating a partial route.

One url will render one unique “page” for one content instance. Different url:s are not supposed to render the same content. A partial route makes it possible to use a nice url instead of query parameters to specify what content that should be rendered on a page. Look at the following example, taken from the Quicksilver implementation, which goes to the same content instance:

  • http://quicksilver/en/fashion/mens/Category/jackets/Color/blue: Blue jackets for men.
  • http://quicksilver/en/fashion/womens/Category/dresses/Color/black/Size/m: Black dresses in size medium for women.

A partial route can be used to both create url:s and read incoming requests. I will only use the partial route for reading incoming requests. The rendering of url:s will be done in a separate service. The examples use Episerver Commerce, but it’s possible to make the same functionality for CMS sites as well.

I will create a class that inherits from "HierarchicalCatalogPartialRouter", and override "RoutePartial". This method should first get the catalog content by calling the base method. Then it’s time to figure out when the rest of the url means.

public class FacetPartialRoute : HierarchicalCatalogPartialRouter
    {
        private readonly FacetUrlService _facetUrlCreator;

        public FacetPartialRoute(Func<ContentReference> routeStartingPoint, CatalogContentBase commerceRoot,
            bool enableOutgoingSeoUri)
            : this(
            routeStartingPoint, 
            commerceRoot, 
            enableOutgoingSeoUri,
            ServiceLocator.Current.GetInstance<IContentLoader>(),
            ServiceLocator.Current.GetInstance<IRoutingSegmentLoader>(),
            ServiceLocator.Current.GetInstance<IContentVersionRepository>(), 
            ServiceLocator.Current.GetInstance<IUrlSegmentRouter>(),
            ServiceLocator.Current.GetInstance<IContentLanguageSettingsHandler>(),
            ServiceLocator.Current.GetInstance<FacetUrlService>())
        {
        }

        [DefaultConstructor]
        public FacetPartialRoute(Func<ContentReference> routeStartingPoint, CatalogContentBase commerceRoot,
            bool supportSeoUri, IContentLoader contentLoader, IRoutingSegmentLoader routingSegmentLoader,
            IContentVersionRepository contentVersionRepository, IUrlSegmentRouter urlSegmentRouter,
            IContentLanguageSettingsHandler contentLanguageSettingsHandler,
            FacetUrlService facetUrlCreator)
            : base(
                routeStartingPoint, commerceRoot, supportSeoUri, contentLoader, routingSegmentLoader,
                contentVersionRepository, urlSegmentRouter, contentLanguageSettingsHandler)
        {
            _facetUrlCreator = facetUrlCreator;
        }

        public override object RoutePartial(PageData content, SegmentContext segmentContext)
        {
            var routedContet = base.RoutePartial(content, segmentContext);

            var segmentPair = segmentContext.GetNextValue(segmentContext.RemainingPath);
            if (String.IsNullOrEmpty(segmentPair.Next))
            {
                return routedContet;
            }

            var facetNames = _facetUrlCreator.GetFacetModels().ToArray();

            var nextSegment = _facetUrlCreator.GetFacetValue(facetNames, segmentPair.Next);
            if (String.IsNullOrEmpty(nextSegment))
            {
                return routedContet;
            }

            var routeFacets = segmentContext.RouteData.Values[FacetUrlService.RouteFacets] as ConcurrentDictionary<RouteFacetModel, HashSet<object>>;
            if (routeFacets == null)
            {
                segmentContext.RouteData.Values[FacetUrlService.RouteFacets] = new ConcurrentDictionary<RouteFacetModel, HashSet<object>>();
                routeFacets = (ConcurrentDictionary<RouteFacetModel, HashSet<object>>)segmentContext.RouteData.Values[FacetUrlService.RouteFacets];
            }

            AddFacetsToSegmentContext(routeFacets, segmentContext, facetNames, nextSegment, segmentPair.Remaining, null);
            return routedContet;
        }

        private void AddFacetsToSegmentContext(ConcurrentDictionary<RouteFacetModel, HashSet<object>> routeFacets, SegmentContext segmentContext, RouteFacetModel[] facetNames, string nextSegment, string remaining, RouteFacetModel currentFacet)
        {
            if (String.IsNullOrEmpty(nextSegment))
            {
                return;
            }

            var value = facetNames.FirstOrDefault(x => x.FacetName == nextSegment);
            if (value != null)
            {
                currentFacet = value;
                
            }
            else if (currentFacet != null)
            {
                var facetValue = _facetUrlCreator.GetFacetValue(facetNames, nextSegment);

                routeFacets.AddOrUpdate(currentFacet,
                   (key) => new HashSet<object> { facetValue },
                   (key, list) =>
                   {
                       list.Add(facetValue);
                       return list;
                   });
            }

            segmentContext.RemainingPath = remaining;

            var segmentPair = segmentContext.GetNextValue(segmentContext.RemainingPath);
            nextSegment = _facetUrlCreator.GetFacetValue(facetNames, segmentPair.Next);

            AddFacetsToSegmentContext(routeFacets, segmentContext, facetNames, nextSegment, segmentPair.Remaining, currentFacet);
        }
    }

Route registration

We need a way of registering our route(s). We will create a class for the registrations called CatalogContentRouteRegistration:

[ServiceConfiguration(Lifecycle = ServiceInstanceScope.Singleton)]
    public class CatalogContentRouteRegistration
    {
        private readonly IContentLoader _contentLoader;
        private readonly ReferenceConverter _referenceConverter;

        public CatalogContentRouteRegistration(IContentLoader contentLoader, ReferenceConverter referenceConverter)
        {
            _contentLoader = contentLoader;
            _referenceConverter = referenceConverter;
        }

        public void RegisterDefaultRoute()
        {
            RegisterDefaultRoute(false);
        }

        public void RegisterDefaultRoute(bool enableOutgoingSeoUri)
        {
            var commerceRootContent = _contentLoader.Get<CatalogContentBase>(_referenceConverter.GetRootLink());

            var pageLink = ContentReference.IsNullOrEmpty(SiteDefinition.Current.StartPage)
                ? SiteDefinition.Current.RootPage
                : SiteDefinition.Current.StartPage;

            RegisterRoute(pageLink, commerceRootContent, enableOutgoingSeoUri);
        }

        public void RegisterRoute(ContentReference pageLink, ContentReference catalogLink, bool enableOutgoingSeoUri)
        {
            var commerceRootContent = _contentLoader.Get<CatalogContentBase>(catalogLink);
            RegisterRoute(pageLink, commerceRootContent, enableOutgoingSeoUri);
        }

        public void RegisterRoute(ContentReference pageLink, CatalogContentBase catalogContentBase, bool enableOutgoingSeoUri)
        {
            RouteTable.Routes.RegisterPartialRouter(new FacetPartialRoute(() => pageLink, catalogContentBase, enableOutgoingSeoUri));
        }
    }

FacetUrlService

I will create a new service named “FacetUrlService”. The main responsibility for this service is to create a virtual path for selected facets on a site. A method called “GetFilterPath” will take the standard virtual path as one parameter, and a dictionary containing selected facets as the other parameter. The return value will be a string containing the virtual path including facets from the parameter.

[ServiceConfiguration(Lifecycle = ServiceInstanceScope.Singleton)]
    public class FacetUrlService
    {
        public const string RouteFacets = "routeFacets";
        private readonly DynamicDataStoreFactory _dynamicDataStoreFactory;
        private readonly ISynchronizedObjectInstanceCache _objectInstanceCache;
        private readonly UrlResolver _urlResolver;

        public FacetUrlService(DynamicDataStoreFactory dynamicDataStoreFactory, ISynchronizedObjectInstanceCache objectInstanceCache, UrlResolver urlResolver)
        {
            _dynamicDataStoreFactory = dynamicDataStoreFactory;
            _objectInstanceCache = objectInstanceCache;
            _urlResolver = urlResolver;
        }

        public IEnumerable<RouteFacetModel> GetFacetModels()
        {
            var facetNames = GetCachedFacetNames();
            if (facetNames != null)
            {
                return facetNames;
            }

            var routingFacetNameStore = GetRoutingFacetNameStore();
            var allRouteFacetModels = routingFacetNameStore.LoadAll<RouteFacetModel>();

            var cacheKey = GetCacheName();
            _objectInstanceCache.Insert(cacheKey, allRouteFacetModels, new CacheEvictionPolicy(new string[0]));

            return allRouteFacetModels;
        }

        internal string GetUrl(IContent currentContent, RouteValueDictionary routeValues, string facetType, string facetKeyPath, string facetKey, object facetValue)
        {
            var originalRouteFacets = routeValues[RouteFacets] as ConcurrentDictionary<RouteFacetModel, HashSet<object>>;

            var routeFacets = new Dictionary<RouteFacetModel, HashSet<object>>();
            if (originalRouteFacets != null)
            {
                foreach (var routeFacetModel in originalRouteFacets.Keys)
                {
                    routeFacets.Add(routeFacetModel, new HashSet<object>());
                    foreach (var value in originalRouteFacets[routeFacetModel])
                    {
                        routeFacets[routeFacetModel].Add(value);
                    }
                }
            }

            var model = routeFacets.Select(x => x.Key).SingleOrDefault(x => x.FacetName == facetKey);
            if (model != null)
            {
                routeFacets[model].Add(facetValue);
            }
            else
            {
                model = new RouteFacetModel
                {
                    FacetName = facetKey,
                    FacetPath = facetKeyPath,
                    FacetType = facetType
                };
                routeFacets.Add(model, new HashSet<object> { facetValue });
            }

            string language = null;
            var languageContent = currentContent as ILocalizable;
            if (languageContent != null)
            {
                language = languageContent.Language.Name;
            }

            var url = _urlResolver.GetUrl(currentContent.ContentLink, language);
            return url.Length > 1 ? GetUrl(url.Substring(0, url.Length - 1), routeFacets) : url;
        }

        internal string GetUrl(string partialVirtualPath, IDictionary<RouteFacetModel, HashSet<object>> routeFacets)
        {
            var path = new StringBuilder(partialVirtualPath);

            var routeFacetKeys = routeFacets.Keys.OrderBy(x => x.FacetName);
            foreach (var routeFacetKey in routeFacetKeys)
            {
                HashSet<object> keyValues;
                if (routeFacets.TryGetValue(routeFacetKey, out keyValues))
                {
                    SaveIfNotExist(routeFacetKey);
                    path.Append(String.Concat("/", routeFacetKey.FacetName));

                    var keyValueStrings = keyValues.Select(x => x.ToString()).OrderBy(x => x);
                    foreach (var keyValueString in keyValueStrings)
                    {
                        var facetValue = GetFacetValueWhenCreatingUrl(keyValueString);
                        path.Append(String.Concat("/", facetValue));
                    }
                }
            }

            return path.ToString();
        }

        internal string GetFacetValue(IEnumerable<RouteFacetModel> facetNames, string originalName)
        {
            var possibleProblems = facetNames.Where(x => x.FacetName.EndsWith(originalName));
            if (!possibleProblems.Any())
            {
                return originalName;
            }

            var modifiedName = originalName;
            while (modifiedName.Length > 0)
            {
                modifiedName = modifiedName.Substring(1);
                if (!facetNames.Any(x => x.FacetName.EndsWith(originalName)))
                {
                    return modifiedName;
                }
            }

            return originalName;
        }

        private string GetFacetValueWhenCreatingUrl(string originalName)
        {
            var facetNames = GetFacetModels();
            return GetFacetValueWhenCreatingUrl(facetNames, originalName);
        }

        private static string GetFacetValueWhenCreatingUrl(IEnumerable<RouteFacetModel> facetNames, string originalName)
        {
            if (facetNames == null || !facetNames.Any(x => x.FacetName == originalName))
            {
                return originalName;
            }

            return GetFacetValueWhenCreatingUrl(facetNames, String.Concat("f", originalName));
        }

        private void SaveIfNotExist(RouteFacetModel facetName)
        {
            var facetNames = GetFacetModels();
            if (facetNames != null && facetNames.Any(x => x.FacetName == facetName.FacetName))
            {
                return;
            }

            var routingFacetNameStore = GetRoutingFacetNameStore();
            routingFacetNameStore.Save(facetName);
            ClearFacetNamesCache();
        }

        private IEnumerable<RouteFacetModel> GetCachedFacetNames()
        {
            return _objectInstanceCache.Get(GetCacheName()) as IEnumerable<RouteFacetModel>;
        }

        private void ClearFacetNamesCache()
        {
            _objectInstanceCache.Remove(GetCacheName());
        }

        private static string GetCacheName()
        {
            return "bc:routingfacetnames";
        }

        private DynamicDataStore GetRoutingFacetNameStore()
        {
            const string routingFacetNames = "RoutingFacetNames";
            return _dynamicDataStoreFactory.GetStore(routingFacetNames) ??
                _dynamicDataStoreFactory.CreateStore(routingFacetNames, typeof(RouteFacetModel));
        }
    }

HtmlHelper

The last thing I need is a HtmlHelper extension method, which will take the current content, the facet name, and facet value as parameters. We will then call FacetUrlService with the current url and facets to get a new url for a specific facet value.

We can now call this helper method for every facet value in a facet navigation. The url:s will be unique for every possible combination if we make sure to sort the facets and facet values in our service. The url:s will also be normal url:s that a crawler easily can index. This can make a crawler index every possible facet combination on your facet navigated page as individual pages.

        public static MvcHtmlString FacetContentUrl(this HtmlHelper htmlHelper, IContent currentContent, string facetType, string facetKeyPath, string facetKey, object facetValue)
        {
            var url = ServiceLocator.Current.GetInstance<FacetUrlService>().GetUrl(currentContent, htmlHelper.ViewContext.RouteData.Values, facetType, facetKeyPath, facetKey, facetValue);

            return new MvcHtmlString(url);
        }

Implementation on Quicksilver

I will make a new blog post soon where I use the nuget package blow to implement this on Quicksilver.

Source code

https://github.com/jonasbergqvist/BrilliantCut.AutomaticLandingPage

Nuget package

The easiest way to install the packages are to create a new package source in visual studio that points to "http://nuget.jobe.employee.episerver.com/" (https://docs.nuget.org/consume/Package-Manager-Dialog). Now it's possible to install the packages by writing "install-package brilliantcut.automaticlandingpage" in the "Package manager console".

Jan 07, 2016

Comments

Please login to comment.
Latest blogs
A day in the life of an Optimizely OMVP - Opticon London 2025

This installment of a day in the life of an Optimizely OMVP gives an in-depth coverage of my trip down to London to attend Opticon London 2025 held...

Graham Carr | Oct 2, 2025

Optimizely Web Experimentation Using Real-Time Segments: A Step-by-Step Guide

  Introduction Personalization has become de facto standard for any digital channel to improve the user's engagement KPI’s.  Personalization uses...

Ratish | Oct 1, 2025 |

Trigger DXP Warmup Locally to Catch Bugs & Performance Issues Early

Here’s our documentation on warmup in DXP : 🔗 https://docs.developers.optimizely.com/digital-experience-platform/docs/warming-up-sites What I didn...

dada | Sep 29, 2025

Creating Opal Tools for Stott Robots Handler

This summer, the Netcel Development team and I took part in Optimizely’s Opal Hackathon. The challenge from Optimizely was to extend Opal’s abiliti...

Mark Stott | Sep 28, 2025

Integrating Commerce Search v3 (Vertex AI) with Optimizely Configured Commerce

Introduction This blog provides a technical guide for integrating Commerce Search v3, which leverages Google Cloud's Vertex AI Search, into an...

Vaibhav | Sep 27, 2025

A day in the life of an Optimizely MVP - Opti Graph Extensions add-on v1.0.0 released

I am pleased to announce that the official v1.0.0 of the Opti Graph Extensions add-on has now been released and is generally available. Refer to my...

Graham Carr | Sep 25, 2025