Sitecore 10 Multisite Content Managed Custom 404 Error Pages
Having a custom 404 page is a better experience for your users… but in a multisite setup, you may want a custom 404 that is different per site… better yet, a content managed 404!
In Sitecore 10, you can use a simple add to your Site Definitions as follows (assuming you have a page named 404 in the root of your site):
itemNotFoundUrl="/404"
However, this creates a 302 redirect rather than a true 404 page. To that end, we want to add a Processor to manage these needs. Here are the steps I took to create content managed custom 404 error pages in a Sitecore 10 multisite setup.
Contents
Create Custom 404 Pages Per Site
In the root of each site, create a page named 404. The name is important because it is what we will reference as part of further configuration. This can be as pretty or ugly as you want… but I recommend a nice layout that is content managed.
Set the Site Definition ItemNotFoundURL
In each of your Site Definitions create an entry for ItemNotFoundURL (and perhaps layoutNotFoundUrl if that meets your requirements). In our example the entry would be added as shown in the config below. However, if you stop here, while it will redirect to your custom 404 page… it will do so as 302 redirect and for SEO purposes, we want a 404. Further details of what can be added to the Site Definitions can be found at: Site specific error and 404 page (stockpick.nl) and Walkthrough: Configuring a website to require explicit consent for tracking | Sitecore Documentation
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set" xmlns:role="http://www.sitecore.net/xmlconfig/role/" xmlns:environment="http://www.sitecore.net/xmlconfig/environment/"> <sitecore environment:require="!Local"> <sites> <site name="site-1" inherits="website" set:dictionaryDomain="{YourGUIDHere}" set:targetHostName="" set:hostName="" set:enablePreview="true" set:virtualFolder="/site1" set:physicalFolder="/site1" set:rootPath="/sitecore/content/site1" patch:before="site[@name='siteportal']" /> <site role:require="ContentDelivery" name="site-1" set:enableDebugger="false" set:allowDebug="false" set:cacheRenderingParameters="true" set:renderingParametersCacheSize="25MB" itemNotFoundUrl="/404" set:targetHostName="site1.com" set:hostName="site1.com" /> </sites> </sitecore> </configuration>
Create Custom Processor
This will tie into the HttpRequest and it does a few important things, specifically:
- Based on the type of pages, decide whether to provide the 404 page or not
- Set the status code and corresponding information to a 404 (without this, it will perform a 200 OK as it finds your custom 404 page)
Note, this code was only minorly tweaked based on the very helpful blog post here: Sitecore Custom 404 Page – Till the end (wordpress.com)
using System.IO;
using System.Net;
using System.Web;
using Sitecore;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Pipelines.HttpRequest;
using Sitecore.Web;
namespace MyCustom404Feature.404Error.Pipelines
{
public class CustomNotFoundProcessor : HttpRequestProcessor
{
protected HttpRequestArgs Args { get; set; }
//The below properties will be used to check if the pipeline should run or not, if these are missing, even the launchpad will not open
protected virtual bool LocalPathStartsWithSitecore => this.Args.LocalPath.StartsWith("/sitecore");
protected virtual bool RequestIsForPhysicalFile => File.Exists(System.Web.HttpContext.Current.Server.MapPath(this.Args.Url.FilePath));
protected virtual bool FilePathStartsWithSitecore => this.Args.Url.FilePath.StartsWith("/sitecore");
public override void Process(HttpRequestArgs args)
{
//This is where the main process will happen
Assert.ArgumentNotNull(args, "args");
Args = args;
if (ShouldNotProcess(args))
{
//Skipping the processor
return;
}
//Set the context item to the custom 404 page
Context.Item = Get404Page();
//Return Status Codes to 404
HttpContext.Current.Response.TrySkipIisCustomErrors = true;
HttpContext.Current.Response.StatusCode = (int)HttpStatusCode.NotFound;
HttpContext.Current.Response.StatusDescription = "Page not found";
if (Context.Item == null)
{
return;
}
}
public Item Get404Page()
{
//Getting the 404 page by item path
var err404ItemPath = $"{Context.Site.StartPath}/404";
var error404Item = Context.Database.GetItem(err404ItemPath);
return error404Item;
}
public bool ShouldNotProcess(HttpRequestArgs args)
{
if (IsValidItem)
{
return true;
}
if (LocalPathStartsWithSitecore)
{
return true;
}
if (FilePathStartsWithSitecore)
{
return true;
}
if (RequestIsForPhysicalFile)
{
return true;
}
if (Context.Site == null)
{
return true;
}
if (args.PermissionDenied)
{
return true;
}
return false;
}
protected virtual bool IsValidItem
{
get
{
if (Context.Item == null)
{
return false;
}
if (Context.Item.Versions.Count <= 0)
{
return false;
}
var tenant404 = Get404Page();
if (Context.Item != null && tenant404 != null && Context.Item.ID == tenant404.ID)
{
return false;
}
return (Context.Item.Visualization.Layout != null)
|| !string.IsNullOrEmpty(WebUtil.GetQueryString("sc_layout"));
}
}
}
}
Set the Patch Config to Add the Processor and Server-Side Redirect
We want to enable server-side redirection to ensure we produce our 404 without a 302 via our custom processor. Here is the patch file that corresponds to above. Note that it patches before the Sitecore.Pipelines.HttpRequest.LayoutResolver.
IMPORTANT: You need to have your custom 404 pages at /404 in each site created before deploying this and the custom processor as on Sitecore initialization, it is already looking for these pages.
<?xml version="1.0"?> <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <settings> <!-- USE SERVER-SIDE REDIRECT FOR REQUEST ERRORS If true, Sitecore will use Server.Transfer instead of Response.Redirect to redirect request to service pages when an error occurs (item not found, access denied etc). Out of the box, Sitecore will perform a 302 redirect to the 404 page, to instead immediately serve up the 404 page, the following changes are necessary. Default value: false --> <setting name="RequestErrors.UseServerSideRedirect"> <patch:attribute name="value">true</patch:attribute> </setting> </settings> <pipelines> <httpRequestBegin> <processor patch:before="processor[@type='Sitecore.Pipelines.HttpRequest.LayoutResolver, Sitecore.Kernel']" type="MyCustom404Feature.404Error.Pipelines.CustomNotFoundProcessor, MyCustom404Feature.404Error" /> </httpRequestBegin> </pipelines> </sitecore> </configuration>