Site Specific Link Provider for Multisite Implementation in Sitecore

I’ve been using this Site Specific Link Provider for a while now and have been meaning to blog it for some time, a couple of recent blog posts from John West and the addition of the new LinkProviderSwitcher to Sitecore 8 about prompted me to finally get my thoughts onto keyboard. When I read that a new LinkProviderSwitcher had been provided I got a bit excited that we may finally have site specific link provider, alas it was just a way of allowing us to switch the provider inside a using block.

Some of you may be aware that Sitecore has allowed us to define multiple Link Providers for some time, we just add multiple providers to the linkManager config section, but it hasn’t really provided us a nice way of utilizing that functionality without hard-coding it:

Sitecore.Links.LinkManager.Providers["myCustomProvider"].GetItemUrl(item,options);

Separate providers would be useful when working in a multi-site implementation when different sites have different link requirements e.g. multi-lingual site vs single language site, different requirements for generating URLs or if for some bizarre reasons you wanted one site to still include the aspx extension… I’ve had the requirement before where one site required the language to be embedded (multi-lingual) whereas another site served a different market with only a single language but the logic for generating the URLs was exactly the same. But since there was no nice way of accessing these provider it has often led us to hard code our way around this as well 😦

public override string GetItemUrl(Sitecore.Data.Items.Item item, Sitecore.Links.UrlOptions options)
{
    if (/* my condition of when to apply fully qualified links, e.g. a specific device maybe */)
    {
        options.LanguageEmbedding = LanguageEmbedding.Never;       
    }
    return base.GetItemUrl(item, options);
}

Switching Link Provider

Since Sitecore already supplies us a LinkProviderCollection, we just need a way to wire it up to tell the Link Provider which one it should be using.

public class SwitchingLinkProvider : Sitecore.Links.LinkProvider
{
    private const String FALLBACK_ATTRIBUTE_KEY = "fallback";
    private const String LINKPROVIDER_ATTRIBUTE_KEY = "linkProvider";
    private string Fallback { get; set; }

    public override void Initialize(string name, NameValueCollection config)
    {
        base.Initialize(name, config);
        this.Fallback = config[FALLBACK_ATTRIBUTE_KEY];
        Assert.IsNotNullOrEmpty(this.Fallback, "fallback");
    }
    
    private Sitecore.Links.LinkProvider SiteLinkProvider
    {
        get
        {
            var siteLinkProvider = (Sitecore.Context.Site != null)
                                    ? Sitecore.Context.Site.Properties[LINKPROVIDER_ATTRIBUTE_KEY] : String.Empty;

            if (String.IsNullOrEmpty(siteLinkProvider))
                siteLinkProvider = this.Fallback;

            return LinkManager.Providers[siteLinkProvider] 
                   ?? LinkManager.Providers[this.Fallback];
        }
    }
    ...    
}

And then we need to override all the properties and methods to return the correct value from the SiteLinkProvider, e.g.

public override UrlOptions GetDefaultUrlOptions()
{
    return SiteLinkProvider.GetDefaultUrlOptions();
}

public override string GetItemUrl(Item item, UrlOptions options)
{
    return SiteLinkProvider.GetItemUrl(item, options);
}

public override bool AddAspxExtension
{
    get
    {
        return SiteLinkProvider.AddAspxExtension;
    }
}

You then need to change the default provider to use the switcher and also set a fallback to use for sites that do not specify a provider:

<linkManager>
  <patch:attribute name="defaultProvider" value="switcher" />
  <providers>
    <add name="switcher" fallback="sitecore" type="Sitecore.Custom.Links.SwitchingLinkProvider, Sitecore.Custom" />
  </providers>
</linkManager>

We can now specify more link providers with whatever options we need, including different completely different types:

<linkManager>
  <providers>
    <add name="provider-site1" type="Sitecore.Links.LinkProvider, Sitecore.Kernel" addAspxExtension="false" languageEmbedding="never" useDisplayName="true" ... />
    <add name="provider-site2" type="Sitecore.Custom.Links.SpecificLinkProvider, Sitecore.Custom" alwaysIncludeServerUrl="true" languageEmbedding="always" lowercaseUrls="true" ... />    
  </providers>
</linkManager>

And to hook it all up we just need to add a custom attribute to the site node to specify which provider to use:

<sites>
  <site name="site1" linkProvider="provider-site1" ... />
  <site name="site2" linkProvider="provider-site2" ... />
  <site name="site3" ... />
</sites>

So now the link will be generated using the named provider specified in the site node. If no provider is specified then the fallback will be used. Be sure the fallback exists, there is no check in code around this so it will throw an exception. But this gives us the freedom to specify whatever LinkProvider, custom or otherwise, with different options set for each one to match the requirements for each individual site. There’s nothing special to code in your Link Providers:

public class LinkProvider : Sitecore.Links.LinkProvider
{
    public override string GetItemUrl(Item item, UrlOptions options)
    {    
        Assert.ArgumentNotNull(item, "item");
        Assert.ArgumentNotNull(options, "options");

        //custom logic as needed -- bad example, but you know...
        if (item.TemplateName == "example")
            return MyCustomUrl(item);

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

Used in conjunction with the Site Specific Pipelines in Multi-Site Implementation, this gives you a simple way to generate and resolve links on a site by site basis.

You can find the full code for the SwitchingLinkProvider here: https://gist.github.com/jammykam/daa151abeeac8a7029bf

For Sitecore 8.2, see the updated blog post for a switching Link Manager

Generating links from scheduled tasks

Unfortunately when you generate links from a scheduled tasks or the shell context your links will be generated using the Link Provider of the context site (e.g. scheduler or shell), and if none is specified then the fallback. This obviously is not what you want, such as when generating a site map. You will have the same issue no matter what technique you use if you are relying on context site.

The workaround is you need to switch site context when calling the GetItemUrl() method:

// work out the site the item is associated with, e.g. http://stackoverflow.com/a/14201308/661447
SiteContext siteContext = Factory.GetSite("sitename");

using (new Sitecore.Sites.SiteContextSwitcher(siteContext))
{
    string url = Sitecore.Links.LinkManager.GetItemUrl(item);
}

Additional reading:

And after all that work, I came across a post from Martin Davies with almost the same implementation. Obviously my Google skills were lacking for some reason before I started, but please read his post since it provides a lot more background info than I have given. I probably should have followed John’s lead and named the post Yet Another Site Specific Link Provider :-p

9 comments

  1. Martin Davies (TwentyGotoTen) · February 4, 2015

    Great post. Great minds think alike 😉

    • jammykam · February 4, 2015

      Thanks Martin. Been meaning to blog this since before we spoke in Barcelona! Been using this for several months now without any performance issues, surprised there isn’t a better way of accessing the LinkProviders in Sitecore.

  2. Ian Graham · February 4, 2015

    Good stuff, always useful. Yeah, I got excited about the LinkProviderSwitcher as well, but found an empty shell!! Think a site specific link provider should be part of Sitecore.

    • jammykam · February 4, 2015

      Yes, agree considering so much is touted about the multi-lingual, multi-site capabilities there’s still a few missing pieces that need implementing.

  3. Martin English · August 2, 2016

    Nice work man. I know this is an older post, but it had just the bits I needed for my implementation.
    Cheers!

    • jammykam · August 4, 2016

      Great, glad you found it useful!

  4. Pingback: Site Specific Link Manager Part 2 – Sitecore 8.2 Dependency Injection Support | jammykam
  5. Anders Gjelstrup · November 10, 2017

    I stumbled upon this blog post during research for multisite solution. Regarding link generation from scheduled tasks I found a rather sweet fix for this in another blogpost. Setting the rootPath of the scheduler site to a bogus value.
    http://blog.paulgeorge.co.uk/2011/05/04/sitecore-linkmanager-part-2-out-of-context-link-generation/

  6. Pingback: Comparison between Sitecore Custom and SXA Approaches | Sitecore basics!

Leave a comment