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 disconnected and connected mode.

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": "available-in-connected-mode",
            "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": "available-in-connected-mode",
  "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 extend this rendering data however you like.

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 both the disconnected Layout Service and Sitecore's Layout Service to add ATLSUG members to components with a specific name (e.g., ContentBlock).

First we'll tackle how to do it in disconnected mode, and then move on to connected mode.

Disconnected Mode

Most JSS apps are going to begin their life in disconnected mode, which serves up content from yml (or json) files through a Node.js proxy server that runs a mock Layout Service. The customization steps below should work for the Angular, React, and Vue sample apps, though I've only tested for the React app.

Customize Disconnected Server

In the JSS sample apps, the disconnected mode server is created in scripts\disconnected-mode-proxy.js with the createDefaultDisconnectedServer method from the sitecore-jss-dev-tools package. This method accepts an options parameter that lets you extend a bit of the disconnected server's functionality: afterMiddlewareRegistered, onError, and customizeRoute, among others. The one we're interested in is customizeRenderings.

Open up scripts\disconnected-mode-proxy.js and add this to the top under the const config = require('../package.json').config line:

const { addAtlSugMembers } = require('./layout-service/add-atlsug-members');

This is a method we'll implement in a moment that will add the list of ATLSUG members to specific renderings.

Add this to the proxyOptions object:

customizeRendering: (transformedRendering, rawRendering) => {
  let customizedRendering = addAtlSugMembers(transformedRendering, rawRendering);
  return customizedRendering;
}

When done, disconnected-mode-proxy.js should look like this: https://github.com/coreyasmith/jss-extensible-json-renderings/blob/master/src/Project/JssRocks/app/scripts/disconnected-mode-proxy.js.

Add ATLSUG Members to Renderings

Create a file called scripts\layout-service\add-atlsug-members.js like this:

const 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'
];

const applicableRenderings = ["ContentBlock"];

const addAtlSugMembers = (transformedRendering, rawRendering) => {
  if (!applicableRenderings.includes(rawRendering.renderingName))
    return undefined;

  return {
    ...transformedRendering,
    atlSugMembers: [...atlSugMembers]
  };
};

exports.addAtlSugMembers = addAtlSugMembers;

This exports a method that adds an atlSugMembers property to any rendering that is in the applicableRenderings list with ATLSUG members.

Display ATLSUG Members

Now you can start consuming ATLSUG members in your ContentBlock rendering like this:

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

    <RichText className="contentDescription" field={fields.content} />

    {atlSugMembers && (
      <ul>
        {atlSugMembers.map(member => (
          <li>{member}</li>
        ))}
      </ul>
    )}
  </React.Fragment>
);

atlSugMembers will be a property available on the rendering prop thanks to the customizations made to the disconnected server above.

Connected Mode

For Connected Mode, we'll add an Add ATLSUG Members checkbox field to JSON Renderings that will drive which renderings have the list of ATLSUG members included.

Add ATLSUG Members Checkbox to Renderings

Navigate to /sitecore/templates/Foundation/JavaScript Services/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.LayoutService and Sitecore.JavaScriptServices.ViewEngine NuGet packages.

using System;
using CoreySmith.Foundation.LayoutService.ItemRendering;
using CoreySmith.Foundation.LayoutService.Pipelines.LayoutService.TransformPlaceholderElement;
using Sitecore.Abstractions;
using Sitecore.JavaScriptServices.ViewEngine.LayoutService.Serialization;
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.Serialization;

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 Sitecore Rendering item from which the model was created. Below is a decorator for the JSON rendering model that has a reference to both the Layout Service Configuration Name and Sitecore Rendering item 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
  {
    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[Templates.JsonRendering.Fields.AddAtlSugMembers];
      return addAtlSugMembersField?.Checked ?? false;
    }
  }
}

Change the AddAtlSugMembersFieldId constant if you've been following along step by step to match your Add ATLSUG Members field ID.

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 both the disconnected Layout Service and Sitecore's Layout Service. For the disconnected Layout Service, further rendering customizations are as easy as plugging a function into customizeRendering in disconnected-mode-proxy.js. For Sitecore's Layout Service, further rendering customizations are as easy as a pipeline processor with the plumbing in place from above. Although it may seem like a mountain of code, put 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 JSS so flexible and for all of their help.

Sitecore JavaScript Services Team and me

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