I've been working with disconnected mode while working on a new project lately and I wanted to experiment with adding the global navigation and other settings to the context object that gets returned from the layout service request.

If you take a look at scripts/disconnected-mode-proxy.js in your Sitecore JSS project, you'll see that a proxyOptions object gets built with some defaults and then a call to createDefaultDisconnectedServer gets made with these proxy options. This call is responsible for creating an instance of the disconnected layout service.

The proxyOptions object is of type DisconnectedServerOptions if you take a look at the Typescript definitions here. There are a lot of neat options available here, several of which allow us to customize functionality of the disconnected layout service: customizeContext, customizeRoute, customizeRendering.

We're going to take a look at customizeContext in this post, this post was partially inspired by the official Sitecore JSS documentation which demonstrates how to add static renderings to context in connected mode, which I'll explore more at a later time when I switch to connected mode.

So where do we start? Well, I want to define all of my global settings on a content item. It's not a route so I don't think it makes sense to add it there ... if I'm wrong, let me know! So let's start with the template definition:

Note: I've been debating this some in my head since I wrote this statement. I still think this post outlines a valid use case but I think the argument for having it be a route is that you can manage the navigation in Experience Editor and then if you inject the entire route object into context instead in disconnected mode, you can use the RenderStaticItemProcessor to add it to context during integration in connected mode. Will revisit this thought later. If you're looking for personalization in your header or other global components powered by a method like this, I would look into adding routes instead of content items.

sitecore/definitions/SiteSettings-Template.sitecore.js

// eslint-disable-next-line no-unused-vars
import { CommonFieldTypes } from '@sitecore-jss/sitecore-jss-manifest';

export default function (manifest) {
  manifest.addTemplate({
    name: 'SiteSettings-Template',
    fields: [
      { name: 'site_title', type: CommonFieldTypes.SingleLineText, displayName: 'Site Title' },
      { name: 'site_description', type: CommonFieldTypes.SingleLineText, displayName: 'Site Description' },
      { name: 'twitter_url', type: CommonFieldTypes.SingleLineText, displayName: 'Twitter Url' },
      { name: 'navigation_links', type: CommonFieldTypes.ContentList, displayName: 'Navigation Links' }
    ]
  });
}

So typical stuff, right? A title, a description and some navigation links. After that we can mock out an instance of this item in our data directory for the disconnected layout service:

data/SiteSettings/en.yml

id: site-settings
template: SiteSettings-Template
fields:
  site_title: Adam Lamarre
  site_description: Sitecore, development, etc.
  twitter_url: https://twitter.com/erzr
  navigation_links:
    - id: navlink-home
    - id: navlink-sitecore

Now that we have our template and item defined, we can add code to inject this into our context data. I wrote a function that can be generic enough to allow multiple items to be injected into context using customizeContext, so let's start with that function. Add this to your proxyOptions:

  customizeContext: (context, route, currentManifest, request, response) => {
    // Retrieves the top level content node for nonRoutes
    const contentNode = retrieveContentNode(currentManifest);

    // If it does not exist, something is wrong.
    if (!contentNode) {
      console.error('Unable to locate content node, check manifest.');
    }

    // Find all of the items we want to inject into our context
    const additionalContext = {
      site_settings: findContentItemById(contentNode, 'site-settings')
    };

    // Convert all of the found items to layout service structure
    Object.keys(additionalContext).forEach(key => {
      additionalContext[key] = convertContentItemToLayoutServiceFormat(additionalContext[key])
    })

    // Return the existing context with our injected properties added
    return { ...additionalContext, ...context };
  }

There are some functions that needed to be added to support this code as well. Add these above proxyOptions:

// Retrieves the top most node in the nonRoutes array.
const retrieveContentNode = (currentManifest) => {
  // `rootItemName` from content.sitecore.js
  const contentItem = currentManifest.items.nonRoutes.find(nonRoute => nonRoute.name === 'Content');
  return contentItem;
}

This function is used to retrieve the top level node from the manifest. We're ultimately going to traverse the entire nonRoutes tree searching for content items for the nodes we're trying to add. This is where we want to start.

const findContentItemById = (currentNode, id) => {
  let foundNode;

  // check to see if the current node is a match
  const is_match = currentNode.id === id;

  // if it is as match, set found_node to the current node
  if (is_match) {
    foundNode = currentNode;
  } else if (currentNode.children) {
    // otherwise, check the children of this node if they exist
    currentNode.children.some(child => {
      foundNode = findContentItemById(child, id);
      return foundNode != null;
    });
  }

  return foundNode;
}

A little recursive function for traversing the item nodes of the nonRoutes tree structure. This function just checks if it's the item we're looking for and if it isn't, it checks the children of that item.

const convertContentItemToLayoutServiceFormat = (contentItem) => {
  let item;

  if (contentItem) {
    item = JSON.parse(JSON.stringify(contentItem));

    if (item.fields) {
      item.fields = remapFieldsArrayToFieldsObject(item.fields);
    }
  }

  return item;
}

Our last function is to rewrite our found content item to layout service format. If you retrieve the manifest version of the item, you'll find that fields is an array, not an object. So this function makes a deep copy of the content item so there are no side effects from us modifying the content item itself and then we use the Sitecore JSS function remapFieldsArrayToFieldsObject to rewrite the fields of our content item so they're formatted in a way we know and love.

You can check out what this function does here. Unfortunately, Sitecore JSS does not re-export this function in the package so we need to add our own import directly from the file, where luckily it is exported!

const { remapFieldsArrayToFieldsObject } = require('../node_modules/@sitecore-jss/sitecore-jss-dev-tools/dist/disconnected-server/layout-service');

After this is in place, you should have your site_settings object in your context and it should look something like this:

        "context": {
            "site_settings": {
                "id": "site-settings",
                "template": "SiteSettings-Template",
                "fields": {
                    "site_title": {
                        "value": "Adam Lamarre"
                    },
                    "site_description": {
                        "value": "Sitecore, development, etc."
                    },
                    "twitter_url": {
                        "value": "https://twitter.com/erzr"
                    },
                    "navigation_links": [{
                        "fields": {
                            "text": {
                                "value": "Home"
                            },
                            "url": {
                                "value": "/"
                            }
                        },
                        "id": "available-in-connected-mode"
                    }, {
                        "fields": {
                            "text": {
                                "value": "Sitecore"
                            },
                            "url": {
                                "value": "/tag/sitecore/"
                            }
                        },
                        "id": "available-in-connected-mode"
                    }]
                },
                "name": "SiteSettings"
            },
            "pageEditing": false,
            "site": {
                "name": "JssDisconnectedLayoutService"
            },
            "pageState": "normal",
            "language": "en"
        }

Pretty cool. I thought about just mocking out this JSON and just injecting it directly without sourcing it from an item, but where's the fun in that? I like that this solution will allow me to add more to my global context as I see fit.

I'm building in Svelte so my usage of this context object looks like this:

  const sitecoreContext = getSitecoreContext();
  const { site_settings } = sitecoreContext.context;
  const { fields } = site_settings;

  const navigationLinks = getFieldValue(fields, "navigation_links") || fields['navigation_links'];
  const hasNavigation = navigationLinks && navigationLinks.length;
  const twitterUrl = getFieldValue(fields, "twitter_url");
  const siteTitle = getFieldValue(fields, "site_title");

Usage will differ with other frameworks but it should be the same in theory, retrieve the Sitecore context, extract site_settings, profit.

Check out my full gist for scripts/disconnected-mode-proxy.js here.