Creating a custom rendering contents resolver

In my previous post, I covered the basics of how the Layout Service and Rendering Contents Resolvers work together to deliver component rendering data via the API for consumption by rendering hosts using Sitecore’s headless delivery options such as the JSS or .NET Core SDKs.

In this post we will look at why we might want to customise the structure and content of component rendering data returned via the API.

Let’s start with an example.

Dangerous Creatures of Australia

Everyone knows that Australia is stuffed to the gills with lethal creatures such as sharks, spiders, jellyfish, snakes, ants, and scorpions. In fact it is so dangerous that you should never come here. To warn people of the dangers, I’ve created a blog about the perils of these creatures.

Within a JSS site and JSS tenant hosted in SXA there is a root Blog node based on a Blog route template (which extends the default JSS App Route) and some child routes created using a Blog Article route template:

On individual Blog Article routes I’ve added a rendering via Standard Values presentation details which is called Recommended For You and which renders out some promo cards that point to recommended blog articles.

Recommended blogs rendering

This rendering uses a datasource containing a Multilist field which content managers can use to assign specific articles as “Recommended” articles:

The JSS app will iterate through these and render a promo card with an image, teaser text, and a link to the actual article.

Let’s take a look at what the output of this looks like using the default features of the Layout Service.:

As you can see, there is a single rendering Recommended Blogs that has a datasource assigned which contains 2 fields: moduleTitle and recommendedItems.

The recommendedItems field is a Multilist and that presents in the field value as a | separated list of item IDs representing the recommended blog articles. The first problem here is, well, what the heck are we supposed to do with a bunch of IDs? Our front end devs won’t be able to render a list of promo items that link to the recommended blogs when they only have some ID values to work with. We could use GraphQL but I’m going to use a rendering contents resolver because it will provide a more consistent, familiar data structure for the devs to work with (and if I didn’t, then this blog post would be called something else.)

Extending the Datasource Resolver

As I mentioned in my previous post, the rendering contents resolvers (except Sitecore Forms) all use the same code, just with different parameters. That code is in:

Sitecore.LayoutService.ItemRendering.ContentsResolvers.RenderingContentsResolver, Sitecore.LayoutService

To extend this functionality we can override the ResolveContents method (or implement IRenderingContentsResolver):

public override object ResolveContents(Sitecore.Mvc.Presentation.Rendering rendering, IRenderingConfiguration renderingConfig)

From there it’s a simple matter to get the datasource using GetContextItem. For this to work you can either set UseContextItem to false in the code, or uncheck the checkbox in the Sitecore Content Editor:

The first version of the code looks like this:

using Newtonsoft.Json.Linq;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.LayoutService.Configuration;
using System.Collections.Generic;
using System.Linq;

namespace JSSDemo.Feature.Blog.Extensions
{
    public class RecommendedBlogsContentsResolver1 : Sitecore.LayoutService.ItemRendering.ContentsResolvers.RenderingContentsResolver
    {
        private List<Item> items = new List<Item>();

        public override object ResolveContents(Sitecore.Mvc.Presentation.Rendering rendering, IRenderingConfiguration renderingConfig)
        {
            Assert.ArgumentNotNull(rendering, nameof(rendering));
            Assert.ArgumentNotNull(renderingConfig, nameof(renderingConfig));

            Item ds = GetContextItem(rendering, renderingConfig);

            var recommendedItemsFieldId = Templates.RecommendedForYouTemplate.RecommendedItemsFieldId;

            //if the rendering datasource has curated items
            if (ds.Fields.Contains(recommendedItemsFieldId) && !string.IsNullOrWhiteSpace(ds.Fields[recommendedItemsFieldId].Value))
            {
                List<string> recItemIds = ds.Fields[recommendedItemsFieldId].Value.Split('|').ToList();
                foreach (var id in recItemIds)
                {
                    var item = Sitecore.Context.Database.GetItem(new ID(id));
                    items.Add(item);
                }
            }
            
            if (!items.Any())
                return null;

            JObject jobject = new JObject()
            {
                ["items"] = (JToken)new JArray()
            };

            List<Item> objList = items != null ? items.ToList() : null;
            if (objList == null || objList.Count == 0)
                return jobject;
            jobject["items"] = ProcessItems(objList, rendering, renderingConfig);
            return jobject;
        }        
    }
}

The code is pretty straightforward, it:

  • gets the datasource item
  • checks that the recommendedItems field exists and retrieves the value
  • splits the list of Ids and iterates over it
  • gets each item from the context database and adds to a list of items
  • serialises the list of items and returns them to the layout service

[The Nuget packages required for this code are Sitecore.Kernel and Sitecore.LayoutService]

A new rendering contents resolver is added in Sitecore using this assembly:

And then applied to the rendering:

The rendering data in the Layout Service JSON now looks like this:

Which is much better. We now have an array of Sitecore Items and all the fields that we need (and some we don’t).

Improving the code

There’s a couple of things about the above output that are not ideal:

  • It is returning ALL of the fields. Ideally we want to only return what we need, otherwise the Layout Service JSON is unnecessarily bloated (especially if the articleContent field is populated.)
  • There is no URL to the item

You could look at using ContentSearch for retrieving the data, but the Rendering Contents Resolver serializer expects only items so a content search results model would still have to be mapped onto an item before serialization, which seems kind of pointless. I looked at extending the serializer code, but it was Items all the way down and not worth the effort. In terms of efficiency, this rendering will probably take advantage of the item cache and other caching options can be applied so that it doesn’t hit the database as frequently.

Another option you might think would be to modify the Fields property of the Item…..

20 Funny Memes On How To Say No | SayingImages.com

So in the end I settled on creating a model Sitecore template for the blog promo card which contained only the fields that I wanted to return, and then mapped the Blog item data onto an in-memory “fake” Item and serialized that. The model template has only a few fields:

The new code looks like this:

using Newtonsoft.Json.Linq;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Globalization;
using Sitecore.LayoutService.Configuration;
using System.Collections.Generic;
using System.Linq;

namespace JSSDemo.Feature.Blog.Extensions
{
    public class RecommendedBlogsContentsResolver2 : Sitecore.LayoutService.ItemRendering.ContentsResolvers.RenderingContentsResolver
    {
        private List<Item> items = new List<Item>();

        public override object ResolveContents(Sitecore.Mvc.Presentation.Rendering rendering, IRenderingConfiguration renderingConfig)
        {
            Assert.ArgumentNotNull(rendering, nameof(rendering));
            Assert.ArgumentNotNull(renderingConfig, nameof(renderingConfig));

            Item ds = GetContextItem(rendering, renderingConfig);

            var recommendedItemsFieldId = Templates.RecommendedForYouTemplate.RecommendedItemsFieldId;

            //if the rendering datasource has curated items
            if (ds.Fields.Contains(recommendedItemsFieldId) && !string.IsNullOrWhiteSpace(ds.Fields[recommendedItemsFieldId].Value))
            {
                List<string> recItemIds = ds.Fields[recommendedItemsFieldId].Value.Split('|').ToList();
                foreach (var id in recItemIds)
                {
                    var item = Sitecore.Context.Database.GetItem(new ID(id));
                    var articleModel = MapItemToRenderingModelItem(item, GetItemUrl(item));
                    items.Add(articleModel);
                }
            }
            
            if (!items.Any())
                return null;

            JObject jobject = new JObject()
            {
                ["items"] = (JToken)new JArray()
            };

            List<Item> objList = items != null ? items.ToList() : null;
            if (objList == null || objList.Count == 0)
                return jobject;
            jobject["items"] = ProcessItems(objList, rendering, renderingConfig);
            return jobject;
        }

        private Item MapItemToRenderingModelItem(Item item, string itemUrl)
        {
            var newId = new ID();
            var def = new ItemDefinition(newId, item.Name, Templates.BlogCardRenderingModel.TemplateId, ID.Null);

            //Populate fields
            var fields = new FieldList();
            fields.Add(Templates.BlogCardRenderingModel.Fields.ArticleTitle, item.Fields[Templates.BlogArticlePage.Fields.ArticleTitle].Value);

            //Use SuccessImage if available, else use HeaderImage
            if (item.Fields[Templates.BlogArticlePage.Fields.SuggestedImage].HasValue)
            {
                fields.Add(Templates.BlogCardRenderingModel.Fields.Image, item.Fields[Templates.BlogArticlePage.Fields.SuggestedImage].Value);
            }
            else
            {
                fields.Add(Templates.BlogCardRenderingModel.Fields.Image, item.Fields[Templates.BlogArticlePage.Fields.HeaderImage].Value);
            }

            fields.Add(Templates.BlogCardRenderingModel.Fields.Categories, item.Fields[Templates.BlogArticlePage.Fields.Categories].Value);
            fields.Add(Templates.BlogCardRenderingModel.Fields.Url, itemUrl);
            fields.Add(Templates.BlogCardRenderingModel.Fields.Teaser, item.Fields[Templates.BlogArticlePage.Fields.Teaser].Value);

            var data = new ItemData(def, Language.Current, Sitecore.Data.Version.First, fields);
            var db = Sitecore.Context.Database;

            var resultItem = new Item(newId, data, db);
            return resultItem;
        }

        private string GetItemUrl(Item item)
        {
            return Sitecore.Links.LinkManager.GetItemUrl(item);
        }
    }
}

Instead of adding raw Blog Article Items to the items collection, this code maps the retrieved Items onto a BlogCardRenderingModel which is created on the fly in code and returns a collection of those instead. I used Alistair Deneys’ (@adeneys) excellent blog post on this as a guide for creating the “fake” model item, although there are a few changes here. In particular:

  • Uses Version.First() instead of Version(1) – needed for Sitecore 10
  • This uses Language.Current instead of parsing the EN language
  • Uses Sitecore context database instead of the “dummy” database approach which did not work for me in Sitecore 10
  • Uses a “real” Sitecore template ID so that the necessary fields are available. Using a fake template ID does not work in this situation because the fields won’t exist for the mapping code to populate.

The mapping also calculates the URL for the item using LinkManager. The final JSON output now looks like this:

Each item in the rendering data now only contains the fields articleTitle, categories, image, teaserText, url, id and name, which is a lot leaner and prevents the Layout Service from becoming unnecessarily bloated.

Conclusion

Modifying or extending the contents resolver allows us to reduce bloat and return exactly the data we need, but it also opens up opportunities to pull data in from other sources for use in renderings, combine data from the Content Search API with Sitecore item queries, and filter, sort, and group the data in different ways.

[The code and content for this post and the previous one are on Github]

One thought on “Creating a custom rendering contents resolver

Comments are closed.