Implementing Custom Link Provider in Sitecore XM Cloud
Hello everyone, In this blog post we will see how we can override link provider class in Sitecore XM Cloud with a practical usecase. In Sitecore XM Cloud, the default link provider helps generate URLs based on the item's path and configured routing rules. However, in many real-world scenarios, marketers and content authors need more flexibility—like overriding the generated URL with a custom one defined at the content level.
This blog walks through a custom link provider use case where a template includes a field such as "Override URL", allowing authors to specify a different link for a given page
Let's walk through the scenario. Imagine you have a Search Page in Sitecore that includes an "Override URL" field. Now, create a sample content item—for example, a News Article page under a News folder.
Whenever a component on this News Article page tries to generate a link to the main Search Page, we want to intercept this behavior. If the Search Page’s "Override URL" field is populated with a custom link (e.g., a link to a search experience tailored for news articles), then we should generate the link pointing to that override URL instead of the default one.
In other words, if the override is configured, components should respect it and link to the specific, context-driven search page relevant to the News Article.
Let's first see, what default providers we have under linkmanager in showconfig.aspx
We can see there are 3 providers under linkmanager. We will add our own provider to the list. In my case, I have utilized localizedProvider and extracted the dll having the implementation to see what i need to do to achieve my functionality. We all know, that link generation happend=s through LinkManager GetItemUrl method. So we need to override the method so that we can inject our own code into the method.
I just copied all the code from localizedProvider (Sitecore.XA.Foundation.Multisite.LinkManagers.LocalizableLinkProvider) to my CustomLinkProvider class and inherited that class from LinkProvider class. You will see there is GetItemUrl method which is overriden where we are going to add our own code which is highlighted. Below is the code snippet for your reference.
namespace Foundation.Links
{
using System;
using System.Collections.Specialized;
using System.Web;
using System.Web.Caching;
using Foundation.Links.Constants;
using Microsoft.Extensions.DependencyInjection;
using Sitecore.Abstractions;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Sitecore.DependencyInjection;
using Sitecore.Diagnostics;
using Sitecore.Links;
using Sitecore.Links.UrlBuilders;
using Sitecore.Sites;
using Sitecore.Sites.Headless;
using Sitecore.Text;
using Sitecore.Web;
using Sitecore.XA.Foundation.Abstractions;
using Sitecore.XA.Foundation.Multisite;
using Sitecore.XA.Foundation.Multisite.Extensions;
using Sitecore.XA.Foundation.SitecoreExtensions.Extensions;
/// <summary>
/// Custom link provider for handling SEO-specific link generation, caching, and localization logic.
/// Extends the default Sitecore LinkProvider to support override links, site resolution, and Experience Editor/Preview scenarios.
/// </summary>
public class CustomLinkProvider : LinkProvider
{
private int _cacheExpiration = 1;
/// <summary>
/// Gets the current page mode (edit, preview, etc.) from the service provider.
/// </summary>
protected new IPageMode PageMode { get; } = ServiceLocator.ServiceProvider.GetService<IPageMode>();
/// <summary>
/// Gets the current Sitecore context from the service provider.
/// </summary>
protected new IContext Context { get; } = ServiceLocator.ServiceProvider.GetService<IContext>();
/// <summary>
/// Initializes a new instance of the <see cref="CustomLinkProvider"/> class.
/// </summary>
public CustomLinkProvider()
: base(ServiceLocator.ServiceProvider.GetService<BaseFactory>(), ServiceLocator.ServiceProvider.GetService<BaseSettings>())
{
}
/// <summary>
/// Initializes the link provider with the specified name and configuration.
/// </summary>
/// <param name="name">The name of the link provider.</param>
/// <param name="config">The configuration collection.</param>
public override void Initialize(string name, NameValueCollection config)
{
Assert.ArgumentNotNullOrEmpty(name, "name");
Assert.ArgumentNotNull(config, "config");
base.Initialize(name, config);
if (!int.TryParse(config["cacheExpiration"], out this._cacheExpiration))
{
this._cacheExpiration = 1;
}
}
/// <summary>
/// Gets the default URL builder options for items in the current context.
/// </summary>
/// <returns>The default item URL builder options.</returns>
public override DefaultItemUrlBuilderOptions GetDefaultUrlBuilderOptions()
{
DefaultItemUrlBuilderOptions defaultUrlBuilderOptions = base.GetDefaultUrlBuilderOptions();
if (SiteContext.Current.IsLanguageEmbeddingEnabled())
{
defaultUrlBuilderOptions.LanguageEmbedding = LanguageEmbedding.Always;
}
return defaultUrlBuilderOptions;
}
/// <summary>
/// Gets the URL for the specified item, applying custom logic for override links, caching, and localization.
/// </summary>
/// <param name="item">The Sitecore item.</param>
/// <param name="options">The URL builder options.</param>
/// <returns>The generated item URL.</returns>
public override string GetItemUrl(Item item, ItemUrlBuilderOptions options)
{
Assert.ArgumentNotNull((object)item, nameof(item));
Assert.ArgumentNotNull((object)options, nameof(options));
// Check if the General Link field exists and has a search page template
if (item.Fields[Fields.OverrideLinkFieldName] != null)
{
// Extract the URL value from the General Link field
LinkField linkField = item.Fields[Fields.OverrideLinkFieldName];
item = linkField.TargetItem ?? item;
return = base.GetItemUrl(item, options);
}
options.SiteResolving = !this.IsEditOrPreview && options.SiteResolving.GetValueOrDefault();
SiteInfo siteInfo = ServiceLocator.ServiceProvider.GetService<ISiteInfoResolver>().GetSiteInfo(item);
if (options.SiteResolving.HasValue && !options.SiteResolving.Value && this.IsEditOrPreview &&siteInfo != null)
{
options.Site = new SiteContext(siteInfo);
}
string itemUrl = base.GetItemUrl(item, options);
if (item.Database == null || item.Database.Name.Equals("core", StringComparison.OrdinalIgnoreCase) || (options.Site != null && options.Site.Name.Equals("shell", StringComparison.OrdinalIgnoreCase)))
{
return itemUrl;
}
string text = siteInfo?.Name;
string text2 = options.AlwaysIncludeServerUrl.GetValueOrDefault() ? "absolute" : "relative";
string key = $"LLM_{itemUrl}_{item.Database.Name}_{this.Context?.Site?.Name}_{text}_{text2}_{this.IsEditOrPreview}";
string text3 = this.IsEditOrPreview ? string.Empty : (HttpRuntime.Cache.Get(key) as string);
if (!string.IsNullOrWhiteSpace(text3))
{
return text3;
}
if (!item.Paths.IsMediaItem)
{
bool? flag = null;
ItemUrlBuilderOptions itemUrlBuilderOptions = null;
if (siteInfo != null && this.Context?.Site != null)
{
flag = !string.Equals(siteInfo.Name, this.Context.Site.Name, StringComparison.OrdinalIgnoreCase);
if (flag.Value)
{
itemUrlBuilderOptions = options.Clone() as ItemUrlBuilderOptions;
itemUrlBuilderOptions.AlwaysIncludeServerUrl = true;
itemUrlBuilderOptions.Site = new SiteContext(siteInfo);
itemUrl = base.GetItemUrl(item, itemUrlBuilderOptions);
}
}
try
{
text3 = this.GetLocalizedUrl(item, itemUrl, itemUrlBuilderOptions ?? options, text);
}
catch (Exception exception)
{
Log.Error("Couldn't generate localized URL for: '" + itemUrl + "'", exception, this);
text3 = itemUrl;
}
HeadlessMode mode = this.Context.HeadlessContext.GetMode();
bool flag2 = mode == HeadlessMode.Edit;
bool flag3 = mode == HeadlessMode.Preview;
if (!flag2 && !flag3 && this.IsEditOrPreview && flag.HasValue && flag.Value)
{
UrlString urlString = new UrlString(text3);
string value = Sitecore.Context.PageMode.IsExperienceEditor ? "edit" : "preview";
text3 = urlString.Add("sc_mode", value);
}
HttpRuntime.Cache.Insert(key, text3, null, DateTime.UtcNow.AddMinutes(this._cacheExpiration), Cache.NoSlidingExpiration, CacheItemPriority.NotRemovable, null);
return text3;
}
return itemUrl;
}
/// <summary>
/// Determines whether the current mode is either Experience Editor editing or preview mode.
/// </summary>
protected bool IsEditOrPreview
{
get
{
if (!this.PageMode.IsExperienceEditorEditing)
{
return this.PageMode.IsPreview;
}
return true;
}
}
/// <summary>
/// Generates a localized URL for the specified item, applying site and server URL logic.
/// </summary>
/// <param name="item">The Sitecore item.</param>
/// <param name="url">The base URL.</param>
/// <param name="options">The URL builder options.</param>
/// <param name="targetSite">The target site name.</param>
/// <returns>The localized URL.</returns>
private string GetLocalizedUrl(Item item, string url, ItemUrlBuilderOptions options, string targetSite)
{
Uri uri = null;
if (!url.StartsWith("/", StringComparison.Ordinal))
{
uri = new Uri(url);
url = uri.LocalPath;
}
if (options.AddAspxExtension.GetValueOrDefault())
{
url = url.Replace(".aspx", string.Empty);
}
string text = url;
if (options.AddAspxExtension.GetValueOrDefault() && text.Length > 1)
{
text += ".aspx";
}
if (options.AlwaysIncludeServerUrl.GetValueOrDefault() && uri != null)
{
string text2 = uri.Host;
string scheme = uri.Scheme;
SiteContext site = options.Site;
if (site != null)
{
SiteInfo siteInfo = site.SiteInfo;
if (!siteInfo.Scheme.IsNullOrWhiteSpace())
{
scheme = siteInfo.Scheme;
}
string text3 = siteInfo.ResolveTargetHostName();
if (text3 != null && !text3.IsNullOrWhiteSpace())
{
text2 = text3;
}
}
text = scheme + Uri.SchemeDelimiter + text2 + text;
}
if (!string.IsNullOrWhiteSpace(targetSite))
{
bool? flag = options.Site?.Properties["IsSxaSite"]?.Equals("true", StringComparison.OrdinalIgnoreCase);
if (flag.HasValue && flag.Value && (this.PageMode.IsExperienceEditorEditing || this.PageMode.IsPreview))
{
text = new UrlString(text).Add("sc_site", targetSite);
}
}
return text;
}
}
}
Now we need to patch the code implementation.
<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
<sitecore>
<linkManager>
<patch:attribute name="defaultProvider">overrideLinkManager</patch:attribute>
<providers>
<add name="overrideLinkManager" type="Foundation.Links.CustomLinkProvider, Foundation.Links"
languageEmbedding="always"
patch:before="add[@name='sitecore']" resolve="true"/>
</providers>
</linkManager>
</sitecore>
</configuration>
After patching, just deploy your code and you can verify the same in showconfig.aspx to see your custom link provided is added to LinkManager provider section. Now whenever the url generation happens for search page, it will first check if override link field has value, then it will take value as reference for url generation.
In my case i have pointed the default provider to my overrideLinkManager instead of switchableLinkProvider. So I don't need to add the custom link provider in site grouping as it is common for all my sites. But if you need this custom link provider specific for one site only, then use below patch file. Follow more in this SitecoreBlog
<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
<sitecore>
<linkManager>
<providers>
<add name="overrideLinkManager" type="Foundation.Links.CustomLinkProvider, Foundation.Links"
languageEmbedding="always"
patch:before="add[@name='sitecore']" resolve="true"/>
</providers>
</linkManager>
</sitecore>
</configuration>
Replace the customLinkProvider by overrideLinkManager as per our case.
Thank you for reading and happy learning!!!.
You can check my other blogs too if interested. Blog Website
Comments
Post a Comment