Monday, October 12, 2026

Sitecore SXA resolvedlinks

Sitecore SXA ComputedIndexField: resolvedlinks


A while ago - actually 8 years apparently, time flies... - I wrote an article about Sitecore SXA and the search related features. It included the use of "resolvedlinks".  This is a piece of code that is used by SXA for some ComputedFields in the index, but it can be used for our own use as well. 

A Sitecore "computed index field" lets you inject custom logic to generate and/or transform index data dynamically during the indexing process instead of mapping raw field text directly. This allows you to add a field that is not a direct copy of the content of a field. For the rest it is a normal index field, with all the options for such a field on tokenization and such. 

The idea behind the resolvedLinks implementation is simple. If you have an item that contains a reference field like a Multilist of Treelist , the index will store the guids used in that reference field as that is the raw content. In some cases you do want not the guid but an actual value representing that reference. 

In that old blog I had an example as we used the resolvedLinks to add computed index fields to be used as facets. You do not want to show guids as facets - you want an actual string that defines the referenced item.

I already used it on other places as well when I wanted the data to be available in the index for retrieval to avoid having to fetch the referenced items on each request. Several use cases are possible, but recently we bumped into an issue where it didn't fully match our requirements anymore. 

The issue was quotes. Or other weird characters probably. But definetely quotes. 

Taxonomy information architecture

Actually it comes to down to how we setup things in the first place. We never assumed the editors would be using those characters in page titles and that was our mistake. So the "resolvedLinks" is linked to the title of those pages (as requested by the customer and the analysts as this was the most consistent solution). And that is a bad idea...  so we learned now that we should provide an additional field to enter the name or title to use in the taxonomy. 

But of course, editors are human (for now) and as such lazy.  I know there are options to make sure this field is filled, using a copy of the title if needed and such.  But we also have other options. 

The solution

First of all, we did create the extra field on the items to add an optional taxonomy title. Optional, because if the page title was sufficient that is also ok for us. And this also means all the existing data is also still good - backwards compatibility is important on existing sites with quite a bit of content.

We decided to create our own version of the resolvedLinks, one that supports a fallback field. This way we can attach it to the new taxonomy title field but keep the title field as backup in case the new field is empty.


using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml;
using Sitecore.ContentSearch;
using Sitecore.ContentSearch.ComputedFields;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Globalization;

namespace Foundation.Taxonomy.Index
{
    public class ResolvedLinksWithFallback : AbstractComputedIndexField
    {
        public ResolvedLinksWithFallback()
        {
        }

        public ResolvedLinksWithFallback(XmlNode node) : base(node)
        {
            ContentField = node.Attributes?["contentField"]?.Value;
            FallbackContentField = node.Attributes?["fallbackContentField"]?.Value;
            ReferenceField = node.Attributes?["referenceField"]?.Value;
        }

        public string ContentField { get; set; }

        public string FallbackContentField { get; set; }

        public string ReferenceField { get; set; }

        public override object ComputeFieldValue(IIndexable indexable)
        {
            if (string.IsNullOrEmpty(ReferenceField))
            {
                return null;
            }

            var referencedValues = GetReferencedValuesForField(indexable, ReferenceField);
            return referencedValues != null && referencedValues.Any() ? (object)referencedValues : null;
        }

        protected virtual IEnumerable<string> GetReferencedValuesForField(IIndexable indexable, string fieldName)
        {
            var sourceField = GetSourceField(indexable, fieldName);
            if (sourceField == null)
            {
                return null;
            }

            var ids = GetIds((string)sourceField.Value);
            return !ids.Any() ? null : GetReferencedValues(ids, indexable);
        }

        protected virtual IEnumerable<string> GetReferencedValues(IEnumerable<ID> referencedIds, IIndexable indexable)
        {
            var referencedValues = new List<string>();
            if (!(indexable is SitecoreIndexableItem indexable1))
            {
                return referencedValues;
            }

            var language = Getlanguage(indexable1);
            foreach (ID referencedId in referencedIds)
            {
                var referecedItem = GetReferencedItem(referencedId, indexable1.Item.Database, language);
                if (referecedItem == null)
                {
                    continue;
                }

                var contentValue = !string.IsNullOrEmpty(ContentField) ? GetFieldValue(referecedItem, ContentField) : null;

                if (string.IsNullOrEmpty(contentValue) && !string.IsNullOrEmpty(FallbackContentField))
                {
                    contentValue = GetFieldValue(referecedItem, FallbackContentField);
                }

                if (!string.IsNullOrEmpty(contentValue))
                {
                    referencedValues.Add(contentValue);
                }
            }

            return referencedValues;
        }

        protected virtual string GetFieldValue(Item item, string fieldName) => item.Fields[fieldName]?.Value;

        protected virtual IEnumerable<ID> GetIds(string fieldValue) => fieldValue
            .Split(new char[1] { '|' }, StringSplitOptions.RemoveEmptyEntries)
            .Where(ID.IsID)
            .Select(ID.Parse)
            .ToList();

        protected virtual Language Getlanguage(SitecoreIndexableItem indexable)
        {
            return indexable.Item?.Language;
        }

        protected virtual ID GetId(string id) => ID.IsID(id) ? ID.Parse(id) : null;

        protected virtual IIndexableDataField GetSourceField(IIndexable indexable, string fieldName)
        {
            return indexable.Fields.FirstOrDefault(field => field != null && field.Name.Equals(fieldName, StringComparison.OrdinalIgnoreCase));
        }

        protected virtual Item GetReferencedItem(ID id, Database database, Language language)
        {
            return database.GetItem(id, language);
        }
    }
  }

So with this bit of code we can now also use the resolved links feature with a fallback field, making it backwards compatible for us. Editors can keep on being lazy and fill in just the title - but they can overrule it if wanted and/or needed for the taxonomy to work.

Using the new code is the same as for the out-of-the-box resolvedlinks:

<sitecore>
  <contentSearch>
    <indexConfigurations search:require="solr">
      <defaultSolrIndexConfiguration>
        <documentOptions>
          <fields hint="raw:AddComputedIndexField">
            <field fieldName="F1" type="Sitecore.XA.Foundation.Search.ComputedFields.ResolvedLinks, Sitecore.XA.Foundation.Search" returnType="stringCollection" referenceField="Field1" contentField="title"/>
            <field fieldName="F2" type="Foundation.Taxonomy.Index.ResolvedLinksWithFallback, Foundation.Taxonomy" returnType="stringCollection" referenceField="Field2" contentField="taxonomytitle" fallbackContentField="title"/>
          </fields>
        </documentOptions>
      </defaultSolrIndexConfiguration>
    </indexConfigurations>
  </contentSearch>
</sitecore>

Once you move to SitecoreAI this becomes all obsolete, but for all those still working on Sitecore XM (with SXA - with or without JSS) this is hopefully still useful.


No comments:

Post a Comment