Extending the Layout Service Context

Out of the box, the Layout Service returns the Context and Route objects (see my earlier post for more info on the Layout Service.) On my current JSS React project we wanted to add various metadata fields to the route and render them in the HEAD tag via Helmet, which was fine until we hit the need to add a URL for the OpenGraph og:url metadata tag.

The requirement was:

if no value is supplied in the Canonical URL field or the OpenGraph URL metadata field in the Sitecore route item, then show the current URL of the route.

In server-side MVC land we would do this all the time, and in Sitecore JSS you’d think we could use Window.location (because JavaScript). But (and there’s always a “but”) this project uses React with Server Side Rendering (SSR), so the Window.location object isn’t available when the app is first rendered and delivered to the browser by Node.js. Now maybe there’s a clever way to handle this with React, Node, and a handful of magic beans, but that’s not something I am particularly comfortable with, so I chose to do it in .NET on the server by modifying the data returned by the Layout Service. And aside from that, this approach can be used to populate the context with other data that isn’t available client-side.

Where does this bit go?

Given that the Layout Service returns something much like this:

Then where is the right place to put the URL in the layout service JSON in order to populate the og:url metadata?

The first option I considered was to add it to the Route, but this appeared to entail overriding the Sitecore.LayoutService.ItemRendering.Render method and extending the RenderedItem class so that it returned a URL property, but the amount of change required was extensive, or so it seemed to me anyway.

Option 2 was to add it to the Context which was much simpler and used some typical pipeline processor code that can be patched in. Looking at the pipeline config for GetLayoutServiceContext:

it was apparent that it would be simple to add another pipeline processor that would add the data to the Context. Using LinkManager to calculate the URL for the context item, and patching the processor into the Layout Service pipeline required minimal code:

ItemContext.cs

using Sitecore.Diagnostics;
using Sitecore.LayoutService.ItemRendering.Pipelines.GetLayoutServiceContext;
using System.Collections.Generic;
using Sitecore;
using Sitecore.Links;

namespace JSSDemo.Feature.Metadata.PipelineProcessors
{
    public class ItemContext : IGetLayoutServiceContextProcessor
    {
        public const string Key = "itemUrl";

        public void Process(GetLayoutServiceContextArgs args)
        {
            if (Context.Item != null)
            {
                Assert.ArgumentNotNull((object)args, nameof(args));
                IDictionary<string, object> contextData = args.ContextData;
                var options = LinkManager.GetDefaultUrlBuilderOptions();

                options.AlwaysIncludeServerUrl = true;
                string link = LinkManager.GetItemUrl(Context.Item, options);

                args.ContextData.Add(Key, link);
            }
        }
    }
}

Layout Service Pipeline Patch

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <group groupName="layoutService">
        <pipelines>
          <getLayoutServiceContext>
			  <processor type="JSSDemo.Feature.Metadata.PipelineProcessors.ItemContext, JSSDemo.Feature.Metadata" />	
		  </getLayoutServiceContext>
		</pipelines>
      </group>
    </pipelines>
  </sitecore>
</configuration>

Which resulted in the expected JSON output with an “itemUrl” property containing the URL of the context item:

And the Sitecore config was patched to be:

Another approach might be to use a rendering with a datasource, but that would not be a great editor experience.

You could argue that the context item URL doesn’t belong in the Context and ideally the route is the best place for this data, but given the changes needed to change the rendering of the route, this seemed the best option. I’d be interested to hear of a low-touch method to amend the route properties.