Sitecore Bucketed URLs Are a Thing of the Past, With the Custom URL Resolver

In my last post, I shared a way to hide bucketed folders from Sitecore URLs to make them more SEO friendly. So how do we find an item if part of the standard URL is missing? The answer is by overriding the standard Item Resolver, and iterating over the item.Children to find what we're looking for. 


Overriding ItemResolver

Same as before, standard values are going to be patched in to this custom URL resolver, along with databases, sites, roots and a new one, ignore. The ignore list should be pretty obvious, and can have additional values to bypass the method we'll cover below.

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"></configuration><configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"></configuration>
  <sitecore>
    <pipelines>
      <httpRequestBegin>
        <processor type="MySite.Pipelines.CustomUrlResolver, MySite"
          patch:after="processor[@type='Sitecore.Pipelines.HttpRequest.ItemResolver, Sitecore.Kernel']">
          <databases hint="list">
            <database>master</database>
            <database>web</database>
          </databases>
          <sites hint="list">
            <site>website</site>
            <site>website2</site>
          </sites>
          <roots hint="list">
            <root>/sitecore/content/website/home/</root>
            <root>/sitecore/content/website2/home/</root>
          </roots>
          <ignore hint="list">
            <ignore>/tdsservice</ignore>
            <ignore>/api/</ignore>
            <ignore>/keepalive</ignore>
          </ignore>
        </processor>
      </httpRequestBegin>
    </pipelines>
  </sitecore>
</configuration>


The CustomUrlResolver in Action

This method is a little easier to understand than the last one. The preliminary checks are in place to ensure we want to run the FindItem method, where the magic really happens.

namespace MySite.Pipelines
{
    public class CustomUrlResolver : HttpRequestProcessor
    {
        public List<string> Sites { get; set; }
        public List<string> Roots { get; set; }
        public List<string> Databases { get; set; }
        public List<string> Ignore { get; set; }
        private static readonly string _processLogPrefix = $"[{typeof(CustomUrlResolver).FullName}]: ";
        public CustomUrlResolver()
        {
            Sites = new List<string>();
            Roots = new List<string>();
            Databases = new List<string>();
            Ignore = new List<string>();
        }
        
        public override void Process(HttpRequestArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            if (Context.Item == null)
            {
                var itemPath = MainUtil.DecodeName(args.Url.ItemPath);
                var filePath = MainUtil.DecodeName(args.Url.FilePath);
                if (Ignore.Any(ignore => filePath.StartsWith(ignore, StringComparison.InvariantCultureIgnoreCase)))
                    return;
                else if (!Roots.Any(root => itemPath.StartsWith(root, StringComparison.InvariantCultureIgnoreCase)))
                    Log.Debug(_processLogPrefix + $"Won't attempt to resolve item for {args.RequestUrl} because the item path ({itemPath ?? "null"}) does not have an included root.");
                else if (Context.Database == null)
                    Log.Debug(_processLogPrefix + $"Won't attempt to resolve item for {args.RequestUrl} because the context database is null.");
                else if (!Databases.Contains(Context.Database.Name, StringComparer.InvariantCultureIgnoreCase))
                    Log.Debug(_processLogPrefix + $"Won't attempt to resolve item for {args.RequestUrl} because the context database ({Context.Database.Name}) is not included.");
                else if (!Sites.Contains(Context.Site?.Name, StringComparer.InvariantCultureIgnoreCase))
                    Log.Debug(_processLogPrefix + $"Won't attempt to resolve item for {args.RequestUrl} because the context site ({Context.Site?.Name ?? "null"}) is not included.");
                else
                {
                    try
                    {
                        if (itemPath.StartsWith(Context.Site.StartPath, StringComparison.InvariantCultureIgnoreCase))
                        {
                            var pathParts = filePath.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
                            var item = FindItem(Context.Site.GetStartItem(), pathParts, args);
                            if (item != null)
                                Context.Item = item;
                        }
                    }
                    catch (Exception e)
                    {
                        Log.Error(_processLogPrefix + "Error finding page", e, this);
                    }
                }
            }
        }


The FindItem method is called with the site's start item, the URL (split into a string list) and the common args. The children of the home item, and then each found child in the URL will all look to see if it is found by name or display name, and return that item. So essentially we dig down the content tree by matching the URL parts and when the name of the item matches the requested item it gets returned. 

        protected Item FindItem(Item item, IEnumerable<string> names, HttpRequestArgs args)
        {
            if (item != null)
            {
                if (names.Any())
                {
                    string name = names.First();
                    foreach (Item childItem in item.Children)
                    {
                        if (childItem.IsDerived(Templates._Navigable.ID))
                        {
                            if (childItem.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase))
                                return FindItem(childItem, names.Skip(1), args);
                            if (childItem.DisplayName.Equals(name, StringComparison.InvariantCultureIgnoreCase))
                                return FindItem(childItem, names.Skip(1), args);
                        }
                    }
                    foreach (Item childItem in item.Children)
                    {
                        if (childItem.IsDerived(Templates._Navigable.ID)) continue;
                        var candidateItem = FindItem(childItem, names, args);
                        if (candidateItem?.IsDerived(Templates._Navigable.ID) ?? false)
                            return FindItem(candidateItem, names.Skip(1), args);
                    }
                }
                else
                {
                    return item;
                }
            }
            return null;
        }
    }
}


It's that easy! Now you can have all the URLs you want under a single item without those pesky date folder. There's also no impact to performance using this method in place of OOTB functions. Message me if you have any trouble integrating this into your project.