Sitecore custom Urls
Home

Sitecore Custom Urls

Author: Ajit Sharma | Categories: Sitecore CMS, CMS

Overview

In Sitecore, the URL is generated by the structure of items you put in place in the content tree (by default). For example, if your “Careers” item in Sitecore content tree looks like this:

  • Sitecore
    • Content
      • Home
        • About Our Company
          • History
          • Careers

The URL of your Careers page will look like this:

www.yourcompany.com/About Our Company/Careers.aspx

Sitecore determines the default URL for an item based on its path; for instance the default URL for /sitecore/content/home/about our company/careers/jobs is www.yourcompany.com/about/our company/careers/jobs.aspx. In some cases, it is useful to have shorter URLs which map to longer paths, for instance, www.yourcompany.com/jobs.aspx may be preferable for marketing materials (email campaigns, print advertisements, etc.). Sitecore supports alternate URLs through a feature known as aliases. This takes people to the same page with a lot fewer words to cross your eyes.

There is another module “Sitecore Redirect Manager” available at http://marketplace.sitecore.net/en/Modules/Sitecore_Redirect_Manager.aspx which can be used for redirecting. This module helps you to keep the rank and search rating for pages (whole sections if needed) which were replaced, moved or removed from your site.

Both the Aliases the Redirection module are good for some generic scenarios, but a custom approach that I have described below is better for some other scenarios mentioned below.

  • You want Sitecore to generate the Custom URLs when item is linked in Rich Text Editor or Sitecore.LinkManager is used to get the Url of the item. By default, the Sitecore Link Manager does not take account of aliases or redirection mappings. This means that the Links generated by Sitecore will point to the Original Url.
  • You do not want to add/update/delete the aliases or redirection entries manually whenever a new item is added/update/deleted as this could easily be missed.
  • You don’t want to compromise on performance. Both the approaches might pose a performance concern because the mappings are stored in a flat list.

Sometimes, what we want is the freedom to create URLs which are independent of the content tree structure. Also, the approach should be free from the limitations that we discussed above. For this, we need to teach Sitecore to generate custom URLs based on our custom rules. In addition, we would need a component to process such custom URL rules.

Case Study

In case, of one of our clients, there were over 2500 people for whom items were created under “People” folder and further grouped in Folders named “A”, “B”, “C” etc. based on the Last name. The client wanted that URL of each of the biography pages should be based on the person’s Email id. For example, the person with email id as john.doe@yourcompanyname.com should be accessible via the following URL.

www.yourcompany.com/john-doe

Solution to cater to this requirement

When it comes to handling any custom URL handling requirements, there are mainly two components we have to deal with.

1. Custom Link Provider: This custom logic here will generate the custom URL.

2. Custom Item Resolver: The custom logic here will attempt to resolve a valid item in the content tree by the custom URL.

How to create Custom Link Provider?

1. Simply inherit a class from Sitecore.Links.LinkProvider and override the GetItemUrl(...) method.

namespace CustomLibrary.Links
{
   public class CustomLinkProvider : Sitecore.Links.LinkProvider
   {
      public override string GetItemUrl(Sitecore.Data.Items.Item item,
         Sitecore.Links.UrlOptions options)
      {
         if (/* my condition of when to create custom url */)
         {
            /*generate and return custom url*/
            return CustomUrlManager.GetCustomUrl(item);
         }
         /*else return the default url generated by Sitecore*/
         return base.GetItemUrl(item, options);
      }
   }
}

2. After doing this, change the defaultProvider in the linkManager (in web.config) to your custom class like:

<linkManager defaultProvider="custom">
   <providers>
      <clear />
      <add name="sitecore" type="Sitecore.Links.LinkProvider, Sitecore.Kernel" addAspxExtension="true"
         alwaysIncludeServerUrl="false" encodeNames="true" languageEmbedding="never" languageLocation="filePath"
         shortenUrls="true" useDisplayName="false" />
      <add name="custom" type="CustomLibrary.Links.CustomLinkProvider, CustomLibrary" addAspxExtension="true"
         alwaysIncludeServerUrl="false" encodeNames="true" languageEmbedding="never" languageLocation="filePath"
         shortenUrls="true" useDisplayName="false" />
   </providers>
</linkManager>

Sitecore Pipelines and Processors

Pipelines define a sequence of processors that implement a function, such as defining the Sitecore context for an HTTP request. Pipelines assist with encapsulation, flexible configuration, separation of concerns, testability and other objectives. Some of the key pipelines include:

  • <initialize>: Initializes the Sitecore application.
  • <preprocessRequest>: Invoked for each HTTP request managed by ASP.NET, but aborted for some requests.
  • <httpRequestBegin>: Defines the Sitecore context. Invoked for each HTTP request not directed to ASP.NET by the <preprocessRequest> pipeline. It basically contains the request processing logic. We’ll be adding a custom processor in this pipeline to map the custom URLs with Sitecore items.

For more information around pipelines, see John West’s blog post Important Pipelines in the Sitecore Digital Marketing System. For information about creating and invoking pipelines, see Creating and running custom pipelines in Sitecore by Alistair Deneys.

Each processor in a pipeline contains a method named Process() that accepts a single argument and returns void. This method should return immediately if the processing context is not relevant to the processor. A processor can abort the pipeline, preventing Sitecore from invoking subsequent processors.

The processor ItemResolver is responsible for getting the Sitecore context item by the incoming URL.

How to create Custom Item Resolver?

1. For creating Custom Item Resolver, we’ll inherit a class from Sitecore.Pipelines.HttpRequest.HttpRequestProcessor and override the GetItemUrl(...) method.

namespace CustomLibrary
{
   public class CustomUrlResolver : HttpRequestProcessor
   {
      public override void Process(HttpRequestArgs args)
      {
         Assert.ArgumentNotNull(args, "args");

         /*In Case Sitecore has mapped the item, do not do anything and simply return*/
         if (Context.Item != null || Context.Database == null || args.Url.ItemPath.Length == 0) return;

         /*If not, Check for item based on the FilePath*/
         Item contextItem = CustomUrlManager.GetItemByFilePath(args.Url.FilePath);
         if (contextItem != null) Context.Item = contextItem;
      }
   }
}

2. In web.config, add new processor to handle the custom URL requests. Make sure you add it just after “ItemResolver” processor. The proper order is important.

<processor type="Sitecore.Pipelines.HttpRequest.ItemResolver, Sitecore.Kernel"/>
<!-- the proper order is important -->
<processor type="CustomLibrary.CustomUrlResolver, CustomLibrary"/>
<processor type="Sitecore.Pipelines.HttpRequest.LayoutResolver, Sitecore.Kernel"/>

Now after going through both custom Link Provider and Item Resolver, you must have noticed that we have used “CustomUrlManger” class in both Link Provider and Item Resolver classes. This class contains wrapper functions to generate custom URL and map item with the custom URL.

namespace CustomLibrary
{
   public static class CustomUrlManager
   {
      /*This function takes Sitecore item as argument and returns true if item has a Custom URL and false if not.*/
      public static bool CustomUrlExists(Item item)
      {
         bool aliasExists=false;

         /*Check if item has a custom URL. Eg. Check for Template Id/Template name of item */
         /*Eg. If Item is based on People Template then set "aliasExists" as true*/
         /*return "aliasExists" [true/false]*/
	return aliasExists;
      }

      /* This function takes sitecore item as argument and returns custom Url (string) based on our custom Rules */
      public static string CustomUrlManager.GetCustomUrl(Item item)
      {
         string customUrl=string.Empty;
         /*Create custom Url based on custom rules*/
         /*Eg.*/
         /*If Item is based on People Template Then*/
         /*Read the value of the Email Field and generate the custom URL based on it*/
         /*return custom Url*/
         return customUrl;
      }

      public static string CustomUrlManager.GetAlias(string customUrl)
      {
         /*Remove extra unwanted characters like -, / etc. from URL and return Alias that would be stored in index*/
         return customUrl;
      }

      /*This function looks for the item ,based on "File Path", using the custom search index(CustomUrlIndex) and
         returns the mapped item (Sitecore Item)*/
      public static Item CustomUrlManager.GetItemByFilePath(string filePath)
      {
         Item mappedItem=null;

         /*Find the item based on the filePath. We have used custom search index to store the mapping between Item and
            the custom Url (based on email id in our case)*/
         /*return the mapped Item*/
         string alias= CustomUrlManager.GetAlias(filePath);
         UrlSearchResults objUrlSearchResults=new UrlSearchResults(alias);
         mappedItem = objUrlSearchResults. GetItemByAlias(alias)
         return mappedItem;
      }
   }
}

I used custom search Index for storing mapping between Item and the custom Url for optimized performance. Lucene Search is much faster than fast query or code which iterates through a list of items. I’ll just provide a basic guideline and code around how to create a custom search index. We’ll use Sitecore.Search to create our Custom Search.

For creating the Custom URL Index, we need to do the following:

Create our own crawler/indexer for creating custom index. We can create custom crawler by creating a class inherited from ” Sitecore.Search.Crawlers.DatabaseCrawler” class and then overriding the “AddAllFields” method. We will store the custom url/alias associated with item in this index.The class CustomUrlManager discussed earlier will be used to get the custom url/alias associated with the item. The document.Add method is used to add custom field to the document. It takes a Field object as an argument.

document.Add(new Field(fieldName, fieldValue, storageType, fieldIndexType))

Please refer to Lucene documentation to find out what each of these options mean.

After creating the crawler, we need to create a custom search result class which will use the custom index to find Sitecore items mapped with the custom URLs. This class will handle the custom search queries. We will create a query to search for the item which maps with the respective custom url/alias.

Now all we need to do is to add Search configuration settings for the custom indexer in web.config

Please refer to my Custom Search blog for more details.

Now we can start creating the custom search.

Search Code

using Sitecore;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Lucene.Net.Documents;
using Sitecore.Data.Managers;
using Field = Lucene.Net.Documents.Field;
using System;
using Sitecore.Search;
using Lucene.Net.Search;
using System.Collections.Generic;
using System.Web.UI.WebControls;
using Sitecore.Data;

namespace CustomLibrary
{
   public struct UrlIndexFields
   {
      public const string UrlAlias = "UrlAlias";
   }

   public class UrlIndexer : Sitecore.Search.Crawlers.DatabaseCrawler
   {
      ///<summary>
      ///Indexes the Url Alias Information
      ///</summary>
      ///<param name="item"></param>
      ///<param name="document"></param>
      ///<param name="versionSpecific"></param>
      protected override void AddAllFields(Document document, Item item, bool versionSpecific)
      {
         try
         {
            if (CustomUrlManager.CustomUrlExists(item))
            {
               string urlAlias = CustomUrlManager.GetAlias(CustomUrlManager.GetCustomUrl(item));
               if (!string.IsNullOrEmpty(urlAlias))
               {
                  document.Add(new Field(UrlIndexFields.UrlAlias, urlAlias, Field.Store.YES, Field.Index.ANALYZED));
               }
            }
         }
         catch (Exception exception)
         {
            Sitecore.Diagnostics.Log.Error("CustomLibrary.UrlIndexer > AddAllFields : ", exception, this);
         }
      }
   }

   public class UrlSearchResults
   {
      Sitecore.Search.Index index = SearchManager.GetIndex("UrlIndex");

      public Item GetItemByAlias(string alias)
      {
         List<Item> items = null;
         SearchHits KeywordHits;
         try
         {
            using (IndexSearchContext SearchContext = index.CreateSearchContext())
            {
               KeywordHits = DoAliasSearch(SearchContext, alias);
               items = GetItemsList(KeywordHits);
               if (items.Count > 0)
               {
                  return items[0];
               }
            }
         }
         catch (Exception exception)
         {
            Sitecore.Diagnostics.Log.Error("BusinessModules.UrlSearchResults > GetItemByAlias : ", exception, this);
         }
         return (Item)null;
      }

      private Sitecore.Search.SearchHits DoAliasSearch(Sitecore.Search.IndexSearchContext searchContext, string alias)
      {
         CombinedQuery query = new CombinedQuery();
         if (String.IsNullOrEmpty(alias) == false)
         {
            QueryBase aliasQuery = new FieldQuery(UrlIndexFields.UrlAlias, "\"" + alias + "\"");
            query.Add(aliasQuery, QueryOccurance.Must);
         }
         SearchHits searchhits = searchContext.Search(query, index.GetDocumentCount());
         return searchhits;
      }
   }
}

Helpful References: