
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 ๐