Extending FriendlyUrlRewrite provider for EPiServer 6 R2

It is a common practice among CMS to convert their internal links to external which are user friendly. EPiServer achieves that by using the FriendlyUrlRewrite provider. In a recent case one of our customers wanted to remove a specific query string from their site urls and replace it with an actual name that this string represented. Since the original case is complicated a simpler example will be presented. Suppose that there is an enumerable with 3 pairs, Stockholm = 1, Lund = 2 and Uppsala = 3. If there is a link of type www.softresource.se/Kontakt/?region=2  that should be rewritten into www.softresource.se/Kontakt/Lund/

While in EPiServer 7 (or latest) this can be easily handled with routing, in EPiServer 6 R2 the only option is to extend the FriendlyUrlRewrite provider. This can turn into a very complex task depending on the demands of rewriting. In the following example aside from EPi 6 R2 the site is also using PageTypeBuilder.

Firstly, one needs to create a class that extends EPiServer FriendlyUrlRewrite and override the following methods

public class SrUrlRewriteProvider : EPiServer.Web.FriendlyUrlRewriteProvider {

   public override bool TryConvertToInternal(UrlBuilder url, out CultureInfo preferredCulture, out object internalObject) {}

   public override bool ConvertToInternal(UrlBuilder url, out object internalObject){}

   protected override bool ConvertToInternalInternal(UrlBuilder url, ref object internalObject) {}

   protected override bool ConvertToExternalInternal(UrlBuilder url, object internalObject, Encoding convertToEncoding){}
}

Then EPiserver.config urlRewrite providers must be updated. Use the name of the class as name and in the type field include the full path of the class and then the namespace.     

<add name="SrUrlRewriteProvider"
          enableSimpleAddress="true"
          friendlyUrlCacheAbsoluteExpiration="0:0:10"
          type="SrExternwebben.UrlRewriteProviders.SrUrlRewriteProvider, SrExternwebben"
          description="Rewrites regional url" />

Then set this one as the default provider <urlRewrite defaultProvider="SrUrlRewriteProvider">

Now that UrlRewriteProvider is set up we can start working with the functions. But let’s explain first what they do. When an EPiServer page loads, the first function to fire is the ConvertToInternalInternal. This functions gets the friendly url (the actual link on the browser) and converts it to the internal EPiServer url (Something like the following /Common/PageTemplates/ContactPageTemplate.aspx?id=45&epslanguange=sv ). If EPiServer finds the correct page then it starts to render it and with that all the urls included in that page. At that step the function TryConvertToInternal fires. This function should check if a url is qualified for conversion from our provider or not. If not it should be passed to the default provider of EPiServer. If it is qualified it should be rewritten using the method ConvertToInternalExternal() which will do the final rewriting.

While a few people use only the ConvertToInternal method in this case only the TryConvertToInternal fired on debug. Just to be safe both are included in the class and since they do the same thing their contents should be the same.

  public override bool TryConvertToInternal(UrlBuilder url, out CultureInfo preferredCulture, out object internalObject)
       {
           internalObject = null;
           preferredCulture = ContentLanguage.PreferredCulture;
           if (this.IsUrlEscaped(url.ToString()))
           {
               return base.ConvertToInternalInternal(url, ref internalObject);
           }

var referrer = url.ToString().Replace(HttpContext.Current.Request.Url.Scheme + "://" + HttpContext.Current.Request.Url.Host, string.Empty);

           if (this.IsReferrerEscaped(referrer))
           {
               return base.ConvertToInternalInternal(url, ref internalObject);
           }

           return this.ConvertToInternalInternal(url, ref internalObject);
       }

Below actual code related to this example is avoided in favor of describing the obstacles that one might encounter and their solution. Since it will be referred quite a few times below to use the function from the default UrlRewriteProvider use base or else this (as shown in the example above).

The most important step is to define the logic of the algorithm and how to handle the links. With regards to the example with the 3 regions the logic should be the following for ConvertToInternalInternal function.

  • Check if the url is not null.
  • Exclude all the cases that a url should not be rewritten. Ex. A handler using the region query string.
  • Handle the url properly when the site is being browsed from edit mode. When browsing the rewritten pages from edit mode the site was crashing. That happened when the code had to cast the HttpContext.Current.Handler as PageData and it couldn’t get the id of the page. One solution is to check if the page is in edit mode and if so pass it to the normal provider. The other solution is to clean the url. After examination of the error I found out that a couple of urls seemed like www.softresource.se/Kontakt/Lund/?id_45_24564?epslanguage=sv
    This means that EPiServer was adding after the friendly url the id of the page and the WorkPageId separated by a “_”. Using another method that removed the “_” and passed the WorkPageId later in the pagereference to get right version of the page the site worked correctly on edit mode.
  • As a last step a check is required to see if the url can be converted to PageData. If so then there is no need to rewrite it. Else if the last segment of the url has as name one of the three names of the Region enum remove it from the url and then convert it to PageData. If the url is correct the conversion should be successful. Finally the url in the function should be updated with the page Id, the language and the region with their value in the queryCollection and return true like below.
url.Path = Url(page.StaticLinkURL).Path;
url.QueryCollection.Add("id", page.PageLink.ID.ToString());
url.QueryCollection.Add("epslanguage", ContentLanguage.PreferredCulture.TwoLetterISOLanguageName);
url.QueryCollection.Add("region", regionId);
return true;

ConvertToInternalExternal on the other hand is responsible for rewriting internal links to external and while the aforementioned function fires only once in every page load this one fires for every link included in the page. This includes css, js references, scripts etc. Additionaly links that should be handled by that might not only be internal but also already converted to external (for instance if there is a dropdown with items each region link to ContactPage that are still not rewritten). Here is how the algorithm should handle the links.

  • Check if the url is not null.
  • Exclude all the cases that a url should not be rewritten and can be easily identified like scripts etc. For example local scripts are located in a different folder than PageTemplates that needs to be rewritten.
  • Check if the link is internal and try to get the page id. If yes try to get the region query string. If region exists then the region name can be added in the end of the pages url.
  • If there is no PageReference is found segment the url and check if the last segment has the query string of a region (This is to handle friendly urls with no rewriting). If so remove it from the queryCollection, get the region's’ name and add it as an additional segment to the url and then return true.

With regards to the last part of the algorithm one might say that it is better to rewrite those urls when they are created in code behind than rewriting them for a second time. While this solution is ideal as a concept and less performance expensive in this example two additional things were taken into account. First the option to roll back from the custom UrlRewrite provider to the default without having to worry about the functionality on the site. Also a lot of the site needed to be rewritten and tested to verify that it works correct. Secondly ConvertToExternal method should be overridden which I have already tried and noticed that all the urls in the page tree in the edit mode were rewritten also and as such I preferred to go with my initial method.

Another issue that might be happening on the rewritten pages is that ___doPostback() javascript methods are failing. This unexpected behavior might occur when the action in the main form of the page is not rewritten properly. If it is fixed all the postback events should work as expected.

If all the above are covered and there is solid logic in the conversion algorithm of the friendly urls to internal and vice versa then the site should work with no problem.