The problem

EPiServer has great functionality for managing SEO friendly URLs where the user can effectively manage their own URL structure such as:

http://www.mysite.com/news/europe/uk/liverpool/football-news-item/

In most cases this works really well. However I was recently working with SEO consultant David Deutsch on a project where search engine rankings are paramount to the success of the business. His requirement was to have reallyfriendly URLs for SEO purposes. 

Using the example above he wanted:

http://www.mysite.com/Latest-World-Football-News/ instead of http://www.mysite.com/news/.

It could be argued that we could simply change the URL segment and be done. However the keywords needed to appear in the URL, only for the current page whilst still maintaining a hierarchy of child pages. For example he also wanted the following pages to be valid:

  • http://www.mysite.com/news/European-Football-News/ instead of http://www.mysite.com/news/europe
  • http://www.mysite.com/news/europe/Football-News-For-The-UK/ instead of http://www.mysite.com/news/europe/uk
  • http://www.mysite.com/news/europe/uk/Liverpool-FC-News/ instead of http://www.mysite.com/news/europe/uk/liverpool

and so on…

There are good SEO reasons for this as we don't want keywords from a parent page polluting the keywords on a child page. In the example given "European Football News" are the keywords on the Europe news page but keywords on the ultimate child page may be "Liverpool FC News" which aren't really related to the parent keywords. (Also as a side note, as developers we don't really want really really long URLs).

The solution

So it can be seen that any page needs to effectively have two URL segments, one for SEO purposes and one for serving children pages. This is pretty easily achieved as follows:

  • Add a "SEO URL segment" to each page type that we want to use it
  • Create a custom URL rewriter based on the standard friendly URL rewriter (see below)
  • Use the SEO friendly URL rewriter in place of the standard friendly URL rewriter

This code overrides the ConvertToInternal and ConvertToExternal methods of the EPiServer FriendlyUrlRewriteProvider and creates a new "SEOFriendlyURLRewriter":

using System;
using System.Web;
using EPiServer.Web;
using EPiServer;
using EPiServer.PlugIn;
using EPiServer.Core;
 
namespace EPi6Tools
{
    public class SEOFriendlyURLRewriter : FriendlyUrlRewriteProvider
    {
        public static string seoFriendlyPropName = "SEOFriendlyURL";
 
        private string cacheKeyPrefix = "__SEOURLRewriter";
 
        public override bool ConvertToInternal(EPiServer.UrlBuilder url, out object internalObject)
        {
            //Check if there is an override on the friendly URL and replace with the genuine friendly URL if so
            object cachedURL = CacheManager.Get(cacheKeyPrefix + url.Path);
            if (cachedURL != null && !string.IsNullOrEmpty(cachedURL.ToString()))
                url.Path = cachedURL.ToString();
            return base.ConvertToInternal(url, out internalObject);
        }
 
        public override bool ConvertToExternal(EPiServer.UrlBuilder url, object internalObject, System.Text.Encoding toEncoding)
        {
            bool rtn = base.ConvertToExternal(url, internalObject, toEncoding);
            // At this point the url object contains the external URL (e.g. http://<mysite>/en/News/) Check if the page
            // has a "even more SEO friendly" URL segment defined
 
            //There is a SEO Url so we want to use this instead of the simple friendly URL
            if (internalObject != null && internalObject.GetType() == typeof(PageReference) && !PageReference.IsNullOrEmpty(internalObject as PageReference))
            {
                PageData currentPage = DataFactory.Instance.GetPage(internalObject as PageReference);
 
                if (currentPage[SEOFriendlyURLRewriter.seoFriendlyPropName] != null && !string.IsNullOrEmpty(currentPage[SEOFriendlyURLRewriter.seoFriendlyPropName].ToString()))
                {
                    //Replace the defined URL segment with the SEO friendly segment
                    string originalURL = url.Path;
                    //Remove the last segment
                    url.Host = "www.temporary.com"; //used to get access to the URL segments
                    url.Path = url.Path.Remove(url.Path.Length - url.Uri.Segments[url.Uri.Segments.Length - 1].Length);
                    url.Host = string.Empty;
 
                    //Add the SEO friendly segment instead
                    url.Path += currentPage[SEOFriendlyURLRewriter.seoFriendlyPropName].ToString() + "/";
 
                    //Cache the result so it gets converted when going to the internal URL
                    CacheManager.Add("__SEOURLRewriter" + url.Path, originalURL);
                }
            }
            return rtn;
        }
    }
 
}
 

This looks for a really friendly SEO property and uses that in place of the URL segment if its specified. Obviously this needs to be plugged into episerver.config as follows:

...
<urlRewrite defaultProvider="SEOFriendlyUrlRewriteProvider">
  <providers>
    <add description="Really SEO Friendly URL rewriter" name="SEOFriendlyUrlRewriteProvider"
      type="EPi6Tools.Modules.SEOFriendlyURLRewriter,EPi6Tools" />
    <add description="EPiServer standard Friendly URL rewriter" name="EPiServerFriendlyUrlRewriteProvider"
      type="EPiServer.Web.FriendlyUrlRewriteProvider,EPiServer" />
    <add description="EPiServer identity URL rewriter" name="EPiServerIdentityUrlRewriteProvider"
      type="EPiServer.Web.IdentityUrlRewriteProvider,EPiServer" />
    <add description="EPiServer bypass URL rewriter" name="EPiServerNullUrlRewriteProvider"
      type="EPiServer.Web.NullUrlRewriteProvider,EPiServer" />
  </providers>
</urlRewrite>
...

However this isn't the end of the story. If someone was browsing to http://www.mysite.com/news/europe/Football-News-For-The-UK/ then removed the "Latest-Football-News-UK" segment then EPiServer will serve http://www.mysite.com/news/europe/ quite happily. However we don't really want this as if the link was ever used then Google could penalise the site for having duplicate content. So this drives the requirement to automatically 301 redirect to the really SEO friendly name. This is achieved with a page plug in as follows:

using System;
using System.Web;
using EPiServer.Web;
using EPiServer;
using EPiServer.PlugIn;
using EPiServer.Core;
 
namespace EPi6Tools
{
    [PagePlugIn]
    public class SEOFriendlyRedirector
    {
        public static void Initialize(int optionFlag)
        {
            PageBase.PageSetup += new PageSetupEventHandler(PageSetup);
        }
 
        public static void PageSetup(PageBase sender, PageSetupEventArgs e)
        {
            sender.Load += new EventHandler(CheckForSEOFriendlyLink);
        }
 
        public static void CheckForSEOFriendlyLink(object sender, EventArgs e)
        {
            //Check to see if this is an EPiServer template and that we have an EPiServer page object
            if (!(sender is TemplatePage) || (sender as TemplatePage).CurrentPage == null)
            {
                return;
            }
 
            PageData currentPage = (sender as TemplatePage).CurrentPage;
 
            //Check if the page has a SEO friendly alternative URL defined
            string seoFriendlySegmenet = currentPage[SEOFriendlyURLRewriter.seoFriendlyPropName] != null ? currentPage[SEOFriendlyURLRewriter.seoFriendlyPropName].ToString() : string.Empty;
            if (!string.IsNullOrEmpty(seoFriendlySegmenet))
            {
                UrlBuilder currentURL = new UrlBuilder(HttpContext.Current.Request.RawUrl) { Host = HttpContext.Current.Request.Url.Host };
                //Check if the page is serving on the page defined in the URL segment
                if (currentURL.Path.EndsWith("/" + currentPage.URLSegment + @"/"))
                {
                    //Work out the new URL
                    currentURL.Path = currentURL.Path.Remove(currentURL.Path.Length - (currentPage.URLSegment.Length + 1), currentPage.URLSegment.Length + 1) + seoFriendlySegmenet + "/";
                    //301 back to the original URL
                    HttpContext.Current.Response.Status = "301 Moved Permanently";
                    System.Web.HttpContext.Current.Response.AddHeader("Location", currentURL.ToString());
                    System.Web.HttpContext.Current.Response.End();
                }
 
            }
 
        }
    }
 
}

This code is designed to show a concept is not production ready (there is no error handling or logging for a start). Use at your own risk.

Conclusion

This is a simple change that allows SEO experts to specify as many keywords as they like in the URL for particular page. While it feels a little odd to have a URL structure that's not perfectly logical it allows the SEO guys to get exactly the structure they want/need. Ultimately this should drive traffic to your site and keep your SEO guys very happy.

Note: The project we worked on wasn't for a football site!


Comments