Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Sitcore 404 Redirect to Custom Item when Item is Not Found

0.00/5 (No votes)
13 Mar 2018 1  
This article is about how to implement Custom Item Not Found in Sitecore.

Introduction

This article is about implementation of 404 page as an item within Sitecore. There are few things that need to be modified to get an optimistic result after creating 404 (Item Not Found) page in Sitecore. The implementation shown in this article is for education purposes. Readers are expected to implement them using all the best practices that are known to them.

Who Can Read This Article?

This article is for all developers with basic knowledge of Sitecore with MVC. The developer should have installed sitecore and deployed the website.

Background

We will be implementing how to redirect to an Item in sitecore when the URL goes wrong. We will also consider effective measures that need to be taken in order to keep our website visible and rank good in Search Engine Optimization.

We will first create an Item in Sitecore to handle the Item Not Found page. We will then talk through the missing elements that need to be implemented to successfully complete the Item Not found Page.

Using the Code

Implementation

We will just have a glance of the project we will be working on.

After installing the sitecore EXE or ZIP. We will have a default Home site available. We will use the same Home site for our project. In the above figure, you will see that Layout is changed to NewWebSiteLayout. This Layout has been created under Layouts-> SumitPOC folder (you can create where you like). We also see that under Control tab, we have a rendering selected naming Simple Sunil Hi.

The NewWebSiteLayout is as shown below:

The Rendering is as shown below:

The Output of the above is as shown below:

Now that we have an idea about how things will work. Let us try to see what happens when we type the wrong URL.

The result of the above URL is as shown below:

This is the default page that is set by sitecore. We get this page because this static page is been configured in the Sitecore.config file present under website\App_config\.

Our intention is to show a custom Item not found page that can be modified by Content editor whenever required.

Let us start with the implementation.

Like any other item creation, we will create an Item for Item not found. In order to achieve this, just create an Item under Sitecore->Content->Home. Select Standard Template and name it as ItemNotFound.

The structure will be similar to the screen shown below:

We just need to give this Item a rendering that will be called to show Item Not Found page.

Create a rendering under Renderings->SumitPOC->Item Not Found.

The structure will be similar to the screen shown below:

Given below is the implementation of the Controller naming ItemNotFoundController.

using System.Web.Mvc;
namespace SumitPOC.Controllers
{
    public class ItemNotFoundController : Controller
    {
       // GET: ItemNotFound
        public ActionResult Index()
        {
           return View();
        }
    }
}

The view is as shown below:

<div><string>The Item you are Searching is not found....</string></div>

We have successfully created the Item to handle Item Not Found. We need to configure Sitecore to call our page instead of static page given by sitecore. Therefore, we select the path of our ItemNotFound Item and modify the Sitecore.config as shown below:

<setting name="ItemNotFoundUrl" value="/sitecore/service/notfound.aspx"/>

to:

<setting name="ItemNotFoundUrl" value="/sitecore/content/Home/ItemNotFound"/>

Now when we publish the web site and run, we see the below page on Item not found.

The output now is as shown below:

This shows that we have clearly implemented custom ItemNotFound page in sitecore.

Observation: Though we have implemented the custom ItemNotFound page in sitecore, there are few points that we have to consider.

1. Check the URL.

The URL we passed was “http://sumitpoc/11111”.

The URL we got back after execution is http://sumitpoc/sitecore/content/Home/ItemNotFound?item=%2f111111&user=extranet%5cAnonymous&site=website

Consider a Practical Situation

Let us experiment with some live websites.

Type https://stackoverflow.com/questions/49098720/fatal-error-when-hiding-collectionview

And press enter. You will get a page with some information.

Let us observe what happens when we miss type something let's say, we miss typed the part questions to quest.

We get the below page:

Observe the URL. It has not changed.

The reason is if the user has miss typed it. He only has to correct the mistake.

In our case, user has to re type the whole URL again.

We have to solve this in our custom Implementation of ItemNotFound page.

2. Check the Redirect

There are many ways to check the redirect Trace. The most common option is to press Function key “F12”, go to network tab and check the redirects. In this article, we have used the chrome extension “Link Redirect trace”.

Our URL was redirected to the error URL and the status code changed from 302 to 200. The custom page that we have created, exist in Sitecore and the URL has found that page therefore status code is 200. The behavior of redirect is correct technically, but logically, it should be 404 because this is the page that is reached when Item is not found.

Why are we focusing on reducing the redirect? Why are we trying to change 200 to 404?

Search Engine Optimization will rank the site very bad if it goes in endless loop. In our case, 302 to 200 is an endless loop because, logically the page is Item Not Found, but we are getting 200 stating crawl further.

We will also see that 302 to 404 is ending the loop, but still Search Engine Optimization is going one step below to find 404. So we need to eliminate this 302 as well.

Again, let us get back to our previous example and observe the behavior.

Let us give the same URL “https://stackoverflow.com/quest/49098720/fatal-error-when-hiding-collectionview

The page shown here is again a custom page but the status code is 404. We have to solve this in our custom Implementation of ItemNotFound page.

Conclusion

  1. Implementation completed successfully with 2 major modifications required.
  2. Wrong Url Typed by the Client should not change.
  3. There should be a “404” page in place of “302” to “200”

Implementation of Major Modifications

In order to fulfill the major modification requirement, we will have to use server side redirect I.e 301 (server transfer). Using server transfer, the URL does not change. To get 404 status, we will have to implement few thing under pipeline.

Let us start with the implementation. Just to get overview, this code will be common for the application therefore; we will be writing a code under Foundation Folder.

In order to make 301 redirect, we need to go to the sitecore.config file and change the value of RequestErrors.UseServerSideRedirect to True. As shown below:

<setting name="RequestErrors.UseServerSideRedirect" value="true"/>

Let me put the code for all the Cs class here.

1) CustomExecuteRequest.cs
using Sitecore;
using Sitecore.Configuration;
using Sitecore.Pipelines.HttpRequest;
using Sitecore.Web;
using System.Web;

namespace SumitPOC.Foundation.Pipeline.HttpRequest
{
    public class <code>CustomExecuteRequest</code> : <code>ExecuteRequest</code>
    {
        protected override void PerformRedirect(string url)
        {
            if (   Context.Site == null
                || Context.Database == null
                || Context.Database.Name == "core")
                {
                    return;
                }
                //need to retrieve not found item to account
                //for sites utilizing virtualFolder attribute
                //site this under <setting name="ItemNotFoundUrl"
                //value="/sitecore/content/Home/ItemNotFound"/>
                //the value "/sitecore/content/Home/ItemNotFound" is the Custom item you have created.
                var notFoundItem = Context.Database.GetItem(Settings.ItemNotFoundUrl);

                if (notFoundItem == null)
                {
                     return;
                 }
                //This code works along with the commented constructor.
                //BaseSiteManager, BaseItemManager and BaseLinkManager are
                // are present in SItecore Kernel 10.0. We are using 8.1 so not of any use for us.
                //var notFoundUrl = _baseLinkManager.GetItemUrl(notFoundItem);
                if (string.IsNullOrWhiteSpace(Settings.ItemNotFoundUrl))
                {
                   return;
                }
                //under Sitecore.config, make this true.
                //<setting name="RequestErrors.UseServerSideRedirect" value="true"/>
             if (Settings.RequestErrors.UseServerSideRedirect)
            {
                HttpContext.Current.Server.TransferRequest(Settings.ItemNotFoundUrl);
            }
            else
            {
                WebUtil.Redirect(Settings.ItemNotFoundUrl, false);
            }
        }
    }
}

The using statements are deliberately added.

Once we have CustomExecuteRequest file ready, we need to attach it to the pipeline.

In order to add it to the pipeline, we have to know one more new thing. We have to create a folder under App_Config -> Include under this folder, create a custom Folder that starts with Z.(a Z and Dot).

Why we need to do this is because, at run time sitecore combines all the configs under Include -> Z. type folder and combines them with the main Web.config.

What Did We Achieve By This?

If we want to migrate our code from Sitecore one version to higher, anything that is under Include folder will not be replaced. Therefore, our custom logic will not break even after migration.

Given below is how we have implemented it under Include folder.

(In case include folder is not present in your MVC application, just copy the folder and added it in your solution. Include it in your project.)

CustomErrorPipelines.config is as shown below:

    <?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
   <pipelines>
     <httpRequestBegin>
        <!-- Reads updated "RequestErrors.UseServerSideRedirect" 
             value and transfers request to LayoutNoutFoundUrl or ItemNotFoundUrl, 
             preserving requested URL -->
        <processor type="SumitPOC.Foundation.Pipeline.HttpRequest.CustomExecuteRequest, 
         SumitPOC" resolve="true"

                   patch:instead="*[@type='Sitecore.Pipelines.HttpRequest.ExecuteRequest, 
                   Sitecore.Kernel']"/>
      </httpRequestBegin>
    </pipelines>
  </sitecore>
</configuration>
2) Set404StatusCode.cs
using Sitecore.Configuration;
using Sitecore.Pipelines.HttpRequest;
using SumitPOC.Common;
using System;
using System.Web;

namespace SumitPOC.Foundation.Pipeline.HttpRequest
{
    public class <code>Set404StatusCode</code> : <code>HttpRequestBase</code>
    {
        protected override void Execute(HttpRequestArgs args)
        {
            // retain 500 response if already set
            if (HttpContext.Current.Response.StatusCode >= 500 || args.Context.Request.RawUrl == "/")
                return;

            // return if request does not end with value set in ItemNotFoundUrl, i.e. successful page
            if (!args.Context.Request.Url.LocalPath.EndsWith
               (Settings.ItemNotFoundUrl, StringComparison.InvariantCultureIgnoreCase))
                return;

            HttpContext.Current.Response.TrySkipIisCustomErrors = true;
            HttpContext.Current.Response.StatusCode = SiteCoreRouteValues.PageNotFoundCode;
            HttpContext.Current.Response.StatusDescription = 
                             SiteCoreRouteValues.PageNotFoundDescription;
        }
    }
}
3) HttpRequestBase.cs
using Sitecore;
using Sitecore.Pipelines.HttpRequest;
using System;
using System.Linq;

namespace SumitPOC.Foundation.Pipeline.HttpRequest
{
    public abstract class HttpRequestBase : HttpRequestProcessor
    {
        /// <summary>
        /// allowedSites and disallowedSites are mutually exclusive, use one or the other
        /// in the event both are used, disallowedSites is enforced
        /// </summary>
        public string allowedSites { get; set; }
        public string disallowedSites { get; set; }
        public string ignoredPaths { get; set; }
        public string ignoredModes { get; set; }
        /// <summary>
        /// allowedDatabases and disallowedDatabases are mutually exclusive, use one or the other
        /// in the event both are used, disallowedDatabases is enforced
        /// </summary>
        public string allowedDatabases { get; set; }
        public string disallowedDatabases { get; set; }

        private const string EditMode = "Edit";

        /// <summary>
        /// Overridden HttpRequestProcessor method
        /// </summary>
        /// <param name="e;args"e;></param>
        public override void Process(HttpRequestArgs args)
        {
            if (IsValid(args))
            {
                Execute(args);
            }
        }

        protected abstract void Execute(HttpRequestArgs args);

        protected virtual bool IsValid(HttpRequestArgs hArgs)
        {
            return SitesAllowed()
                && PathNotIgnored(hArgs)
                && ModeNotIgnored()
                && DatabaseAllowed();
        }

        private bool SitesAllowed()
        {
            // httpRequest processors should never run without a context site
            if (Context.Site == null)
                return false;

            var contextSiteName = Context.GetSiteName();

            if (string.IsNullOrWhiteSpace(contextSiteName))
                return false;

            // disallow checked first to trump an allowance
            if (!string.IsNullOrWhiteSpace(disallowedSites))
            {
                return !disallowedSites
                    .Split(',')
                    .Select(i => i.Trim())
                    .Any(siteName => string.Equals
                    (siteName, contextSiteName, StringComparison.CurrentCultureIgnoreCase));
            }

            if (!string.IsNullOrWhiteSpace(allowedSites))
            {
                return allowedSites
                    .Split(',')
                    .Select(i => i.Trim())
                    .Any(siteName => string.Equals(siteName, 
                         contextSiteName, StringComparison.CurrentCultureIgnoreCase));
            }

            return true;
        }

        private bool PathNotIgnored(HttpRequestArgs hArgs)
        {
            if (string.IsNullOrWhiteSpace(ignoredPaths))
                return true;

            var ignoredPath = ignoredPaths
                .Split(',')
                .Select(i => i.Trim())
                .Any(path => hArgs.Context.Request.RawUrl.StartsWith
                    (path, StringComparison.CurrentCultureIgnoreCase));

            return !ignoredPath;
        }

        private bool ModeNotIgnored()
        {
            if (string.IsNullOrWhiteSpace(ignoredModes))
                return true;

            var modes = ignoredModes.Split(',').Select(i => i.Trim());

            var isEditor = Context.PageMode.IsExperienceEditor;

            return !modes.Any(mode =>
              (mode == "Edit" && isEditor) ||
              (mode == "Preview" && Context.PageMode.IsPreview)
            );
        }

        private bool DatabaseAllowed()
        {
            // httpRequest processors should never run without a context database
            if (Context.Database == null)
                return false;

            var contextDatabaseName = Context.Database.Name;

            // disallow checked first to trump an allowance
            if (!string.IsNullOrWhiteSpace(disallowedDatabases))
            {
                return !disallowedDatabases
                    .Split(',')
                    .Select(i => i.Trim())
                    .Any(database => string.Equals
                    (database, contextDatabaseName, StringComparison.CurrentCultureIgnoreCase));
            }

            if (!string.IsNullOrWhiteSpace(allowedDatabases))
            {
                return allowedDatabases
                    .Split(',')
                    .Select(i => i.Trim())
                    .Any(database => string.Equals(database, contextDatabaseName, 
                                                   StringComparison.CurrentCultureIgnoreCase));
            }

            return true;
        }
    }
}

We need to attach the Sitcore404StatusCode.cs to pipeline.

The code for CustomeErrorPipeline404Status.config is as shown below:

    <?xml version="e;1.0"e;?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <httpRequestEnd>
        <!-- Sets a 404 status code on the response -->
        <processor type="SumitPOC.Foundation.Pipeline.HttpRequest.Set404StatusCode, 
                   SumitPOC" resolve="true"

            patch:instead="*[@type='Sitecore.Pipelines.HttpRequest.EndDiagnostics, Sitecore.Kernel']"/>
      </httpRequestEnd>
    </pipelines>
  </sitecore>
</configuration>

Publish your code. Check if thing work as expected.

Now, we should have handled all the Major Modifications.

The result set will now be:

Even after successfully completing the Major Modifications, we still have one issue and that is with Image rendering.

If we have a URL similar to http://sumitpoc/~/media%20library/Images/SumitPOC/testimonial_video.

And the user has miss typed the URL, he will get 302 to 404.

In most of the case, Images are inserted using sitecore GUI and through code. No one added them In URL. Yet we have the solution and will be discussed later.

Points of Interest

Note

Error handling.

Start your website. Try to attach Debugger in Visual Studio. Press CTRL + ALT+ P, then find w3wp.exe, attach the process.

Even after attaching, if your debugger is not getting attached, then do the following:

  1. If you publish your code into Website bin, then do not do this.Other developers, just copy the DLL you changed, along with PDB file of the DLL, and paste it into the website’s bin folder. Run the website, now add debugger point and try to attach the W3Wp.exe.
  2. Even after following the first approach, your debugger is not attached; go to the app pool of your website.

    Your app pool is as shown above.

    Set Enabled 32-bit Application to True. Now start your website and set debugger into your DLL and try to attach the W3WP.exe.

  3. Even after following the 2nd point your debugger is not getting attached, then go to Visual Studio 2010. Go to tools -> Options -> Debugging -> Enable Just In My Code and uncheck it as shown below.

  4. If your code starts to work, means the DLL are from the release mode and not from the Debug mode. Your build publish target is publishing the filer from Release mode (do not know why?). Just manually build the code with debug selected as shown below:

    Copy the DLLs and paste it in your website folder. Attach W3WP process. Now it should attach and work as required.

    Why Copy paste? The reason is, we do not know if the publish we have set is working as expected.

    During diagnosis or trouble shooting, we should avoid complex steps.

History

  • 13th March, 2018: Initial version

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here