20 July 2014

301 Redirect Handling

In previous post we saw that we could create a Custom Field and store values we wanted in it. In our case we took a simple example to store data for a 301 redirect. We saw the use of a button on the field to regenerate the data whenever we wanted... Also for that example we chose to store the data in Json format. This was totally arbitratory and you can choose to store your data in whatever format you need. Well let's try to finish the code to see how we can integrate that plus pipeline action (like here) to handle the 301 redirects.

1- We need to define our templates

We will need 3 templates
  • The Redirection Status: 301, 302... this will help us to define the status code and the status text to pass to the Response. The template will looks like:


  • The Domain Redirection Item: This item will store the Json list objects as per our previous post as well as the domain we want the redirection to be applicable. Here we are thinking about 301 for multi-sites. Indeed, if your sitecore instance has multiple sites and you are setting up a redirection for /applications, this might be a valid  redirection for Site 1 but /applications could exist on Site 2... So better to plan for it. Now, when we had a look at the Domain Dictionary, we saw that in the configuration of your site you will have the attribute "dictionaryDomain". well we will use the same attribute to find our site domain for redirection. The template should look like the following (note the second field is our custom field):

  • Finally we will need a Redirect Item template. This template will have 3 fields: the requested URL (single line text), Redirect To: droptree that can point to a media or content item... and Status Code: droptree pointing at our Redirection Status folder as per above image...\

 2- Custom Field

well we already saw that part... under our "Redirections", we will have our Domain Item that will store the domain and the Json Data... Under this domain item, we will define all the redirection items.
Our Custom Field, will have a button which will trigger an action to get all the children and build the Json List of objects... You can follow this post to setup the field.

So we now that the data we are going to use on the HTTP Request Begin pipeline is stored on the domain item, so theoretically, we just need to retrieve this item, get the data, deserialise and get the relevant redirect if any... then proceed with redirection

3- Configuration

Easy part, we just need a processor on the HTTPRequestBegin Piepline:

      < httpRequestBegin>
        < processor type="MySite.Business.Sitecore.Pipelines.PermanentRedirect, MySite.Business" patch:after="processor[@type='Sitecore.Pipelines.HttpRequest.ItemResolver, Sitecore.Kernel']" />
      < /httpRequestBegin>

4- Code

Now we have our data, we have our pipeline configure, let's intercept the request and handle some 301 shall we?
Create your class PermanentRedirect based on the HttpReqiestProcessor...

namespace MySite.Business.Sitecore.Pipelines
{
    /// 
    /// This processor will check for 301 redirects
    /// 
    public class PermanentRedirect : HttpRequestProcessor
    {
        public override void Process(HttpRequestArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            if (Context.Database == null || args.Url.ItemPath.Length == 0 || Context.Site == null)
                return;
        }
    }
}

Now let's think about what we will need:

we will need a method to set the Response: status, status code, header for the location to redirect to...
So let's create that method. Don't forget, you can modify the response from the Args.Context:

        /// 
        /// Set the Header for the reponse
        /// 
        /// 
        /// 
        /// 
        /// 
        private void SetResponse(string redirectTo, string status, int statusCode, HttpRequestArgs args)
        {
            args.Context.Response.Status = status;
            args.Context.Response.StatusCode = statusCode;
            args.Context.Response.AddHeader("Location", redirectTo);
            args.Context.Response.End();
        }

The next thing we will need is a URL resolver. If you remember on our Json object we will store the RedirectTo value as Sitecore ID (to either a content or a media). So we would need a method to get the item and get either the content URL or a media URL if it is pointing at a document... Notes, that we could have tried to store the nice URL on the Json object but the jSon object is generate from the Custom Field button which would have the Sitecore Context Site as Shell so you would have to pass your website context as UrlOptions. Also you want to check that the item you are redirecting to exsits in your delivery site so it is always better to resolve this item on the pipeline action...

        /// 
        /// This will resolve either the media URL or the Content URL
        /// 
        /// 
        /// 
        private string GetSitecoreUrl(Item redirectToItem)
        {
            string mediaLibraryPath = Settings.GetSetting(SitecoreSettings.MediaPath).ToLower();

            // if the item is not a media item
            if (!redirectToItem.Paths.Path.StartsWith(mediaLibraryPath))
            {
                return LinkManager.GetItemUrl(redirectToItem);
            }

            // media item
            var mediaItem = (MediaItem)redirectToItem;
            var mediaUrl = MediaManager.GetMediaUrl(mediaItem);
            return StringUtil.EnsurePrefix('/', mediaUrl);

        }


So now we have our methods, we can start with the main business logic. The first thing we want is to get the root item for our redirection. This root item is the parent of all the Domain Redirection Items. This will allow us to go retrieve all Domain Redirection Items, and select only the ones matching our context.Site.DictionaryDomain:

            String domain = Context.Site.DictionaryDomain;
            if (string.IsNullOrEmpty(domain))
                return;

            // Root for the redirect folder
            Item rootFolder = Context.Database.GetItem(new SC.Data.ID(ItemId.System.Redirection.RedirectionRootFolder));
            if (rootFolder == null)
                return;

            if(!rootFolder.HasChildren)
                return;

            var validDomainItems = rootFolder.Children.Where(x => x.TemplateID.ToGuid() == new Guid(TemplateIds.System.RedirectionDomain)
                                                && x[FieldIds.System.RedirectionDomain.SiteDomain].ToLower() == domain.ToLower());


            if (!validDomainItems.Any())
                return;


Finally we can now loop through all the valid Domain Redirect Item, Get the Json Data field, Deserialize and find the relevant item to redirect to:

                    List redirectItems = (List)Newtonsoft.Json.JsonConvert.DeserializeObject(dataField, typeof(List));
                    if(redirectItems.Any(x=> x.redirectFrom.ToLower().Contains(requestedPath.ToLower())))
                    {
                        RedirectItem item = redirectItems.First(x =>  x.redirectFrom.ToLower().Contains(requestedPath.ToLower()));

                        // Only get the URL at this point to ensure permission, site context...
                        var redirectToItem = SC.Context.Database.GetItem(item.redirectTo);
                        if (redirectToItem == null)
                            continue;

                        string redirectToUrl = this.GetSitecoreUrl(redirectToItem);

                        SetResponse(redirectToUrl, item.status, item.statusCode, args);
                        return;
                    }



So all together, that could looks like something:
namespace MySite.Business.Sitecore.Pipelines
{
    /// 
    /// This processor will check for 301 redirects
    /// 
    public class PermanentRedirect : HttpRequestProcessor
    {
        public override void Process(HttpRequestArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            if (Context.Database == null || args.Url.ItemPath.Length == 0 || Context.Site == null)
                return;

            if (!Settings.GetSetting(SitecoreSettings.ValidSites).ToLower().Contains(Context.Site.Name))
                return;

            String domain = Context.Site.DictionaryDomain;
            if (string.IsNullOrEmpty(domain))
                return;

            // Root for the redirect folder
            Item rootFolder = Context.Database.GetItem(new SC.Data.ID(ItemId.System.Redirection.RedirectionRootFolder));
            if (rootFolder == null)
                return;

            if(!rootFolder.HasChildren)
                return;

            var validDomainItems = rootFolder.Children.Where(x => x.TemplateID.ToGuid() == new Guid(TemplateIds.System.RedirectionDomain)
                                                && x[FieldIds.System.RedirectionDomain.SiteDomain].ToLower() == domain.ToLower());


            if (!validDomainItems.Any())
                return;

            // get the actual request
            var requestedUrl = HttpContext.Current.Request.Url.ToString();
            var requestedPath = HttpContext.Current.Request.Url.AbsolutePath;

            // Get XML from the field... 
            foreach (var redirectDomainItem in validDomainItems)
            {
                string dataField = redirectDomainItem[FieldIds.System.RedirectionDomain.RedirectionData];
                if (string.IsNullOrEmpty(dataField))
                    continue;

                try
                {
                    //Deserialize the List
                    List redirectItems = (List)Newtonsoft.Json.JsonConvert.DeserializeObject(dataField, typeof(List));
                    if(redirectItems.Any(x=> x.redirectFrom.ToLower().Contains(requestedPath.ToLower())))
                    {
                        RedirectItem item = redirectItems.First(x =>  x.redirectFrom.ToLower().Contains(requestedPath.ToLower()));

                        // Only get the URL at this point to ensure permission, site context...
                        var redirectToItem = SC.Context.Database.GetItem(item.redirectTo);
                        if (redirectToItem == null)
                            continue;

                        string redirectToUrl = this.GetSitecoreUrl(redirectToItem);

                        SetResponse(redirectToUrl, item.status, item.statusCode, args);
                        return;
                    }
                }
                catch (Exception ex)
                {
                    SC.Diagnostics.Log.Warn("Could not deserialize the Json for redirection Object - ex: " + ex.StackTrace , this);
                }
 
            }
        }

        /// 
        /// Set the Header for the reponse
        /// 
        /// 
        /// 
        /// 
        /// 
        private void SetResponse(string redirectTo, string status, int statusCode, HttpRequestArgs args)
        {
            args.Context.Response.Status = status;
            args.Context.Response.StatusCode = statusCode;
            args.Context.Response.AddHeader("Location", redirectTo);
            args.Context.Response.End();
        }

        /// 
        /// This will resolve either the media URL or the Content URL
        /// 
        /// 
        /// 
        private string GetSitecoreUrl(Item redirectToItem)
        {
            string mediaLibraryPath = Settings.GetSetting(SitecoreSettings.MediaPath).ToLower();

            // if the item is not a media item
            if (!redirectToItem.Paths.Path.StartsWith(mediaLibraryPath))
            {
                return LinkManager.GetItemUrl(redirectToItem).Replace("/site content", "");
            }

            // media item
            var mediaItem = (MediaItem)redirectToItem;
            var mediaUrl = MediaManager.GetMediaUrl(mediaItem);
            return StringUtil.EnsurePrefix('/', mediaUrl);

        }
    }
}



You can now try to call your redirect URL and see the redirection happening...

2 comments:

  1. Thank you for sharing such an informative article. I really hope I can see other interesting posts. Keep up the good work!


    Melbourne App Developer

    ReplyDelete