In this post I'll show you how to customize the output of renderings from the Layout Service in Sitecore JavaScript Services (JSS) when you're working in connected/integrated/headless mode.

If you're interested in customizing the output of renderings from the Layout Service in disconnected mode, check out my other post.

Layout Service Rendering Data

The data that the Layout Service sends to your JSS app looks something like this (I've omitted a lot of properties for brevity):

{
  "sitecore": {
    "context": {
      "...omitting": "for brevity..."
    },
    "route": {
      "...omitting": "for brevity...",
      "placeholders": {
        "jss-main": [
          {
            "uid": "2c4a53cc-9da8-5f51-9d79-6ee2fc671b2d",
            "componentName": "ContentBlock",
            "dataSource": "{0014F97C-E1BC-501D-9219-069D52F863F5}",
            "fields": {
              "heading": {
                "value": "Welcome to Sitecore JSS"
              },
              "content": {
                "value": "<p>JSS rocks!</p>\n"
              }
            }
          }
        ]
      }
    }
  }
}

The items in the placeholders object on line 8 are what get served up to your JSS components, like this ContentBlock component from the JSS React template:

const ContentBlock = ({ fields }) => (
  <React.Fragment>
    <Text tag="h2" className="display-4" field={fields.heading} />

    <RichText className="contentDescription" field={fields.content} />
  </React.Fragment>
);

This component is rendering fields, but you could easily consume the uid, componentName, and dataSource properties returned from the Layout Service like below:

const ContentBlock = ({ fields, rendering }) => (
  <React.Fragment>
    <Text tag="h2" className="display-4" field={fields.heading} />

    <RichText className="contentDescription" field={fields.content} />
    <ul>
      <li>uid: {rendering.uid}</li>
      <li>componentName: {rendering.componentName}</li>
      <li>dataSource: {rendering.dataSource}</li>
    </ul>
  </React.Fragment>
);

What if you want to include some other data on your renderings that comes from the server, but not from data source fields? For example, if you wanted to return a list of members from the Atlanta Sitecore User Group as part of your ContentBlock renderings like this:

{
  "uid": "2c4a53cc-9da8-5f51-9d79-6ee2fc671b2d",
  "componentName": "ContentBlock",
  "dataSource": "{0014F97C-E1BC-501D-9219-069D52F863F5}",
  "fields": {
    "heading": {
      "value": "Welcome to Sitecore JSS"
    },
    "content": {
      "value": "<p>JSS rocks!</p>\n"
    }
  },
  "atlSugMembers": [
    "George \"Sitecore George\" Chang",
    "Martin \"Sitecore Artist\" English",
    "Anastasiya \"JavaScript Ninja\" Flynn",
    "Varun \"Too Many Questions\" Nehra",
    "Craig \"From ATL\" Taylor",
    "Amy \"Sitecore Amy\" Winburn"
  ]
}

With a bit of code you can add extensions like this easily.

TL;DR: Show Me the Code!

Dive straight in here on GitHub: https://github.com/coreyasmith/jss-extensible-json-renderings.

The Example

For this post, we'll update the Sitecore Layout Service to add ATLSUG members to JSON Renderings that have an Add ATLSUG Members checkbox field checked.

This post assumes that you're working with JSS Tech Preview 4. This may not be possible on earlier versions of JSS, and the technique may be simpler and/or quite different in the final version that ships later this year.

Extend Renderings from Sitecore

Add ATLSUG Members Checkbox

We'll add a checkbox to the Json Rendering template that, when checked on a JSON rendering, will cause the Layout Service to add a list of ATLSUG members to that rendering.

Navigate to /sitecore/templates/JavaScriptServices/Json Rendering and add a section called ATLSUG. Add a Checkbox field called Add ATLSUG Members. Make note of the ID of this field. It's {05C24A58-91B7-458D-A664-AB5F463DBB57} for me and that's what I'll use later in this post.

Now if you navigate to any of your Json Renderings under /sitecore/layout/Renderings you'll find an ATLSUG section with the Add ATLSUG Members field. If you've just done an import from JSS disconnected mode, you may need to unprotect the item to check this field by clicking Configure in the Content Editor ribbon and then clicking on Unprotect Item.

Add ATLSUG Members Checkbox

Check this field on your ContentBlock rendering.

Pipeline Placeholder Transformer

The Layout Service uses a class called PlaceholderTransformer to turn your JSON Renderings into a format that is ready to be serialized into JSON.

Out of the box, the Placeholder Transformer is not extensible. This can be fixed easily by creating a PipelinePlaceholderTransformer that executes a pipeline. For this class, you will need to add references to both the Sitecore.JavaScriptServices.ViewEngine.dll and Sitecore.LayoutService.dll assemblies that ship with the JSS module in your project--neither are available on NuGet yet.

using System;
using CoreySmith.Foundation.LayoutService.ItemRendering;
using CoreySmith.Foundation.LayoutService.Pipelines.LayoutService.TransformPlaceholderElement;
using Sitecore.Abstractions;
using Sitecore.JavaScriptServices.ViewEngine.LayoutService;
using Sitecore.LayoutService.ItemRendering;

namespace CoreySmith.Foundation.LayoutService.Serialization
{
  public class PipelinePlaceholderTransformer : PlaceholderTransformer
  {
    private const string GroupName = "layoutService";
    private const string PipelineName = "transformPlaceholderElement";

    private readonly BaseCorePipelineManager _pipelineManager;

    public PipelinePlaceholderTransformer(BaseCorePipelineManager pipelineManager)
    {
      _pipelineManager = pipelineManager ?? throw new ArgumentNullException(nameof(pipelineManager));
    }

    public override object TransformPlaceholderElement(RenderedPlaceholderElement element)
    {
      var transformedElement = base.TransformPlaceholderElement(element);
      var renderingConfigurationName = GetRenderingConfigurationName(element);

      var args = new TransformPlaceholderElementPipelineArgs(element, renderingConfigurationName, transformedElement);
      _pipelineManager.Run(PipelineName, args, GroupName);
      return args.Result;
    }

    private static string GetRenderingConfigurationName(RenderedPlaceholderElement element)
    {
      return (element as ExtensibleRenderedJsonRendering)?.RenderingConfigurationName ?? string.Empty;
    }
  }
}

This does the same thing as the out-of-the-box PlaceholderTransformer, but calls a custom <transformPlaceholderElement /> pipeline after preparing the rendering for serialization. Processors for this pipeline inherit from this base class:

using System;
using System.Collections.Generic;
using System.Linq;

namespace CoreySmith.Foundation.LayoutService.Pipelines.LayoutService.TransformPlaceholderElement
{
  public abstract class TransformPlaceholderElementProcessor
  {
    public List<string> AllowedConfigurations { get; set; } = new List<string>();

    public void Process(TransformPlaceholderElementPipelineArgs args)
    {
      if (args == null) throw new ArgumentNullException(nameof(args));
      if (args.RenderingConfigurationName == null) throw new ArgumentNullException(args.RenderingConfigurationName);

      if (!IsConfigurationAllowed(args.RenderingConfigurationName)) return;
      TransformPlaceholderElement(args);
    }

    protected virtual bool IsConfigurationAllowed(string configurationName)
    {
      return !AllowedConfigurations.Any() || AllowedConfigurations.Contains(configurationName);
    }

    public abstract void TransformPlaceholderElement(TransformPlaceholderElementPipelineArgs args);
  }
}

and accept args of this type:

using System;
using Sitecore.LayoutService.ItemRendering;
using Sitecore.Pipelines;

namespace CoreySmith.Foundation.LayoutService.Pipelines.LayoutService.TransformPlaceholderElement
{
  public class TransformPlaceholderElementPipelineArgs : PipelineArgs
  {
    public RenderedPlaceholderElement Element { get; set; }
    public string RenderingConfigurationName { get; set; }
    public dynamic Result { get; set; }

    public TransformPlaceholderElementPipelineArgs(RenderedPlaceholderElement element, string renderingConfigurationName, dynamic transformedElement)
    {
      Element = element ?? throw new ArgumentNullException(nameof(element));
      RenderingConfigurationName = renderingConfigurationName ?? throw new ArgumentNullException(nameof(renderingConfigurationName));
      Result = transformedElement ?? throw new ArgumentNullException(nameof(transformedElement));
    }
  }
}

With these three classes, you can add as many processors as you like to this transformPlaceholderElement pipeline:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <group groupName="layoutService">
        <pipelines>
          <transformPlaceholderElement performanceCritical="true">
            <processor type="YourAssembly.YourProcessor, YourAssembly">
              <allowedConfigurations hint="list">
                <config desc="jss">jss</config>
              </allowedConfigurations>
            </processor>
          </transformPlaceholderElement>
        </pipelines>
      </group>
    </pipelines>
  </sitecore>
</configuration>

Processors in this pipeline are configuration aware, so if you want some processors to execute for only certain Layout Service configurations, list the applicable configurations in the <allowedConfigurations /> node within your <processor /> registration. If you don't specify any configurations, the processor will execute for all configurations.

Register the PipelinePlaceholderTransformer with the Sitecore container like this:

using CoreySmith.Foundation.LayoutService.Serialization;
using Microsoft.Extensions.DependencyInjection;
using Sitecore.DependencyInjection;
using Sitecore.JavaScriptServices.ViewEngine.LayoutService;

namespace CoreySmith.Foundation.LayoutService
{
  public class LayoutServiceConfigurator : IServicesConfigurator
  {
    public void Configure(IServiceCollection serviceCollection)
    {
      serviceCollection.AddTransient<IPlaceholderTransformer, PipelinePlaceholderTransformer>();
    }
  }
}

And the Layout Service will now execute the <transformPlaceholderElement /> pipeline for every rendering!

Extensible JSON Rendering

The PipelinePlaceholderTransformer works with a model of your JSON Renderings that is pretty decoupled from Sitecore--you don't have a reference to the JSON Rendering from which the model was created. Below is is a decorator for the JSON rendering model that has a reference to both the Layout Service Configuration Name and Sitecore Rendering it was created for:

using System;
using Sitecore.JavaScriptServices.ViewEngine.ItemRendering;
using Sitecore.Mvc.Presentation;

namespace CoreySmith.Foundation.LayoutService.ItemRendering
{
  public class ExtensibleRenderedJsonRendering : RenderedJsonRendering
  {
    public string RenderingConfigurationName { get; }
    public Rendering Rendering { get; }

    public ExtensibleRenderedJsonRendering(RenderedJsonRendering innerRendering, string renderingConfigurationName, Rendering rendering)
      : base(innerRendering)
    {
      RenderingConfigurationName = renderingConfigurationName ?? throw new ArgumentNullException(nameof(renderingConfigurationName));
      Rendering = rendering ?? throw new ArgumentNullException(nameof(rendering));
    }
  }
}

If you scroll back up to the PipelinePlaceholderTransformer, you'll see we cast the RenderedPlaceholderElement to this type to get the RenderingConfigurationName. We can then use that in the <transformPlaceholderElement /> pipeline to determine whether or not the processors are running in an allowed configuration.

Put the ExtensibleRenderedJsonRendering into action with the following processor:

using System;
using CoreySmith.Foundation.LayoutService.ItemRendering;
using Sitecore.JavaScriptServices.ViewEngine.ItemRendering;
using Sitecore.LayoutService.Configuration;
using Sitecore.LayoutService.Presentation.Pipelines.RenderJsonRendering;

namespace CoreySmith.Foundation.LayoutService.Pipelines.LayoutService.RenderJsonRendering
{
  public class WrapWithExtensibleRenderedJsonRendering : BaseRenderJsonRendering
  {
    public WrapWithExtensibleRenderedJsonRendering(IConfiguration configuration)
      : base(configuration)
    {
    }

    protected override void SetResult(RenderJsonRenderingArgs args)
    {
      if (args == null) throw new ArgumentNullException(nameof(args));
      if (args.RenderingConfiguration == null) throw new ArgumentNullException(nameof(args.RenderingConfiguration));

      if (!(args.Result is RenderedJsonRendering jssRenderedJsonRendering)) return;
      args.Result = new ExtensibleRenderedJsonRendering(jssRenderedJsonRendering, args.RenderingConfiguration.Name, args.Rendering);
    }
  }
}

Plug this in to the last position of the Layout Service's <renderJsonRendering /> pipeline:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <group groupName="layoutService">
        <pipelines>
          <renderJsonRendering>
            <processor type="CoreySmith.Foundation.LayoutService.Pipelines.LayoutService.RenderJsonRendering.WrapWithExtensibleRenderedJsonRendering, CoreySmith.Foundation.LayoutService"
                       resolve="true"
                       patch:after="processor[last()]" />
          </renderJsonRendering>
        </pipelines>
      </group>
    </pipelines>
  </sitecore>
</configuration>

Now you have all of the plumbing in place to customize renderings from the Layout Service however you like!

Add ATLSUG Members to Renderings

Finally, the part you've been waiting for: customization of renderings in the Layout Service. With all of the above infrastructure in place, this is easy going forward 😊. Create a pipeline processor that derives from TransformPlaceholderElementProcessor:

using CoreySmith.Foundation.LayoutService.ItemRendering;
using CoreySmith.Foundation.LayoutService.Pipelines.LayoutService.TransformPlaceholderElement;
using Sitecore.Data.Fields;

namespace CoreySmith.Feature.AtlSug.Pipelines.LayoutService.TransformPlaceholderElement
{
  public class AddAtlSugMembers : TransformPlaceholderElementProcessor
  {
    private const string AddAtlSugMembersFieldId = "{05C24A58-91B7-458D-A664-AB5F463DBB57}"  

    public override void TransformPlaceholderElement(TransformPlaceholderElementPipelineArgs args)
    {
      if (!(args.Element is ExtensibleRenderedJsonRendering extensibleRendering)) return;
      if (!ShouldAddAtlSugMembers(extensibleRendering)) return;

      args.Result.atlSugMembers = new[]
      {
        "George \"Sitecore George\" Chang",
        "Martin \"Sitecore Artist\" English",
        "Anastasiya \"JavaScript Ninja\" Flynn",
        "Varun \"Too Many Questions\" Nehra",
        "Craig \"From ATL\" Taylor",
        "Amy \"Sitecore Amy\" Winburn"
      };
    }

    private static bool ShouldAddAtlSugMembers(ExtensibleRenderedJsonRendering extensibleRendering)
    {
      var rendering = extensibleRendering.Rendering?.RenderingItem?.InnerItem;
      if (rendering == null) return false;

      var addAtlSugMembersField = (CheckboxField)rendering.Fields[AddAtlSugMembersFieldId];
      return addAtlSugMembersField?.Checked ?? false;
    }
  }
}

Change the AddAtlSugMembersFieldId constant if you've been following along step by step.

Register in the <transformPlaceholderElement /> pipeline:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <group groupName="layoutService">
        <pipelines>
          <transformPlaceholderElement>
            <processor type="CoreySmith.Feature.AtlSug.Pipelines.LayoutService.TransformPlaceholderElement.AddAtlSugMembers, CoreySmith.Feature.AtlSug">
              <allowedConfigurations hint="list">
                <config desc="jss">jss</config>
              </allowedConfigurations>
            </processor>
          </transformPlaceholderElement>
        </pipelines>
      </group>
    </pipelines>
  </sitecore>
</configuration>

In this case I only want this processor to apply to the jss Layout Service configuration, so I added that to the <allowedConfigurations /> node. Which Layout Service configurations your processors apply to are totally up to you (generally this will always be at least jss).

All done! Going forward further customizations are possible just by adding a custom processor to <transfromPlaceholderElement />.

What About Rendering Content Resolvers?

In the Layout Service, you can write your own Rendering Content Resolver (RCR) to do exactly the kind of customization I've described above, with one caveat: any data you return through an RCR is going to be output in the fields property of your rendering. If you want to add your data to the root of your renderings, or modify something in the root of your rendering (e.g., componentName), an RCR won't cut it.

Additionally, RCRs don't lend themselves to composition (although you could easily write one to address this). For every customization you want to add to your renderings, you have to write a new RCR. Consider the scenario above where you add a custom Add ATLSUG Members checkbox field to all JSON Renderings. Out of the box, JSS ships with five RCRs:

  • Context Item Children Resolver
  • Context Item Resolver
  • Datasource Item Children Resolver
  • Datasource Resolver
  • Folder Filter Resolver

Every rendering in your application can use a different RCR. If you want the Add ATLSUG Members field to apply to any rendering, you'll have to:

  1. Create a custom implementation of RenderingContentsResolver, e.g., AtlSugMembersRenderingContentsResolver.
  2. Update the Type field of all five of the RCRs listed above with your new type, plus any other custom RCRs in your solution.
  3. Add all five of the out-of-the-box RCRs listed above into source control.

Now what if you want to add another customization, like Add Queen City SUG Members? You'll have to extend AddAtlSugMembersRenderingContentsResolver and repeat the process above.

An alternative might be to create a PipelineRenderingContentsResolver that executes a custom pipeline where you could patch in processors as you wish, but you've still got the problem that you'll have to make sure all of your RCR items use that type.

Make no mistake, RCRs are excellent and should always be your first approach when customizing renderings in the Layout Service. But if you want complete customization of your renderings, the PipelinePlaceholderTransformer described in this post is your best bet.

Conclusion

Hopefully now you feel comfortable customizing route data from the Sitecore Layout Service. This won't help you in disconnected mode though, so check out my post on extending rendering data in disconnected mode.

Although it probably felt like a mountain of code to read through, once you get the initial plumbing in place, further customizations of the Layout Service is easy. Stuff the PipelinePlaceholderTransformer and its supporting code in your Foundation layer and forget about it.

Big kudos to the JavaScript Services team (Kam Figy, Alex Shyba, Adam Weber, and Nick Wesselman) for making the Layout Service/JavaScript Services so extensible and for all of their help.

Sitecore JavaScript Services Team

Thanks to mon ami Jean-François L'Heureux for reviewing the post!