Adding a Custom 404 Page When the Requested Language Is Missing

Sitecore won't handle a page request as a 404 if it's just the context language that's missing. So, what this means, is your Users will just see  a blank page should the item exist in English but there's no French version, and they're requesting the French page. We need to override HttpRequestProcessor and  ExecuteRequest to make this a friendly experience for your Users.

We're also going to go a bit further. Instead of just offering up a content page that serves as your generic 404, we'll make a language-specific content page that says, "Hey we've got the page you want, but it's not in the language you requested". In the past I've also added a link to the contact form so the User can request a translated version is created. This was for a big site where not all content could be translated, but now I wonder if the translator on retainer was just sending these requests for more work :)

Stopping Default Behaviour

Let's start with ensuring that Sitecore knows this is a missing page, and we'll do this by overriding Sitecore.Pipelines.HttpRequest.HttpRequestProcessor with ItemLanguageVersionValidator in our Foundation.Response project.

You can see that this method checks:

  • The item has no versions
  • It's using a database we expect it to
  • It's in the sites we have listed
  • It's in the proper content path

(These values are all configurable in the Foundation.Response.config file at the bottom of this article)

namespace Sitecore.Foundation.Response.Pipelines.HttpRequest
{
    public class ItemLanguageVersionValidator : Sitecore.Pipelines.HttpRequest.HttpRequestProcessor
    {
        private readonly List<string> databases = new List<string>();
        private readonly List<string> sites = new List<string>();
        private readonly List<string> roots = new List<string>();
        public List<string> Databases => databases;
        public List<string> Sites => sites;
        public List<string> Roots => roots;
        public override void Process(Sitecore.Pipelines.HttpRequest.HttpRequestArgs args)
        {
            if (Context.Item?.Versions.Count > 0
                || !Databases.Contains(Context.Database?.Name, StringComparer.InvariantCultureIgnoreCase)
                || !Sites.Contains(Context.Site?.Name, StringComparer.InvariantCultureIgnoreCase)
                || !Roots.Any(root => Context.Item?.Paths.FullPath.StartsWith(root, StringComparison.InvariantCultureIgnoreCase) ?? false))
            {
                Log.Debug($"Item will not be considered because it doesn't exist under a valid root path. ({Context.Item?.Paths.FullPath})");
            }
            else
            {
                Log.Debug("Item not found in context language, but exists in at least one other language.");
                Context.Items.Add("ItemNotFoundInContextLanguage", Context.Item.ID);
                Context.Item = null;
            }
        }
    }
}


Showing a 404 Page

Ok Sitecore knows this is something that's missing. Now we're going to show a page with a message by overriding the Sitecore.Pipelines.HttpRequest.ExecuteRequest.RedirectOnItemNotFound method. 

namespace Sitecore.Foundation.Response.Pipelines.HttpRequest
{
    public class ExecuteRequest : Sitecore.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)
        {
            var context = HttpContext.Current;
            var filePath = WebUtil.ExtractFilePath(url);
            var installedLanguages = LanguageManager.GetLanguages(Context.Database);
            var parameters = WebUtil.ParseQueryString(url, true);
            context.Response.StatusCode = 404;
            parameters["sc_lang"] = Context.Language?.Name;
            if (!installedLanguages.Contains(Context.Language?.Name.ToLowerInvariant()))
            {
                var useLanguage = installedLanguages.FirstOrDefault(x => x.Name.ToLowerInvariant() == "en");
                parameters["sc_lang"] = (useLanguage != null) ? useLanguage.Name : installedLanguages.FirstOrDefault().Name;
            }
            parameters["site"] = Context.Site.Name;
            try
            {
                if (Context.Database == null
                    || url.ToLower().Contains("/tdsservice")
                    || url.ToLowerInvariant().Contains("api/sitecore"))
                    return;
                if (!string.IsNullOrWhiteSpace(Settings.ItemNotFoundInContextLanguageUrl))
                {
                    var itemNotFoundInContextLanguageId = (ID)Context.Items["ItemNotFoundInContextLanguage"];
                    if (ID.IsNullOrEmpty(itemNotFoundInContextLanguageId))
                    {
                        string path = string.Concat(Context.Site.StartPath, HttpContext.Current.Request.Url.AbsolutePath.Replace("-", " ").Replace("%20", " "));
                        foreach (var language in installedLanguages)
                        {
                            if (Context.Language != language)
                            {
                                Item languageItem = ItemManager.GetItem(path, language, Data.Version.Latest, Context.Database, SecurityCheck.Disable);
                                if (languageItem != null && languageItem.Versions.Count > 0)
                                {
                                    parameters.Remove("sc_itemid");
                                    filePath = Settings.ItemNotFoundInContextLanguageUrl;
                                    break;
                                }
                            }
                        }
                    }
                    else
                    {
                        parameters.Remove("sc_itemid");
                        parameters.Remove("item");
                        parameters.Add("item", itemNotFoundInContextLanguageId.ToString());
                        filePath = Settings.ItemNotFoundInContextLanguageUrl;
                        Context.Items.Remove("ItemNotFoundInContextLanguage");
                    }
                }
                var _url = WebUtil.AddQueryString(filePath, parameters.SelectMany(kvp => new[] { kvp.Key, kvp.Value }).ToArray());
                context.Response.TrySkipIisCustomErrors = true;
                context.Response.Write(WebUtil.ExecuteWebPage(_url));
            }
            catch (Exception ex)
            {
                Log.Error($"[{typeof(ExecuteRequest).FullName}.{nameof(RedirectOnItemNotFound)}({url})]", ex, this);
                base.RedirectOnItemNotFound(url);
            }
            context.Response.End();
        }
    }
}

You can see above the setting, ItemNotFoundInContextLanguageUrl is used to get the path to the special 404 page. In my case, the _url would be this, as the item Guid is my language 404 content page. 

?sc_itemid={CED4079F-5A2A-4F67-B124-7D3D3CC8AD12}&site=website&sc_lang=fr&item=%7b6EA3AC60-CC83-4A7C-8334-4444C5C3D607%7d&user=extranet%5cAnonymous

This configuration file will also include our two new methods and set up the variables required to run this feature. 

<?xml version="1.0"?>
    <sitecore>
        <pipelines>
            <httpRequestBegin>
                <processor type="Sitecore.Foundation.Response.Pipelines.HttpRequest.ItemLanguageVersionValidator, Sitecore.Foundation.Response"
                           patch:after="processor[@type='Sitecore.Pipelines.HttpRequest.ItemResolver, Sitecore.Kernel']">
                    <databases hint="list">
                        <database>web</database>
                    </databases>
                    <sites hint="list">
                        <site>website</site>
                    </sites>
                    <roots hint="list">
                        <root>/sitecore/content/website/home</root>
                    </roots>
                </processor>
                <processor type="Sitecore.Foundation.Response.Pipelines.HttpRequest.ExecuteRequest, Sitecore.Foundation.Response"
                           patch:instead="processor[@type='Sitecore.Pipelines.HttpRequest.ExecuteRequest, Sitecore.Kernel']"/>
            </httpRequestBegin>
        </pipelines>
        <settings>
            <setting name="ItemNotFoundInContextLanguageUrl" value="?sc_itemid={CED4079F-5A2A-4F67-B124-7D3D3CC8AD12}"/>
        </settings>
    </sitecore>
</configuration>


There you have it! Now, your Users will see a friendly 404 page when the context language is missing, instead of a confusing blank page. Way to go!