What to Do When a Client Wants Over 500 Authorable Redirects in the Root of Their Site

Redirects can be managed in a few different ways, but when a client asked for hundreds of them to be authorable off the home page, well, that took my favourites out of the option list. This request is a little unique with the solution leveraging buckets and RedirectOnItemNotFound.

So, you can use aliases or even build a redirect item that can be added anywhere (we do that too), but no item should have more than 100 direct children, and this case calls for it. To resolve this issue, I'll create a bucketed collection of items, and do a lookup and redirect when a page is not found.


Storing the Data

You can store your items anywhere you want, really. We have a generic Settings item under each site node, and in there I've placed a “Root Redirects”, with about 500 redirect items. Wow.



The items are very simple, just having a standard link field and an option to indicate if this is a permanent redirect.


It's important to inform Authors that these would only be employed if there isn't already a page of the same name, since we're taking advantage of the RedirectOnItemNotFound pipeline.


The Redirect in Action

Ok the first thing we need is employ our custom pipeline with a config file, like so:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"></configuration><configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
    <sitecore>
        <pipelines>
            <httprequestbegin>
                <processor type="Sitecore.Feature.Navigation.Pipelines.HttpRequest.ExecuteRequest, Sitecore.Feature.Navigation" 
                patch:instead="processor[@type='Sitecore.Pipelines.HttpRequest.ExecuteRequest, Sitecore.Kernel']"></processor>
            </httprequestbegin>
        </pipelines>
    </sitecore>
</configuration>

Our RedirectOnItemNotFound method will look up an item in the bucketed folder with the same name as the URL, and if it finds one it processes the URL. The first thing to do is get the item name from the URL.

var requestedUrl = HttpContext.Current.Request.Url.ToString().Replace("-", " ").Replace("%20", " ").Trim(new Char[] { '/', ' ' });
var pagesInRequestPath = WebUtil.ExtractFilePath(requestedUrl).Split('/');
var requestedItem = pagesInRequestPath.First();

Once we have the name we're looking for, we get the folder and then do a lookup for a child of this name.

var rootRedirectsPath = $"{Context.Site.RootPath}/Settings/RootRedirects";
var rootRedirects = Context.Database.GetItem(rootRedirectsPath);
var redirectItem = rootRedirects.Axes.GetDescendants().FirstOrDefault(x => x.DisplayName.ToLowerInvariant() == requestedItem.ToLowerInvariant());

Once found, simple HttpContext work is processed to end the request. Here you can see the entire redirect in action. I hope you enjoy using it. It was a fun pipeline to build!

using Microsoft.Extensions.DependencyInjection;
using Sitecore.Abstractions;
using Sitecore.DependencyInjection;
using Sitecore.Diagnostics;
using Sitecore.Foundation.SitecoreExtensions.Extensions;
using Sitecore.Web;
using System;
using System.Linq;
using System.Web;
namespace Sitecore.Feature.Navigation.Pipelines.HttpRequest
{
    public class ExecuteRequest : Sitecore.Foundation.Response.Pipelines.HttpRequest.ExecuteRequest
    {
        public ExecuteRequest() : this(ServiceLocator.ServiceProvider.GetRequiredService<basesitemanager>(), ServiceLocator.ServiceProvider.GetRequiredService<baseitemmanager>())
        {
        }
        public ExecuteRequest(BaseSiteManager siteManager, BaseItemManager itemManager) : base(siteManager, itemManager) { }
        protected override void RedirectOnItemNotFound(string url)
        {
            bool redirected = false;
            try
            {
                var requestedUrl = HttpContext.Current.Request.Url.ToString().Replace("-", " ").Replace("%20", " ").Trim(new Char[] { '/', ' ' });
                var pagesInRequestPath = WebUtil.ExtractFilePath(requestedUrl).Split('/');
                if (pagesInRequestPath.Count() == 1)
                {
                    var requestedItem = pagesInRequestPath.First();
                    var rootRedirectsPath = $"{Context.Site.RootPath}/Settings/RootRedirects";
                    var rootRedirects = Context.Database.GetItem(rootRedirectsPath);
                    if (rootRedirects != null)
                    {
                        var redirectItem = rootRedirects.Axes.GetDescendants().FirstOrDefault(x => x.DisplayName.ToLowerInvariant() == requestedItem.ToLowerInvariant());
                        if (redirectItem != null)
                        {
                            redirected = true;
                            string redirectUrl = redirectItem.GetLinkUrl(Templates._Redirect.Fields.RedirectTo);
                            if (string.IsNullOrWhiteSpace(redirectUrl))
                            {
                                redirectUrl = Context.Site.GetStartItem().Url();
                            }
                            HttpContext.Current.Response.Clear();
                            if (redirectItem.GetBoolean(Templates._Redirect.Fields.IsPermanent))
                            {
                                HttpContext.Current.Response.StatusCode = 301;
                            }
                            else
                            {
                                HttpContext.Current.Response.StatusCode = 302;
                            }
                            HttpContext.Current.Response.AddHeader("Location", redirectUrl);
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                Log.Info($"Navigation -> RedirectOnItemNotFound: Operation timed out while servicing URL. Trying base.RedirectOnItemNotFound. ({url}) Exception: {ex.ToString()}]", this);
                base.RedirectOnItemNotFound(url);
            }
            finally
            {
                if (redirected)
                    HttpContext.Current.Response.End();
            }
            if (!redirected)
            {
                base.RedirectOnItemNotFound(url);
            }
        }
    }
}