A magnifying glass over Sitecore's yellow serialisation banner.

Introduction

In this post, I’ll walk you through how to recreate the Unicorn yellow banner in Sitecore using Content Serialisation (SCS) and some custom C#.

Background: Migrating to SCS from Unicorn

After weighing the pros and cons of moving from Unicorn to Sitecore Content Serialisation (SCS), there was one particular piece of functionality I was sad to see missing: the absence in SCS of Unicorn’s yellow serialisation banner! So I decided to do something about it.

Unicorn is an open source project that (in their words) is a “Sitecore utility designed to simplify deployment of Sitecore items across environments automatically”. And it worked pretty well for us since we adopted Sitecore as our in-house CMS a number of years ago, but over time – especially after adding more and more serialised items – we noticed a real slowdown in performance. Concerns around support and updates for possible future vulnerabilities, though, were what drove the decision to switch to Sitecore’s own attempt at serialisation: SCS.

The Unicorn Yellow Banner in Sitecore

Unicorn's yellow serialisation banner that is shown in Sitecore's Content Editor for serialised items.
Unicorn’s yellow serialisation banner, as seen inside Sitecore.

To understand why Sitecore doesn’t implement a yellow banner solution out-of-the-box, it’s important to understand how Unicorn and SCS both work.

Unicorn is commonly installed into a Sitecore solution via its Nuget package. It integrates into the Sitecore application, meaning binaries and configuration files are published along with your other project code. At runtime, it loads along with the whole application so that it’s able to hook into various pipelines and workflows. We can see in the screenshot below Unicorn’s pipeline processor for the yellow serialisation warning banner is added:

Unicorn's Unicorn.UI.config configuration file, found in App_Config\Include\Unicorn.
Unicorn’s Unicorn.UI.config configuration file, found in App_Config\Include\Unicorn.

It’s that tight integration that allows Unicorn to hook into pipelines, examine configs, and reference YML files directly on your filesystem. But all that comes at a cost: there’s a small performance hit associated with this otherwise useful functionality.

Why is the Yellow Banner Missing in SCS?

Sitecore’s architecture is completely different to the solution Unicorn provided to serialisation. Installing Sitecore Management Services (SMS) enables you to use the Sitecore Command Line Interface (CLI) to interact with a given instance. In that sense, it’s completely disconnected from the CMS itself; it merely makes API calls via a GraphQL-enabled API, as seen in the image below.

Diagram showing how Sitecore Content Serialisation communicates with Sitecore via the Management Services (GraphQL) API.

The interface between the two products most likely explains why Sitecore omitted any kind of yellow serialisation warning banner, like that seen in Unicorn. The CMS instance effectively has no idea whether an item kept in its database is serialised or not.

I’d read about a number of workarounds that try and solve the problem of persisting a serialisation state within Sitecore, such as maintaining hidden fields, but that would mean accepting a burden of maintaining integrity of the field if items moved in or out of serialisation scope. I liked what we had before: a friendly message that everyone was already familiar with. So I implemented one myself!

Sitecore Serialisation Warning

I managed to re-create the Unicorn yellow banner in Sitecore by adding my own pipeline processor:

A custom yellow banner for serialised items in Sitecore's Content Editor.
A custom yellow banner for serialised items in Sitecore’s Content Editor.

Here’s some demo code that you can copy straight into your Sitecore project:

using Foundation.SerializationWarnings.Services;
using Sitecore.Pipelines.GetContentEditorWarnings;
using static Sitecore.Pipelines.GetContentEditorWarnings.GetContentEditorWarningsArgs;

namespace Foundation.SerializationWarnings.Pipelines
{
    public class SerializedItemWarningProcessor
    {
        public void Process(GetContentEditorWarningsArgs args)
        {
            if (args?.Item == null)
                return;

            SerializedItemChecker sic = new SerializedItemChecker();
            var (isSerialized, moduleFile) = sic.IsItemSerialized(args.Item);
            if (isSerialized)
            {
                args.Warnings.Add(new ContentEditorWarning
                {
                    Title = "SCS Item Notice",
                    Text = $"This item is tracked in <b>{moduleFile}</b>. Ensure changes are committed to source control."
                });
            }
        }
    }
}

And don’t forget to create a configuration patch file to hook up the processor in the Sitecore pipelines:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <getContentEditorWarnings>
        <processor type="Foundation.SerializationWarnings.Pipelines.SerializedItemWarningProcessor, Foundation.SerializationWarnings" />
      </getContentEditorWarnings>
    </pipelines>
  </sitecore>
</configuration>

You’ll see in the code above that I’ve added a simple path check for /sitecore/content/mytenant/mysite. When you navigate to it, you’ll see a yellow banner proudly displayed on the page – exactly where Unicorn’s used to sit:

Sitecore's Content Editor with a path open that a module.json file denotes is serialised. A yellow banner reflects that status and displays a message to the user.

Detecting Serialisation & Scope

The service class below loads all module.json files from the ~/App_Data/SerializationJsons folder in the website. It also implements the SCS-specific scopes for includes contained in the module.json files – namely Ignore, SingleItem, ItemAndChildren, and ItemAndDescendants and determines whether paths are serialised based on a number of defined rules:

using Newtonsoft.Json.Linq;
using Sitecore.Data.Items;

namespace Foundation.SerializationWarnings.Services
{
    public enum SerializationScope
    {
        Ignored,
        SingleItem,
        ItemAndChildren,
        ItemAndDescendants
    }

    public class SerializedItemChecker
    {
        private readonly Dictionary<string, (SerializationScope Scope, string ModuleFile)> _paths = new();
        private bool _isLoaded;

        public SerializedItemChecker()
        {
            if (!_isLoaded) { LoadPaths("~/App_Data/SerializationJsons"); }
        }

        public void LoadPaths(string rootDirectory)
        {
            var resolvedPath = System.Web.Hosting.HostingEnvironment.MapPath(rootDirectory);
            if (_isLoaded || string.IsNullOrEmpty(resolvedPath) || !Directory.Exists(resolvedPath))
                return;

            foreach (var file in Directory.GetFiles(resolvedPath, "*.module.json", SearchOption.AllDirectories))
            {
                var json = JObject.Parse(File.ReadAllText(file));

                var items = json["items"]?["includes"] as JArray;
                if (items == null) continue;

                foreach (var item in items)
                {
                    var path = item.Value<string>("path")?.TrimEnd('/');
                    if (string.IsNullOrWhiteSpace(path)) continue;

                    var scopeText = item.Value<string>("scope")?.ToLowerInvariant();

                    var serializationScope = scopeText switch
                    {
                        "singleitem" => SerializationScope.SingleItem,
                        "itemandchildren" => SerializationScope.ItemAndChildren,
                        "itemanddescendants" => SerializationScope.ItemAndDescendants,
                        _ => SerializationScope.ItemAndDescendants // default if unspecified
                    };

                    _paths[path] = (serializationScope, Path.GetFileName(file));
                }
            }

            _isLoaded = true;
        }

        public (bool isSerialized, string moduleFile) IsItemSerialized(Item item)
        {
            var itemPath = item.Paths.FullPath.TrimEnd('/');

            foreach (var kvp in _paths)
            {
                var path = kvp.Key;
                var (scope, moduleFile) = kvp.Value;

                switch (scope)
                {
                    case SerializationScope.SingleItem:
                        if (itemPath.Equals(path, StringComparison.OrdinalIgnoreCase))
                            return (true, moduleFile);
                        break;

                    case SerializationScope.ItemAndChildren:
                        if (itemPath.StartsWith(path + "/", StringComparison.OrdinalIgnoreCase) &&
                            !itemPath.Substring(path.Length + 1).Contains("/"))
                            return (true, moduleFile);
                        break;

                    case SerializationScope.ItemAndDescendants:
                        if (itemPath.Equals(path, StringComparison.OrdinalIgnoreCase) ||
                            itemPath.StartsWith(path + "/", StringComparison.OrdinalIgnoreCase))
                            return (true, moduleFile);
                        break;
                }
            }

            return (false, null);
        }
    }
}

Read more about item scopes in Sitecore’s SCS Configuration Reference.

This method is simple enough that it means there is no direct reading or writing to the filesystem like Unicorn did (aside from the initial load of json files into memory), so you’ll see very little performance impact using this method.

Limitations

There are a couple of caveats worth calling out with this approach:

  • The demo code doesn’t detect changes to module.json files in real time. If you add a file after the initial load in the application, it won’t be processed. This can be overcome by implementing some code to either monitor changes in that directory, or forcing reloads more frequently (e.g., per session).
  • If items are added to a serialised path in a production environment, they will incorrectly show the yellow banner as being serialised when they aren’t. The Unicorn yellow banner in Sitecore got around this by actively monitoring the filesystem’s yml files.

With some careful consideration and addition to the code to suit your desired scenario, these can be overcome.

Final Thoughts

That’s how you can recreate the Unicorn yellow banner in Sitecore with just a few simple steps! And what’s great about it is that you can customise it to suit your needs – the banner message even accepts HTML, so you have full control over how it’s displayed.

The full code implementation is available on my scs-yellow-banner repo on Github. There’s plenty more to add, so check it out and send over your ideas!

Leave a Reply

Your email address will not be published. Required fields are marked *