Simple handling of legacy URLs to an EPiServer 4 site migrated to a later version

When a customer wants to move their site from version 4 of the EPiServer platform to a later one, the URLs will change. It is very likely that so will the ids for the migrated pages. Here is a simple method for retaining functioning bookmarks, unbroken links from other websites, keeping RSS readers happy, while at the same time feeding search engines 301 – Permanently moved information.

This solution is expecting you to already have migrated the legacy page ids from the old platform into properties on the new page types. I realized that I would be able to use the built in page converter tool in EPiServer 6 to convert my pages after I had migrated the old types; therefore I added a property with the name LegacyPageLinkId on the relevant page types in both the source as well as the target solution. Then I wrote a simple scheduled job in the EPiServer 4 environment which went through all of the pages that I wanted to migrate and copied the page id into my property; EPiServer took care of the rest with it’s converter tool.

So, assuming that all of the page types that were migrated have this property.

SomePageTypeBase.cs

        [PageTypeProperty(Type = typeof(PropertyNumber),
            UniqueValuePerLanguage = false,
            DisplayInEditMode = false,
            Searchable = true,
            EditCaption = "Legacy: EPiServer 4 PageId")]
        public virtual int LegacyPageLinkId { get; set; }

It is then simply a matter of parsing the incoming request URL, look for the old id and then finally go fetch the proper page address. Implementing the IHttpModule interface for creating a legacy URL redirect module will result in a great place to do this in. The reason for not using a handler instead (implementing IHttpHandler) is that a handler does just that; handles the request, and most likey results in a blank page unless we take care of the request ourselves. Since we want everything to work just as before unless we decide to redirect the user, the module is a far better choise.

LegacyUrlRedirectModule.cs

public class LegacyUrlRedirectModule : IHttpModule
{
    public void Init(HttpApplication application)
    {
        application.EndRequest += (new EventHandler(this.Application_EndRequest));
    }

    public void Dispose()
    {
    }

The two things needed to be implemented from this interface are the Init(..) and the Dispose() methods. We will not be disposing of anything, so just leave that blank. In Init on the other hand, we need to add a new handler for the EndRequest event. If this is done in, for instance, BeginRequest we will not be able to use EPiServers FindPagesWithCriteria(..), as it just is not there yet.

private void Application_EndRequest(object sender, EventArgs e)
{
    var application = (HttpApplication) sender;
    if (application == null) return;

    var url = application.Context.Request.Url.ToString();
    if (!url.ToLower().StartsWith("/tpl/")) return;

    var targetUrl = GetTargetUrl(url);
    if (string.IsNullOrEmpty(targetUrl)) return;

    application.Context.Response.StatusCode = 301;
    application.Context.Response.Status = "301 Permanenlty Moved";
    application.Context.Response.RedirectLocation = targetUrl;
}

All of my legacy URLs seemed to start with the old template directory path /Tpl/ (28), and since I was not about to have any new addresses looking like that I decided that it would be enough to determine whether or not this was a link about to be broken. Of course, if any of the web editors suddenly decided to make a friendly URL starting with /tpl/ things would probably go south rather fast, but on the other hand, then the GetTargetUrl(..) method would return an empty string canceling the redirection alltogether (30-31).

When I went looking, I found two types of incoming request URLs to the site; for the normal page type it looked like this: /Tpl/NormalPage____1234.aspx and /Tpl/NormalPage.aspx?id=1234

private static string GetTargetUrl(string url)
{
    string legacyPageId;
    var match = Regex.Match(url, @"____\d+.aspx");
    if (match.Success)
    {
        legacyPageId = Regex.Match(match.Value, @"\d+").Value;
    }
    else
    {
        match = Regex.Match(url, @"([?]|&)id=\d+");
        legacyPageId = match.Success ?
            Regex.Match(match.Value, @"\d+").Value :
            string.Empty;
    }
    if (string.IsNullOrEmpty(legacyPageId)) return string.Empty;

The first part of finding out where to send the visitor consists of basic regular expression matching; I have found that Regex Hero can be a very useful tool for this kind of exercises. If we cannot find an id, the redirection is off, but if do find one we pass it on as the value creating a search criterion (60).

    var criteria = new PropertyCriteriaCollection
        {
            new PropertyCriteria
                {
                    Condition = CompareCondition.Equal,
                    Value = legacyPageId,
                    Type = PropertyDataType.Number,
                    Required = true,
                    Name = "LegacyPageLinkId"
                }
        };

    var searchResult = DataFactory.Instance.FindPagesWithCriteria(PageReference.RootPage, criteria);
    return searchResult.Count < 1 ?
            "/" :
            searchResult.First().LinkURL;
}

Should the FindPagesWithCriteria search fail to locate the page with the matching legacy id, I decided that I wanted the visitor to end up at the site’s startpage (69) instead of a 404.

The only thing left to do is getting the redirect module to actually run. This is just a simple addition in the web.config file.

web.config

<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.webServer>
<modules runAllManagedModulesForAllRequests="true">
    <add name="LegacyUrlRedirecModule"
         type="MySite.Core.Web.Legacy.LegacyUrlRedirecModule, MySite.Core" />

In the /configuration/system.webServer/modules tag, add a new module entry and point it to the proper namespace and assembly. I had a separate Core project in the solution where I placed my code, this being the reason for pointing it to the MySite.Core.dll binary.

This filled my purpose, but if you would have to consider, for instance, old bookmarked searches (such as /Tpl/SearchPage____4321.aspx?searchQuery=mysearchquery), it could easily be mapped to a new search page (/Search/?q=mysearchquery, or something like it) with a bit of clever string parsing.