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 😊


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

  1. Hey.. great article.. I have a question though.. were you manage to get the status code (404) passed into your react/nextjs app ?
    For me, even after setting those context status codes, it still returns 200 since item is returned.

    Liked by 1 person

    1. Hello!
      I’m glad and thankful that you are reading my blog posts 🙂

      Hmm, that is strange. We are using this approach on all our sites.

      You have all the code?
      Specially this part:
      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;
      }

      Like

      1. Hey, I have that part.. but I think since we are assigning the context item, layout service thinks its a success and doesn’t return 404..
        But I was able to get a work around and passing the error code from context. Then from nextjs page server side props, check that value and set the response code..
        Thanks for checking my question though..

        Liked by 1 person

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.