26Apr, 2021
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!