Posted in: Comments

Every website should have a friendly 404 page. It’s also nice to give your clients the possibility to edit the content of the 404 page and maybe add a contact form as well. But how do we do it?

First we need a model for the 404 page:

namespace DV.Models
{
    using EPiServer.Core;
    using EPiServer.DataAbstraction;
    using EPiServer.DataAnnotations;

    [ContentType(
        GUID = "7ae4e3c3-d873-4e0b-a4ca-be7435bfd124")]
    [AvailableContentTypes(
       Availability = Availability.None)]
    public class NotFoundPage : PageData
    {
        public override void SetDefaultValues(ContentType contentType)
        {
            base.SetDefaultValues(contentType);

            this.VisibleInMenu = false; 
        }
    }
}

Then we need a template for the page and more importantly, we need to set the status code to 404:

namespace DV.Views
{
    using EPiServer;
    using EPiServer.Editor;
    using log4net;
    using System;
    using DV.Models;

    public partial class NotFound : TemplatePage<NotFoundPage>
    {
        private static readonly ILog Log = LogManager.GetLogger(typeof(NotFound));

        protected override void OnLoad(EventArgs e)
        {
            base.OnLoad(e);

            // Only set status code on GET and in view mode
            if (!PageEditing.PageIsInEditMode && Request.HttpMethod == "GET")
            {
                Log.InfoFormat("404 {0}", Request.QueryString["aspxerrorpath"] ?? Request.RawUrl);

                this.Response.Status = "404 not found";
                this.Response.StatusCode = 404;
            }
        }
    }
}

It’s also good practice to log all the requests that are not found, this way we can determine if we need to set up rewrite rules for some of the missing resources. You would probably do this with Google Analytics as well. Notice that Request.RawUrl will contain the actual requested url!

Now we need to tell the application to route all 404’s to this page. This can be done by redirecting to a hard-coded url, e.g. /page-not-found/ or /views/notfound.aspx. But that’s not that nice. A better way is to just execute the url to the 404 page and keep the requested url in the address bar. This is easily done with some configuration inside the system.webServer section of web.config:

<system.webServer>
	<httpErrors errorMode="Custom">
		<remove statusCode="404" />
		<error statusCode="404" path="/Views/NotFound.aspx" responseMode="ExecuteURL" />
	</httpErrors>
</system.webServer>

The important part here is the responseMode. You could set it to just Redirect, but it’s much more clean to just execute the 404 page and keep the requested url. Then we need to turn off EPiServer’s error handling:

<episerver>
	<applicationSettings globalErrorHandling="Off" />
</episerver>

Best practice is to also to configure customErrors inside system.Web:

<system.web>
	<customErrors defaultRedirect="/Views/NotFound.aspx" mode="Off" redirectMode="ResponseRewrite">
		<error statusCode="404" redirect="/Views/NotFound.aspx" />
	</customErrors>
</system.web>

We do not have ExecuteURL here, but ResponseRewrite is the equivalent. Now all the 404’s will execute our template instead. But wait a minute, the content is not loading! That’s because the url cannot be resolved to any content, hence the 404. So we need to tell which page to actually load. In this case we will add a property to the start page, so an editor can select the 404 page:

[AllowedTypes(new[] { typeof(NotFoundPage) })]
public virtual PageReference NotFoundPage { get; set; }

And in the code-behind of our 404 template we just load that page in the OnPreInit event:

protected override void OnPreInit(EventArgs e)
{
    base.OnPreInit(e);

    var contentLoader = ServiceLocator.Current.GetInstance<IContentLoader>();
    var startPage = contentLoader.Get<StartPage>(ContentReference.StartPage);

    if (!PageEditing.PageIsInEditMode && !ContentReference.IsNullOrEmpty(startPage.NotFoundPage))
    {
        this.CurrentPageLink = startPage.NotFoundPage;
    }
}

Voila! Now we have a working 404 page, where an editor can change the content of the page. There’s only one caveat, what happens if the website is globalized and we have more than one language? We need to load the 404 page in the correct language of course. This is a bit trickier and this is the best solution I came up with. Please let me know if there is a better way of doing this. In the code-behind of the template we just override the InitializeCulture event:

protected override void InitializeCulture()
{
    if (!PageEditing.PageIsInEditMode)
    {
        // Try to find the language id from the actual requested url
        var segments = Request.RawUrl.Split('/');
        var languageSegment = segments.Length > 1 ? segments[1] : string.Empty;

        if (!string.IsNullOrWhiteSpace(languageSegment))
        {
            var languageMatcher = ServiceLocator.Current.GetInstance<ILanguageSegmentMatcher>();

            var languageId = string.Empty;

            if (languageMatcher.TryGetLanguageId(languageSegment, out languageId))
            {
                var currentLanguage = ServiceLocator.Current.GetInstance<IUpdateCurrentLanguage>();

                // Update the language, if language id was found
                currentLanguage.UpdateLanguage(languageId);
            }
        }
    }

    base.InitializeCulture();
}

We’re parsing out the first segment of the requested url, then we’re trying to match that segment with all available language versions of the page, including fallback settings. The correct language will be loaded automatically, if we have a domain for the language and not a segment in the url, but that’s not always the case.