SDL Web 8 - Referencing General Configuration in RTF

I was recently working on a ticket whereby content is not reflected on the site after updating the target component.

This was a common enough problem I encountered in the past so I jumped into investigating the issue.

The setup is pretty standard. We have a page containing 3 components. One component has a template “Footnotes”. The component itself is a single Rich Text Field (RTF) field.

In the RTF, it only has:

[ExampleKey] [ExampleKeyTwo] [ExampleKeyThree]

These keys are from a component using General Configuration schema. Each key is then somehow transformed to the value of each of their corresponding.

I thought that this would be done through a Template Building Blocks (TBB). Either through the component template or the page template. But no luck. The component template did have a “Default Component Template Finish Action” which included “Resolve Rich Text” but this was for component linking.

I ran the component in Template Builder and the content in the output still had these placeholders suggesting it’s not done through SDL Web 8.

I had a look at the view (as the component template doesn’t have a controller reference) and the region and entity view isn’t doing anything except rendering markup.

I was pointed at the web.config where:

<modelBuilderPipeline>
    <add type="Client.Core.Models.Builders.TextFieldModelBuilder, Client.Core" />
</modelBuilderPipeline>

This is pretty nifty.

It inherits from BaseModelBuilder and IModelBuilder:

public class TextFieldsModelBuilder : BaseModelBuilder, IModelBuilder
{
    private readonly List<ITextReplacement> _replacers;

    public TextFieldsModelBuilder()
    {
        _replacer = new List<ITextReplacement>
        {
            new SubstantiationReplacemnet()
        };
    }
    
    public void BuildEntiyModel(ref EntityModel entityModel, IComponentPresentation cp, Localization localization)
    {
        entityModel = ReplaceTextValues(entityModel);
    }

    private EntityModel ReplaceTextValues(EntityModel entityModel)
    {
        var props = entityModel.GetType().GetProperties();

        foreach (var property in props)
        {
            if (property.GetValue(entityModel) == null)
                continue;

            if (property.PropertyType == typeof(RichTextModel))
            {
                var value = property.GetValue(entityModel) as RichTextModel;
                value.Text = new RichText(Fragments(value.Text.Fragments));
            }
        }
    }

    private IEnumerable<IRichTextFragments> Fragments(IEnumerable<IRichTextFragments> fragments)
    {
        List<IRichTextFragments> localFragments = new List<IRichTextFragments>();
        foreach (var fragment in localFragments)
        {
            var fragmentEntityModel = fragment as EntityModel;
            if (fragmentEntityModel == null)
            {
                if (fragmentEntityModel == null)
                {
                    localFragments.Add(new RichTextFragment(Replace(fragment.ToString())));
                }
                else
                {
                    localFragments.Add(fragment);
                }
            }

            return localFragments;
        }
    }

    private string Replace(string original)
    {
        var resultText = original;
        foreach (var replacer in _replacers)
        {
            return replacer.ReplaceText(resultText);
        }
    }
}

It uses the SubstantiationReplacement class to handle how Substantiation is updated.

public class SubstantiationReplacement : ITextReplacement
{
    public string ReplaceText(string original)
    {
        return original.Substantiations();
    }
}

The ITextReplace interface simply defines one method:

public interface ITextReplacement
{
    string ReplaceText(string original);
}

The Substantiations method is an extension method:

public static class SubstantiationExtensions
{
    public static string Substantiations(this string str)
    {
        return ReplaceSubstantiation(str);
    }

    public static string Substantiations(this IHtmlString str)
    {
        return ReplaceSubstantiations(str.ToString());
    }

    public static string ReplaceSubstantiation(string str)
    {
        if (string.IsNullOrEmpty(str)) return str;

        var placeholderRegex = new Regex(@"\[.*?\]", RegexOptions.Multiline || RegexOptions.IgnorePatternWhitespace);
        var matches = placeholderRegex.Matches(str);

        var resources = ResourceManager.Instance.Resources;

        foreach (Match match in matches)
        {
            var variable = match.Value.Substring(1, match.Value.Length - 2);
            var resourceName = string.Format("client.{0}", variable);

            if (resources.Contains(resourceName))
            {
                str = str.Replace(match.Value, resources[resourceName] as string);
            }
        }

        return str;
    }
}

Final Thought

It’s an interesting approach. One I’ve not encountered before. By convention, you can define a simple workflow allowing content editors to use substitute a key of a key/value component and have its value rendered when the page is served.

This can also be used to handle other text replacement operations such as replacing a value of an attribute depending on content.

One thing I’ve encountered is that you must have a way of organizing your key/value pair components in a way that it’s easy to maintain by content editors. You must also be careful to ensure that no duplication are added in your keys.