Canonical URL on EPiServer short addresses and mirrored pages

When EPiServer web editors creates pages with the shortcut type Fetch content from page in EPiServer CMS, or as they use the Simple address option to create an alternative URL to a page, there is a need to create canonical URLs back to the original pages. Failing to do so may cause a loss of search engine ranking due to different URLs competing with each other using identical content.

EPiServer edit mode settings for shortcut page and page with a short link.

I’ve seen different solutions for this over the years, but here is one that gives us what we need with minimum effort; similar to one that I use for my current client.

Automatically render canonical URL metadata when EPiServer editor creates mirrored or short address page

I’ve removed parts of the cluttering overhead for this article making the solution a little less loose than you’d probably prefer.

In order to keep performance impact to a minimum, the rendering of our canonical metadata tag is done without the use of a partial controller. Adding a RenderPartial to the layout will suffice.

_Layout.cshtml

<html>
 <head>
   <meta charset="utf-8" />
   @{Html.RenderPartial("Partials/MetaData/CanonicalUrlTag");}

The partial view uses a service to fetch the data it needs, and only renders the link tag if it finds that the visitor is currently on a mirrored page or a shortcut URL.

CanonicalUrlTag.cshtml

@{
  var urlModel = ServiceLocator.Current.GetInstance<ICanonicalUrlService>().CanonicalUrl;
}
@if (urlModel.IsMirrored || urlModel.IsShortUrl)
{
  <link rel="canonical" href="@urlModel.CanonicalUrl" />
}

CanonicalUrlModel.cs

public class CanonicalUrlModel
{
  public string CanonicalUrl { get; set; }
  public bool IsMirrored { get; set; }
  public bool IsShortUrl { get; set; }
}

The canonical URL service exposes only a single property; the one to get proper model.

ICanonicalUrlService.cs

public interface ICanonicalUrlService
{
    CanonicalUrlModel CanonicalUrl { get; }
}

The implementation on the other hand, is where all the work takes place.

CanonicalUrlService.cs

public class CanonicalUrlService : ICanonicalUrlService
{
  private readonly ISiteDefinitionWrapper _siteDefinition;
  private readonly HttpRequestBase _request;
  private readonly IContentLoader _contentLoader;
  private readonly UrlResolver _urlResolver;
  private readonly IPageRouteHelper _pageRouteHelper;

  // Constructor injection of above resources.

  public CanonicalUrlModel CanonicalUrl
  {
    get { return GetCanonicalUrl(); }
  }

I removed cluttering code, like the constructor injection above. So, let’s get to the interesting method of this class; GetCanonicalUrl. First of all, we should see if we need to output a canonical URL tag at all. By checking whether or not the page is mirrored, or if the visitor is on a short URL, we can quickly return and avoid doing unnecessary work.

If the page is mirrored we determine the original page, otherwise the current page will be used (needed on short URLs). After this, the original friendly absolute URL is retrieved and passed back in a model object.

private CanonicalUrlModel GetCanonicalUrl()
{
  var currentPage = _pageRouteHelper.Page;

  var isShort = IsOnShortUrl();
  var isMirrored = currentPage.IsMirrored();
  if(!isMirrored && !isShort)
  {
    return new CanonicalUrlModel
    {
      IsShortUrl = isShort,
      IsMirrored = isMirrored,
      CanonicalUrl = string.Empty
    };
  }

  var actualPage = isMirrored ?
                     GetMirroredTarget() ?? currentPage :
                     currentPage;

  var canonicalPage = actualPage as ICanonical;
  var canonicalUrl = canonicalPage == null ?
                       actualPage.LinkURL :
                       canonicalPage.CanonicalUrl;

  var urlBuilder = new UrlBuilder(canonicalUrl);
  var currentHost = CurrentHostFor(ContentLanguage .PreferredCulture.IetfLanguageTag);

  var friendlyPath = _urlResolver.GetUrl(urlBuilder.ToString());
  var builder = ToAbsoluteUrl(currentHost, new Uri(friendlyPath, UriKind.Relative));

  var canonical = new CanonicalUrlModel
  {
    IsShortUrl = isShort,
    IsMirrored = isMirrored,
    CanonicalUrl = builder.ToString()
  };
  return canonical;
}

So, let’s get to the methods used above. For the most part, there is nothing special about them.

// Assembles an absolute URL if the uri is relative.
private UrlBuilder ToAbsoluteUrl(Uri host, Uri uri)
{
  return !uri.IsAbsoluteUri ?
    new UrlBuilder(host) { Path = uri.ToString() } :
    new UrlBuilder(uri);
}

// Determines the proper host for a language tag depending on site definitions, maintaining port and protocol.
private Uri CurrentHostFor(string ietfLanguageTag)
{
  var hostDefinition = _siteDefinition
                         .CurrentSiteDefinition
                         .Hosts
                         .FirstOrDefault(definition => definition.Language.IetfLanguageTag == ietfLanguageTag);

  var currentUrl = _request.Url;

  if (hostDefinition != null && !IsWildcardBinding(hostDefinition))
  {
    return new UriBuilder(hostDefinition.Name .EnsureProtocol(currentUrl.Scheme)).Uri;
  }

  var host = currentUrl.Host.EnsureProtocol(currentUrl.Scheme);
  var uriBuilder = new UriBuilder(host);
  if (!currentUrl.IsDefaultPort)
  {
    uriBuilder.Port = currentUrl.Port;
  }
  return uriBuilder.Uri;
}

// We have a permanent redirect wildcard binding in our site definitions to prevent issues where we have no context.
private static bool IsWildcardBinding(HostDefinition hostDefinition)
{
  return hostDefinition.Name == "*";
}

// Determines if we're on a short url by comparing requested URL with current page's URL.
private bool IsOnShortUrl()
{
  var currentPage = _pageRouteHelper.Page;
  if (!currentPage.HasShortUrl())
  {
    return false;
  }
  return _request.Url != null &&
         _request.Url.AbsolutePath.Trim('/')
            .Equals(currentPage.ExternalURL, StringComparison.InvariantCultureIgnoreCase);
}

// Retrieves a page's shortcut target.
private PageData GetMirroredTarget()
{
  var currentPage = _pageRouteHelper.Page;
  var target = (PropertyPageReference) currentPage.Property["PageShortcutLink"];
  var targetPage = _contentLoader.Get<PageData>(target.PageLink);
  return targetPage;
}

SitePageBase.cs

public abstract class SitePageBase : PageData, ICanonical
{
  [Ignore]
  public virtual string CanonicalUrl
  {
    get
    {
      var url = new UrlBuilder(this.LinkURL);
      return url.ToString();
    }
  }

ICanonical.cs

public interface ICanonical
{
  string CanonicalUrl { get; }
}

PageDataExtensions.cs

public static class PageDataExtensions
{
  public static bool IsMirrored(this PageData page)
  {
    return page.LinkType == PageShortcutType.FetchData || page.LinkType == PageShortcutType.Shortcut;
  }

  public static bool HasShortUrl(this PageData page)
  {
    return !string.IsNullOrWhiteSpace(page.ExternalURL);
  }
}

Canonical link elements being rendered

// A request URL such as /short/ will result in:
<link rel="canonical" href="http://somehost.com/a-page-with-a-short-link/" />

// A request URL such as /a-page-with-a-short-link/ will not render a canonical tag.

// A request URL to a mirrored page /mirror-page/ will render:
<link rel="canonical" href="http://somehost.com/original-page/" />

// A request to a page with no mirror nor short link will not render a canonical tag.