MixedItemNameResolver saves the day – 404 page for your Sitecore Headless(JSS) SSR app when using display names in URL

Dear Sitecore developers, don’t forget to attend Sitecore Virtual Developer Day 2022. A lot of cool sessions will be there for you ๐Ÿ™‚

Today I want to share my pain and findings when setting up a 404 page for Sitecore Headless(JSS) SSR apps.

First a shout to the post – 404 Error handling in JSS layout service. You put me on the right track, thank you for this ๐Ÿ™‚

The post would be a done deal if we were using item names(in the URL), but we are using display names.

So basically we check the URL:

https://ahostname/sitecore/api/layout/render/default?item=/some-page-with-item-name&sc_apikey={API_KEY}

If the URL contains /sitecore/api/layout/render/default, extract the item, parse the query and finally try to get an item. If no item is found, then set the context item to a โ€œ404โ€ page item.
But as I mentioned earlier, we are using display names:

https://somehostname/sitecore/api/layout/render/default?item=/some-page-with-display-name&sc_apikey={API_KEY}

So how do we do this? How do we get an item by display name? There are always many solutions for this, but I prefer to use Sitecoreโ€™s own solution.

If you look closer into the ItemResolver – Sitecore.Pipelines.HttpRequest.ItemResolver. In the constructor, it checks for the flag MixedItemNameResolvingMode.Enabled. If it’s enabled then it will set the PathResolver to new MixedItemNameResolver(pathResolver)

And what is so special about MixedItemNameResolver? Well, it does exactly what we want. It gets an item by display name – OUR SAVIOR ๐Ÿ™‚

In our case/scenario the flag MixedItemNameResolvingMode.Enabled is set to true. So basically this means we can let the ItemResolver do all the work for us ๐Ÿ™‚
We will create an ItemNotFoundResolver which will inherit the ItemResolver. And to get an item by display name we will use the “built-in” PathResolver.ResolveItem(pathContainingDisplayNames, rootItem). Here is the code:

using Sitecore.Abstractions;
using Sitecore.Data.ItemResolvers;
using Sitecore.Data.Items;
using Sitecore.Globalization;
using Sitecore.Pipelines.HttpRequest;
using Sitecore.SecurityModel;
using System;
using System.Linq;
using System.Net;
using System.Web;

namespace Sandbox.Feature.NotFound.Pipelines.HttpRequestBegin
{

    public class ItemNotFoundResolver : ItemResolver
    {

        public ItemNotFoundResolver(BaseItemManager itemManager, ItemPathResolver pathResolver) : base(itemManager, pathResolver)
        {
           
        }


        private const string LayoutServicePath = "/sitecore/api/layout/render/jss";
        private const string Error404Page = "404";

        public override void Process(HttpRequestArgs args)
        {
            if (Sitecore.Context.Item != null || Sitecore.Context.Site == null || Sitecore.Context.Database == null)
                return;

            if (!args.RequestUrl.AbsolutePath.StartsWith(LayoutServicePath))
                return;

            if (IsValidDisplayNameItem(args))
                return;

            SetErrorPage();
        }

        private void SetErrorPage()
        {

            Sitecore.Context.Item = Sitecore.Context.Database.GetItem(string.Concat(Sitecore.Context.Site.StartPath, "/", Error404Page), Language.Parse(Sitecore.Context.Site.Language));
     
            HttpContext.Current.Response.TrySkipIisCustomErrors = true;
            HttpContext.Current.Response.StatusCode = (int)HttpStatusCode.NotFound;
        }

        private bool IsValidDisplayNameItem(HttpRequestArgs args)
        {
            var uri = new Uri(args.RequestUrl.AbsoluteUri);

            // Because thereโ€™s no context item available in the layout service, the item path is retrieved from the querystring โ€œitemโ€
            var query = HttpUtility.ParseQueryString(uri.Query).Get("item") ?? "";
            var queryParam = query.Replace("-", " ");
            var rootItem = Sitecore.Context.Database?.GetItem(Sitecore.Context.Site.StartPath);
         

            // Check if the item can be retrieved in any language while using the SecurityDisabler. This way protected pages are checked also.
            using (new SecurityDisabler())
            {
                var resolveItem = this.PathResolver.ResolveItem(queryParam, rootItem);
                if (resolveItem == null) return false;
                bool checkLanguageVersion = HasLanguageVersion(resolveItem, Sitecore.Context.Language.Name);
                return checkLanguageVersion;
            }
          
        }

        private bool HasLanguageVersion(Item item, string languageName)
        {
            var language = item.Languages.FirstOrDefault(l => l.Name == languageName);
            if (language == null) return false;
            var languageSpecificItem = Sitecore.Context.Database.GetItem(item.ID, language);
            return languageSpecificItem != null && languageSpecificItem.Versions.Count > 0;
        }
    

    }
    
}

And to get it to work we will add a config patch, the code will be “executed” after Sitecore’s ItemResolver:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/" xmlns:role="http://www.sitecore.net/xmlconfig/role/">
    <sitecore>
        <pipelines>
            <httpRequestBegin>
                <processor type="Sandbox.Feature.NotFound.Pipelines.HttpRequestBegin.ItemNotFoundResolver, Sandbox.Feature.NotFound" resolve="true" 
                           patch:after="processor[@type='Sitecore.Pipelines.HttpRequest.ItemResolver, Sitecore.Kernel']" />
            </httpRequestBegin>
        </pipelines>
    </sitecore>
</configuration>

Lessons learned, do a deep dive into Sitecore’s code and learn ๐Ÿ™‚

That’s all for now folks ๐Ÿ˜Š


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.