It’s been a busy couple of weeks. Our team has been working extra hard to complete development and content entry for a client’s new website, slated to launch later this month.
As part of our content entry, we discovered that general link fields using the internal link type were not respecting the anchor property. If an internal link was set and contained a value in the anchor sub field, it failed to render on the page; instead just the URL to the page would be included.
This baffled me as I was certain this functionality worked fine in earlier versions of Sitecore and/or with the regular .NET Framework setup. The same could not be said for our website though, as we are using the ASP.NET Core Rendering Engine and it appeared to not work at all.
Troubleshooting
As I started to investigate, I ended up looking into the code that handled how links are rendered with this rendering engine. I discovered in Sitecore.AspNet.RenderingEngine.TagHelpers.Fields.LinkTagHelper that there was no place that the anchor was being retrieved when rendering the HTML <a> tag. I thought that perhaps the anchor was coming through the Href property on HyperLinkField so I decided to do a bit more digging.
private static void RenderMarkup(TagHelperOutput output, HyperLinkField field)
{
if (output.TagName == null)
{
output.Content.SetHtmlContent((IHtmlContent) LinkTagHelper.GenerateLink(field.Value, output));
}
else
{
HyperLink hyperLink = field.Value;
output.Attributes.Add("href", (object) hyperLink.Href);
if (!string.IsNullOrWhiteSpace(hyperLink.Target) && !output.Attributes.ContainsName("target"))
output.Attributes.Add("target", (object) hyperLink.Target);
if (!string.IsNullOrWhiteSpace(hyperLink.Title) && !output.Attributes.ContainsName("title"))
output.Attributes.Add("title", (object) hyperLink.Title);
if (!string.IsNullOrWhiteSpace(hyperLink.Class) && !output.Attributes.ContainsName("class"))
output.Attributes.Add("class", (object) hyperLink.Class);
if (hyperLink.Target == "_blank" && !output.Attributes.ContainsName("rel"))
output.Attributes.Add("rel", (object) "noopener noreferrer");
if (!string.IsNullOrWhiteSpace(output.GetChildContentAsync()?.Result?.GetContent()) || string.IsNullOrWhiteSpace(hyperLink.Text))
return;
output.Content.Append(field.Value?.Text);
}
}
The RenderMarkup function (above) does not make use of any anchor-related data.
I looked into the layout response from Sitecore on a page that contained a link that should’ve had an anchor attached.
"link":{
"jsonValue":{
"value":{
"text":"View Canals Division Outline Map",
"anchor":"testanchor",
"linktype":"internal",
"class":"",
"title":"",
"target":"",
"querystring":"",
"id":"{C3883711-D3FE-4793-8808-89AB2472CB3C}",
"href":"/Some-Page/Another-Page"
}
}
}
What I discovered was that the anchor was being passed through as it’s own property on the link field, and that the href property did not include it.
This confirmed my earlier hunch that the RenderMarkup function in LinkTagHelper.cs did was not making use of the anchor property.
The Solution
I decided then that the best approach was to create my own custom link field tag helper and associated model. This was pretty straightforward as all I needed to do was add support for anchors into the existing tag helper.
I took the code from LinkTagHelper.cs and created a new class in my rendering host project called CustomHyperLinkTagHelper.cs. I then pasted the existing code into it and made a few minor adjustments, namely adding in checks for anchors and appending any available anchors to their link’s href value and updating the actual tag name for use in the .cshtml views.
However, I then noticed that the existing HyperLinkField model did not contain a property for anchor either. This meant that I needed to also create a new link field model, which I called CustomHyperLinkField and a new CustomHyperLink model in which I added an anchor property.
public class CustomHyperLink : HyperLink
{
/// <summary>
/// Gets the anchor for the link.
/// </summary>
[DataMember(EmitDefaultValue = false)]
public string? Anchor { get; set; }
}
The property added to the newly created CustomHyperLink model (above). This extends from the built-in HyperLink model from Sitecore.LayoutService.Client.Response.Model.Properties.
Once I had those new models wired up, I simply updated the references in the tag helper to make use of these, and the newly created Anchor property.
Finally, I updated any model and view references to make use of the new link models and tag.
Notes and Precautions
I’m not sure if this is a bug or is intentional, but my gut is telling me it’s the former. Thankfully it wasn’t too difficult to fix; the only painful part was updating all of the references and testing them to make sure I didn’t miss any 😊.
As with any Sitecore customization, you do run the risk of the base code being updated over time, or even fixing this particular issue. It’s good practice to keep an eye on any changes/fixes when new patches and releases go live.
The Code!
The full code is below. If anything doesn’t make sense or you need more clarification, please leave me a note below!
CustomHyperLinkTagHelper.cs
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
using YourNamespace.Helpers.Tags;
using Sitecore.Internal;
using Sitecore.LayoutService.Client.Response.Model.Fields;
using Sitecore.LayoutService.Client.Response.Model.Properties;
using System;
using System.Collections.ObjectModel;
using YourNamespace.Foundation.LayoutService.Models;
#nullable enable
namespace YourNamespace.Helpers.Tags
{
[HtmlTargetElement("sc-customlink", Attributes = "asp-for", TagStructure = TagStructure.NormalOrSelfClosing)]
[HtmlTargetElement("a", Attributes = "asp-for")]
public class CustomLinkTagHelper : TagHelper
{
private const string _href = "href";
private const string _target = "target";
private const string _title = "title";
private const string _class = "class";
private const string _anchorTag = "a";
private const string _rel = "rel";
private const string _blank = "_blank";
[HtmlAttributeName("asp-for")]
public ModelExpression For { get; set; }
public bool Editable { get; set; } = true;
public override void Process(TagHelperContext context, TagHelperOutput output)
{
Assert.ArgumentNotNull<TagHelperContext>(context, nameof(context));
Assert.ArgumentNotNull<TagHelperOutput>(output, nameof(output));
CustomHyperLinkField field = this.For?.Model is CustomHyperLinkField model ? model : (CustomHyperLinkField)null;
bool flag = this.Editable && !string.IsNullOrEmpty(field?.EditableMarkupFirst) && !string.IsNullOrWhiteSpace(field?.EditableMarkupLast);
if (field == null || string.IsNullOrWhiteSpace(field.Value.Href) && !flag)
return;
if (output.TagName != null && output.TagName.Equals("sc-customlink", StringComparison.OrdinalIgnoreCase) | flag)
output.TagName = (string)null;
if (flag)
CustomLinkTagHelper.RenderEditableMarkup(output, field);
else
CustomLinkTagHelper.RenderMarkup(output, field);
}
private static void RenderMarkup(TagHelperOutput output, CustomHyperLinkField field)
{
if (output.TagName == null)
{
output.Content.SetHtmlContent((IHtmlContent)CustomLinkTagHelper.GenerateLink(field.Value, output));
}
else
{
CustomHyperLink hyperLink = field.Value;
var hyperLinkHref = hyperLink.Href;
if (!string.IsNullOrWhiteSpace(hyperLink.Anchor))
{
// Remove the prefixed # if there is one.
var anchor = hyperLink.Anchor.Replace("#","");
hyperLinkHref += "#" + anchor;
}
output.Attributes.Add("href", (object)hyperLinkHref);
if (!string.IsNullOrWhiteSpace(hyperLink.Target) && !output.Attributes.ContainsName("target"))
output.Attributes.Add("target", (object)hyperLink.Target);
if (!string.IsNullOrWhiteSpace(hyperLink.Title) && !output.Attributes.ContainsName("title"))
output.Attributes.Add("title", (object)hyperLink.Title);
if (!string.IsNullOrWhiteSpace(hyperLink.Class) && !output.Attributes.ContainsName("class"))
output.Attributes.Add("class", (object)hyperLink.Class);
if (hyperLink.Target == "_blank" && !output.Attributes.ContainsName("rel"))
output.Attributes.Add("rel", (object)"noopener noreferrer");
if (!string.IsNullOrWhiteSpace(output.GetChildContentAsync()?.Result?.GetContent()) || string.IsNullOrWhiteSpace(hyperLink.Text))
return;
output.Content.Append(field.Value?.Text);
}
}
private static void RenderEditableMarkup(TagHelperOutput output, CustomHyperLinkField field)
{
DefaultTagHelperContent tagHelperContent = new DefaultTagHelperContent();
tagHelperContent.AppendHtml((IHtmlContent)new HtmlString(field.EditableMarkupFirst));
tagHelperContent.AppendHtml((IHtmlContent)new HtmlString(field.EditableMarkupLast));
output.Content.SetHtmlContent((IHtmlContent)tagHelperContent);
}
private static TagBuilder GenerateLink(CustomHyperLink hyperLink, TagHelperOutput output)
{
Assert.ArgumentNotNull<HyperLink>(hyperLink, nameof(hyperLink));
TagBuilder tagBuilder = new TagBuilder("a");
CustomLinkTagHelper.SetTagContent(hyperLink, output, tagBuilder);
CustomLinkTagHelper.SetAttributes(hyperLink, output, tagBuilder);
return tagBuilder;
}
private static void SetAttributes(
CustomHyperLink hyperLink,
TagHelperOutput output,
TagBuilder tagBuilder)
{
var hyperLinkHref = hyperLink.Href;
if (!string.IsNullOrWhiteSpace(hyperLink.Anchor))
{
// Remove the prefixed # if there is one.
var anchor = hyperLink.Anchor.Replace("#", "");
hyperLinkHref += "#" + anchor;
}
tagBuilder.Attributes.Add("href", hyperLinkHref);
if (!string.IsNullOrWhiteSpace(hyperLink.Target))
tagBuilder.MergeAttribute("target", hyperLink.Target);
if (!string.IsNullOrWhiteSpace(hyperLink.Title))
tagBuilder.MergeAttribute("title", hyperLink.Title);
if (!string.IsNullOrWhiteSpace(hyperLink.Class))
tagBuilder.MergeAttribute("class", hyperLink.Class);
foreach (TagHelperAttribute attribute in (ReadOnlyCollection<TagHelperAttribute>)output.Attributes)
tagBuilder.MergeAttribute(attribute.Name, attribute.Value.ToString(), true);
if (!(hyperLink.Target == "_blank") || tagBuilder.Attributes.Keys.Contains("rel"))
return;
tagBuilder.MergeAttribute("rel", "noopener noreferrer");
}
private static void SetTagContent(
CustomHyperLink hyperLink,
TagHelperOutput output,
TagBuilder tagBuilder)
{
string content = output.GetChildContentAsync().Result.GetContent();
if (!string.IsNullOrWhiteSpace(content))
tagBuilder.InnerHtml.SetHtmlContent(content);
else
tagBuilder.InnerHtml.SetContent(hyperLink.Text);
}
}
}
CustomHyperLink.cs
using Sitecore.LayoutService.Client.Response.Model.Properties;
using System.Runtime.Serialization;
namespace YourNamespace.Foundation.LayoutService.Models
{
public class CustomHyperLink : HyperLink
{
/// <summary>Gets or sets the title of the hyperlink.</summary>
[DataMember(EmitDefaultValue = false)]
public string? LinkType { get; set; }
/// <summary>
/// Gets the anchor for the link.
/// </summary>
[DataMember(EmitDefaultValue = false)]
public string? Anchor { get; set; }
}
}
CustomHyperLinkField
using Sitecore.LayoutService.Client.Response.Model;
namespace YourNamespace.Foundation.LayoutService.Models
{
public class CustomHyperLinkField : WrappedEditableField<CustomHyperLink>
{
/// <summary>
/// Initializes a new instance of the <see cref="T:Sitecore.LayoutService.Client.Response.Model.Fields.HyperLinkField" /> class.
/// </summary>
public CustomHyperLinkField()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="T:Sitecore.LayoutService.Client.Response.Model.Fields.HyperLinkField" /> class.
/// </summary>
/// <param name="value">The initial value.</param>
public CustomHyperLinkField(CustomHyperLink value) => this.Value = value;
}
}
Property Usage in Models
/// <summary>
/// The Link field.
/// </summary>
public CustomHyperLinkField? Link { get; set; }
Property Usage in Views
<sc-customlink asp-for="@Model.Link" class="arrow-link arrow-link--arrow">
</sc-customlink>

Leave a comment