404 error pages for multi-site solutions

This article will guide you how to setup custom 404 error pages for a multi-site solution by implementing the IContentFinder interface.

IContentFinder is part of the Umbraco request pipeline, and Umbraco itself has a number for different content finders - eg. one for looking up content from a nice URL and another one if only the ID is specified in the URL. When you enter a URL for your Umbraco website, Umbraco will go through these content finders one by one, and if a content finder knows how to handle a URL, Umbraco will stop with that content finder. The last content finder (named ContentFinderByNotFoundHandlers) is the one responsible for the default and ugly not found page. To summarize this, Umbraco has the following content finders by default:

  • Umbraco.Web.Routing.ContentFinderByPageIdQuery
  • Umbraco.Web.Routing.ContentFinderByNiceUrl
  • Umbraco.Web.Routing.ContentFinderByIdPath
  • Umbraco.Web.Routing.ContentFinderByNotFoundHandlers

Plan of action

I have created a demo solution using the new Umbraco 7.2, but the code should work the same from at least 6.1.x and up. You can try and download the solution from GitHub or simply view the individual files via your browser. The login for the backoffice is demo for the both the username and password.

The demo solution has three types of sites; each having their own way to handle 404 error pages.

The solution has a single main site, so for this, I have just hardcoded the error page to a specific ID. If looking at this way alone, it would be somewhat similar to configuring error pages via umbracoSettings.config. Besides the error page being served as the error page, it is just a normal page (alias MainPage). The site is configured with the domain awesome.localhost.bjerner.dk

The solution also has a type for an institution site (or sub site if you will). For this type of site, the error page is manually selected via a content picker in the settings for the site in the backoffice. We then extract the specified value in our content finder. In similar way, the 404 error page is a standard page of the type InstitutionPage. The site is configured with the domain institution.localhost.bjerner.dk.

Finally there is also a site type for campaigns. For a campaign site, you should manually create an error page of the type CampaignNotFound directly under the root node of the site. If there are multiple error pages for a given site, the first one is selected. For my example campaign site, the domain is campaign.localhost.bjerner.dk.

All three domains are configured to point to 127.0.0.1, so feel free to add them on your local IIS to play around with the demo solution.

Creating a content finder

At first, let us create the basics for a content finder, and then come back to the logic later.

using System.Linq;
using System.Web;
using Umbraco.Core.Models;
using Umbraco.Web;
using Umbraco.Web.Routing;
using umbraco.cms.businesslogic.web;

namespace FourOhFour.Routing {

    public class FourOhFourContentFinder : IContentFinder {

        public bool TryFindContent(PublishedContentRequest contentRequest) {

            // Return whether an error page was found
            return contentRequest.PublishedContent != null;

        }

    }

}

The IContentFinder interface only describes a single method - that is TryFindContent. The method should return a boolean - true if the content finder could handle the request, otherwise false.

Our content finder will not do anything by itself - we have to tell Umbraco about it. Simply adding our content finder to the end of the list of content finders won't do much since ContentFinderByNotFoundHandlers is the one responsible for showing the standard 404 page, and Umbraco will therefore never reach our custom content finder. So we have to add it right before ContentFinderByNotFoundHandlers. This is done in the following way:

using FourOhFour.Routing;
using Umbraco.Core;
using Umbraco.Web.Routing;

namespace FourOhFour {

    public class Startup : ApplicationEventHandler {

        protected override void ApplicationStarting(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) {

            // Add our custom content finder
            ContentFinderResolver.Current.InsertTypeBefore<ContentFinderByNotFoundHandlers, FourOhFourContentFinder>();

        }

    }

}

We hook into the Umbraco starting event, and insert our own content finder FourOhFourContentFinder right before ContentFinderByNotFoundHandlers, but still after the first three content finders.

Back to the logic in our content finder, the first thing to do is to check what type of site we're dealing with. We can find the site by looking up the domain (so a domain must be configured for the site). We can do that with the following lines (including some error handling):

// Get the current domain name
string domainName = HttpContext.Current.Request.ServerVariables["SERVER_NAME"];

// Get the root node id of the domain
int rootNodeId = Domain.GetRootFromDomain(domainName);

// Return if a root node couldn't be found
if (rootNodeId <= 0) return false;

// Find the root node from the ID
IPublishedContent root = (rootNodeId > 0 ? UmbracoContext.Current.ContentCache.GetById(rootNodeId) : null);

// Return FALSE if the root node wasn't found (AKA move on to the next content finder)
if (root == null) return false;

At this point, the variable root is now the content node of the site in question. We should now check the document type of the site. For the main site, the document type alias is MainSite. Since we for the main site know, that the error page has the ID of 1059, we can simply get it from the content cache like below:

// Handle error pages for each site type
switch (root.DocumentTypeAlias)  {

    case "MainSite": {

        // Get the error page from the hardcoded ID
        contentRequest.PublishedContent = UmbracoContext.Current.ContentCache.GetById(1059);

        break;

    }

}

Even though the error page for an institution site isn't hardcoded, the approach is very similar - now we just have to get the ID of the error page from the site settings first:

// Handle error pages for each site type
switch (root.DocumentTypeAlias)  {

    case "InstitutionSite": {

        int errorPageId = root.GetPropertyValue<int>("notFoundPage");

        // Get the error page from the specified ID in the "notFoundPage" property
        contentRequest.PublishedContent = UmbracoContext.Current.ContentCache.GetById(errorPageId);

        break;

    }

}

For a campagin site, we should find a child node with the document type alias CampaignNotFound. The code for that would look like:

// Handle error pages for each site type
switch (root.DocumentTypeAlias)  {

    case "CampaignSite": {

        // Find the first child with the "CampaignNotFound" document type
        contentRequest.PublishedContent = root.Children.FirstOrDefault(x => x.DocumentTypeAlias == "CampaignNotFound");

        break;

    }

}

That's it!!! Our content finder can now handle error pages for all three types of sites. The entire FourOhFourContentFinder class will now look like:

using System.Linq;
using System.Web;
using Umbraco.Core.Models;
using Umbraco.Web;
using Umbraco.Web.Routing;
using umbraco.cms.businesslogic.web;

namespace FourOhFour.Routing {

    public class FourOhFourContentFinder : IContentFinder {

        public bool TryFindContent(PublishedContentRequest contentRequest) {

            // Get the current domain name
            string domainName = HttpContext.Current.Request.ServerVariables["SERVER_NAME"];

            // Get the root node id of the domain
            int rootNodeId = Domain.GetRootFromDomain(domainName);

            // Return if a root node couldn't be found
            if (rootNodeId <= 0) return false;

            // Find the root node from the ID
            IPublishedContent root = (rootNodeId > 0 ? UmbracoContext.Current.ContentCache.GetById(rootNodeId) : null);

            // Return FALSE if the root node wasn't found (AKA move on to the next content finder)
            if (root == null) return false;

            // Handle error pages for each site type
            switch (root.DocumentTypeAlias)  {

                case "MainSite": {

                    // Get the error page from the hardcoded ID
                    contentRequest.PublishedContent = UmbracoContext.Current.ContentCache.GetById(1059);

                    break;

                }

                case "InstitutionSite": {

                    int errorPageId = root.GetPropertyValue<int>("notFoundPage");

                    // Get the error page from the specified ID in the "notFoundPage" property
                    contentRequest.PublishedContent = UmbracoContext.Current.ContentCache.GetById(errorPageId);

                    break;

                }

                case "CampaignSite": {

                    // Find the first child with the "CampaignNotFound" document type
                    contentRequest.PublishedContent = root.Children.FirstOrDefault(x => x.DocumentTypeAlias == "CampaignNotFound");

                    break;

                }

            }

            // Return whether an error page was found
            return contentRequest.PublishedContent != null;

        }

    }

}

Epilogue

Once you get the hang of it, the three ways to handle error pages shown is this article, are quite simple. IContentFinder is very powerful, and can be used for even more advanced use cases - not just 404 error pages - so I can really recommend to look further into it. An example for another use case could be virtual URLs - eg. making content appear in other locations than what they are in the backoffice.

Links

Anders Bjerner

Anders is on Twitter as