Simple functionality for hreflang metadata in EPiServer websites

The alternate hreflang tag is a way of letting search engines such as Google know about the different languages that your pages exist in. It’s a signal to the engines that they may use in order to serve more relevant content to users based on language. One place to put this information is directly in the markup as below (Other alternatives here).

<link rel="alternate" href="http://blog.mathiaskunto.com/" hreflang="en-GB" />

A translated page should have references to each language version that it exists in, including the current one; so if my blog existed in Swedish and Norwegian as well as in English, then I would need hreflang references to all three URLs on all three page versions (as you can also see in the example by following the previous link).

<link rel="alternate" href="http://blog.mathiaskunto.com/" hreflang="en-GB" />
<link rel="alternate" href="http://blog.mathiaskunto.com/sv-se/" hreflang="sv-SE" />
<link rel="alternate" href="http://blog.mathiaskunto.com/no-no/" hreflang="no-NO" />

Using EPiServer’s built-in globalization capabilities to manage different languages it is rather straight forward to generate this for relevant pages in the CMS. I wrote a short piece of code for a client doing this.

Neither the model nor the view contains anything that’s actually interesting.

The Model – AlternateHrefLangModel.cs

public class AlternateHrefLangModel
{
  public Dictionary<string, string> AlternateUrls { get; set; }
  public bool IsVisible { get; set; }
}

The View – Index.cshtml

@model MyProject.MyNamespacePart.AlternateHrefLangModel

@if (Model.IsVisible)
{
  foreach (var alternateUrl in Model.AlternateUrls)
  {
    <link rel="alternate" href="@alternateUrl.Value" hreflang="@alternateUrl.Key" />
  }
}

In my client’s code we’re using a message bus to allow for overriding of the model population; however, as this is not really needed here I’ve rewritten it a bit.

The Controller – AlternateHrefLangController.cs

public ActionResult Index()
{
  var model = AlternateUrlsModel();
  return PartialView(model);
}

private AlternateHrefLangModel AlternateUrlsModel()
{
  var model = new AlternateHrefLangModel { IsVisible = true };

  // Only original pages should have alternate hreflang links.
  if (CurrentPage.IsMirrored())
  {
    model.IsVisible = false;
    return;
  }

  var allLanguages = CurrentPage.ExistingLanguages.ToArray();

  var pages = allLanguages
                .Select(l => _contentLoader.Get<PageData>(CurrentPage.ContentLink, l))
                .Distinct()
                .Where(p => _filterService.HasVisitorAccessTo(p))
                .ToArray();

  model.AlternateUrls = pages.ToDictionary(page => page.Language.IetfLanguageTag, page => _urlService.AbsoluteUrlInPageLanguage(Url, page));
  return model;
}