Custom Link Expansion in Sitecore: A Deep Dive into ExpandDynamicLinks in Link Provider
Hello everyone, This blog is continuation of my previous blog where we have seen how we can override the default link provider in XM Cloud. Now let's understand, how we can customize the behavior of links available withing Rich Text field.
In Sitecore, managing links correctly is crucial—especially when dealing with Rich Text fields, emails, or content reuse across multiple sites. One of the key methods that helps with this is ExpandDynamicLinks, available in the LinkProvider class.Let’s explore what this method does, why it matters, and where you might use or override it in your Sitecore solution
What is ExpandDynamicLinks?
The ExpandDynamicLinks method is part of the Sitecore.Links.LinkProvider class. It's used to scan HTML or text content and replace Sitecore’s dynamic links (like ~/link.aspx?...) with friendly, human-readable URLs.
Method Signature
public virtual string ExpandDynamicLinks(string text, bool resolveSites);
Parameters:
- text: A string containing HTML or rich text content with potential dynamic links
- resolveSites: A boolean indicating whether the link should be resolved in the context of the site.
What Are Dynamic Links?
When a content editor adds an internal link in a Rich Text field, Sitecore stores it in a dynamic format, such as: 'href="~/link.aspx?_id=110D5590-DEE1-42EA-9CDA-3216E7C8E4C3&lang=en"'
This dynamic URL ensures the link is portable across environments or site structures. But it’s not end-user-friendly. That’s where ExpandDynamicLinks comes in—it transforms this into something like:
Contact Us
Use Case: Page has a field such as "Override URL", allowing authors to specify a different link for a given page. Whenever Content author add the page link to Rich Text field, we need to modify the behavior during URL generation to point to url specified in override field of that page.
Why this matters:
- Ensures internal links resolve correctly across environments or multisite instances.
- Prevents broken links in content migration scenarios
- Improves SEO and accessibility with clean URLs.
So let's start implementing the same:
Step 1:Create Custom Link Provider class (CustomLinkProvider.cs) inheriting from Sitecore default Link Provider class. This class will have a method which is overriding default ExpandDynamicLinks method. Inside this overriden method, we nedd to create an instance of custom LinkParser class as mentioned below CustomLinkParser.cs
public class CustomLinkProvider : LinkProvider
{
public override string ExpandDynamicLinks(string text, bool resolveSites)
{
Assert.ArgumentNotNull((object)text, nameof(text));
CustomLinkParser linkParser = new CustomLinkParser();
ItemUrlBuilderOptions urlBuilderOptions = (ItemUrlBuilderOptions)this.GetDefaultUrlBuilderOptions();
urlBuilderOptions.SiteResolving = new bool?(resolveSites);
return linkParser.ExpandDynamicLinks(text, urlBuilderOptions);
}
}
Step 2: Create CustomLinkParser class. This will basically inherit from default LinkParser class. This class has method called GetExpanders which basically fetch all available expanders link Item, Media etc. To this method we add our own custom ItemLinkExpander where we call our dynamic link manipulator which will you see in some time below.
public class CustomLinkParser : LinkParser
{
protected override IEnumerable GetExpanders()
{
return base.GetExpanders()
.Where(x => !(x is ItemLinkExpander))
.Append((CustomItemLinkExpander)new ItemLinkExpander());
}
}
Step 3: Create CustomItemLinkExpander class. This inherits from default ItemLinkExpander which has parse method from DynamicLink.We will create our own CustomDynamicLink class and will utilize our own parse method.
public class CustomItemLinkExpander : ItemLinkExpander
{
[Obsolete("Please use Expand(ref string, UrlBuilderOptions) instead.")]
public override void Expand(ref string text, UrlOptions urlOptions)
{
this.Expand(ref text, (ItemUrlBuilderOptions)urlOptions);
}
public override void Expand(ref string text, UrlBuilderOptions urlOptions)
{
this.Expand(ref text, (ItemUrlBuilderOptions)urlOptions);
}
private void Expand(ref string text, ItemUrlBuilderOptions urlOptions)
{
Assert.ArgumentNotNull((object)text, nameof(text));
Assert.ArgumentNotNull((object)urlOptions, nameof(urlOptions));
int startIndex1 = text.IndexOf("~/link.aspx?", StringComparison.InvariantCulture);
if (startIndex1 == -1)
return;
StringBuilder stringBuilder = new StringBuilder(text.Length);
int startIndex2 = 0;
for (; startIndex1 >= 0; startIndex1 = text.IndexOf("~/link.aspx?", startIndex2, StringComparison.InvariantCulture))
{
int num = text.IndexOf("_z=z", startIndex1, StringComparison.InvariantCulture);
if (num < 0)
{
text = stringBuilder.ToString();
return;
}
string url = CustomDynamicLink.Parse(text.Substring(startIndex1, num - startIndex1)).GetUrl(urlOptions);
string str = text.Substring(startIndex2, startIndex1 - startIndex2);
stringBuilder.Append(str);
stringBuilder.Append(url);
startIndex2 = num + "_z=z".Length;
}
stringBuilder.Append(text.Substring(startIndex2));
text = stringBuilder.ToString();
}
}
Step 3: Create CustomDynamicLink class. This class will be inherited from DynamicLink class which has method BuildItemUrl. We will add our main logic to build the url.
public class CustomDynamicLink : DynamicLink
{
public DynamicLink(DynamicLink link)
{
this.LinkText = link.LinkText;
this.ItemId = link.ItemId;
this.Language = link.Language;
this.LinkType = link.LinkType;
this.Site = link.Site;
}
public static CustomDynamicLink Parse(string linkText)
{
Assert.ArgumentNotNull((object)linkText, nameof(linkText));
return new CustomDynamicLink(LinkManager.ParseDynamicLink(linkText));
}
protected override string BuildItemUrl(ItemUrlBuilderOptions options)
{
Assert.ArgumentNotNull((object)options, nameof(options));
Item obj = this.GetItem();
if (obj == null)
return this.SetLinkItemNotFoundError();
if (obj.Fields[Fields.OverrideLinkFieldName] != null && obj.Fields[Fields.OverrideLinkFieldName].HasValue)
{
LinkField redirectUrlField = obj.Fields[Fields.OverrideLinkFieldName];
if (redirectUrlField != null && !string.IsNullOrWhiteSpace(redirectUrlField.Value))
{
return redirectUrlField.GetFriendlyUrl();
}
}
Language language = this.Language;
if (language != (Language)null)
{
options = (ItemUrlBuilderOptions)options.Clone();
options.Language = language;
}
return LinkManager.GetItemUrl(obj, options);
}
private Item GetItem()
{
ID itemId = this.GetItemId();
if ((object)itemId == null)
return (Item)null;
Database database = this.GetDatabase();
if (database == null)
return (Item)null;
Language language = this.GetLanguage();
return ItemManager.GetItem(itemId, language, Sitecore.Data.Version.Latest, database, SecurityCheck.Disable);
}
private ID GetItemId() => !ID.IsNullOrEmpty(this.ItemId) ? this.ItemId : (ID)null;
private Language GetLanguage()
{
Language language = this.Language;
return (object)language != null ? language : Language.Current;
}
private Database GetDatabase()
{
Database currentValue = Switcher.CurrentValue;
if (currentValue != null)
return currentValue;
return this.GetSite()?.Database;
}
private SiteContext GetSite() => this.Site ?? Context.Site;
private string SetLinkItemNotFoundError()
{
string part1 = string.Empty;
Database database = this.GetDatabase();
if (database != null)
part1 += database.Name;
ID itemId = this.GetItemId();
if ((object)itemId != null)
part1 = StringUtil.Combine((object)part1, (object)itemId, ":");
Language language = this.GetLanguage();
return WebUtil.AddQueryString(Sitecore.Configuration.Settings.LinkItemNotFoundUrl, "item", StringUtil.Combine((object)part1, (object)language, "@"));
}
}
You can see the above highlighed code where we are doing the actual processing. This way we can manipulate the links inside ricj text. I hope you might like this blogs. This is not only restricted to XP, it can be used in all version of Sitecore Platform like headless, XM Cloud.
Thanks for reading and happy learning!!!
You can check my other blogs too if interested. Blog Website
Comments
Post a Comment