Sitecore cross site links

Reason

Even though cross-site link generation is said to have been fixed in Sitecore 6.4.1 and onward, the standard link provider has the following issues and/or bugs:

  1. URLs are not generated properly when Sitecore.Context.Site is null (e.g. when trying to generate URLs in a HttpModule). Although this is logical, an automatic fallback mechanism would be helpful, instead of having to provide UrlOptions manually.
  2. Cross-site URLs are only generated properly when using markup crunchers like ”LinkProvider.ExpandDynamicLinks” in e.g. rendering pipelines. When calling ”LinkManager.GetItemUrl” cross-site links are not generated properly.
  3. When accessing ”LinkManager.DefaultUrlOptions” (which in turn calls ”LinkProvider.GetDefaultUrlOptions()”), the value of ”UrlOptions.SiteResolving” is always false, regardless of the Web.config settings.

Examples are based  on Sitecore CMS 6.5.0 rev. 120706 (6.5.0 Update-5) and .NET 4.0.

Code

A LinkProvider-implementation which attempts to address the outlined problems is provided below.

using System;
using System.Collections.Generic;</pre>
using System.Collections.Specialized;
using System.Linq;
using System.Reflection;
using Sitecore;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Links;
using Sitecore.Sites;

public class CrossSiteLinkProvider : LinkProvider
{
  private readonly IEnumerable<SiteContext> _sortedSiteContexts;

  public CrossSiteLinkProvider()
  {
    _sortedSiteContexts = GetSortedSiteContexts();
  }

  private IEnumerable<SiteContext> GetSortedSiteContexts()
  {
    return (from site in SiteManager.GetSites()
            let siteContext = SiteContextFactory.GetSiteContext(site.Name)
            let pathPartCount = siteContext.RootPath.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries).Length
            // Ensure that deep-nested ancestors are considered before
            // shallow-nested ancestors when determining a matching site root
            orderby pathPartCount descending
            select siteContext).ToArray();
  }

  public override void Initialize(string name, NameValueCollection config)
  {
    base.Initialize(name, config);
    // Currently the "SiteResolving" property isn't initialized in the Sitecore implementation.
    InitializeSiteResolving(config);
  }

  private void InitializeSiteResolving(NameValueCollection config)
  {
    Assert.ArgumentNotNull(config, "config");
    FieldInfo urlOptionField = typeof(Sitecore.Links.LinkProvider).GetField("defaultUrlOptions", BindingFlags.NonPublic | BindingFlags.Instance);
    UrlOptions urlOptions = (UrlOptions)urlOptionField.GetValue(this);
    urlOptions.SiteResolving = MainUtil.GetBool(config["siteResolving"], false);
  }

  public override string GetItemUrl(Item item, UrlOptions options)
  {
    Assert.ArgumentNotNull(item, "item");
    Assert.ArgumentNotNull(options, "options");
    using (new ProfileSection(GetType().FullName + ".GetItemUrl"))
    {
      if (Context.Site == null || options.SiteResolving)
        options.Site = GetSiteContext(item);

      return base.GetItemUrl(item, options);
    }
  }

  private SiteContext GetSiteContext(Item item)
  {
    string itemPath = StringUtil.EnsurePostfix('/', item.Paths.FullPath);
    return _sortedSiteContexts.FirstOrDefault(site => itemPath.StartsWith(StringUtil.EnsurePostfix('/', site.RootPath)));
  }
}

To replace the standard LinkProvider with the custom implementation, copy the markup shown below into an configuration file (e.g. “CrossSiteLinkProvider.config”) and place it in the “[webroot]/App_Config/Include” folder.

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <linkManager>
      <patch:attribute name="defaultProvider" value="CrossSiteLinkProvider" />
      <providers>
        <add name="CrossSiteLinkProvider"
          type="Your.Namespace.CrossSiteLinkProvider, Your.Assembly"
          addAspxExtension="true"
          alwaysIncludeServerUrl="false"
          encodeNames="true"
          languageEmbedding="asNeeded"
          languageLocation="queryString"
          lowercaseUrls="true"
          shortenUrls="true"
          siteResolving="true"
          useDisplayName="false" />
      </providers>
    </linkManager>
  </sitecore>
</configuration>

Example

Shown below are a series of screenshots displaying a very simple two-site setup with an additional “subsite” (e.g. a campaign landing page or similar).

Each site’s start item has a reference field which points to one of the two other sites:

The sites are defined in Web.config as follows:

A simple test page which uses the standard LinkProvider and the CrossSiteLinkProvider to generate URLs:

URLs as generated on “Site 1”:

URLs as generated on “Site 2”:

URLs as generated on “SubSite”:

3 thoughts on “Sitecore cross site links

  1. Pingback: Sitecore Links with LinkManager and MediaManager | Brian Pedersen's Sitecore and .NET Blog

  2. Pingback: Sitecore Links with LinkManager and MediaManager | CMS News Today

  3. Pingback: Switching Sitecore's SiteContext - Lukasz Rajchel

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s