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!
