Sitecore Experience Edge, GraphQL and publishing pages utilising data sources

We've been working on a large headless solution recently that utilises Sitecore Experience Edge for XM. We are working in a micro-front ends architecture, with some parts of the site leveraging Sitecore JavaScript Rendering SDK (previously known as JSS) for delivering pages whilst supporting full inline editing and page designing. Other parts of the site use GraphQL queries via backend-for-frontend (BFF) style APIs to pull back content. Both these approaches leverage Experience Edge for serving GraphQL however the nuances of how they are implemented need to be understood because we have found they lead to some different behaviours that can impact the way content editors need to use the system.

The point of rendering

One of the most important things to understand about Experience Edge before you start using it is that for performance reasons it does a lot of work at the point of publishing, that used to happen at the point a page was rendered when using Content Delivery instances. The resulting restrictions of this are well summed up in the documentation around Experience Edge limitations and restrictions:

  • The Experience Edge for XM Connector publishes a static snapshot of the Layout Service output for an item at the time of publishing. This means that:
    • ...
    • If you make a change to a rendering configuration or Layout Service extension that affects the output of the Layout Service, you must republish the affected items.

One key upshot of this is that you need to be careful with things like having primary navigation components embedded into your page content rendered through JSS. As each page layout and content is rendered at publish time, to change an item like navigation that uses a data source you need to publish every page that relies on the changed item -- and for something like primary navigation that means republishing your whole site! This means that you need a different way to render parts of your page such as navigation at "render time" (whether that be client side rendering, server side rendering or static/incremental site generation).

This is achievable through Experience Edge GraphQL queries that don't leverage pre-rendered pages

Exprience Edge GraphQL queries

The Sitecore Documentation has a useful reference page with GraphQL query examples, however it's not always fully apparent what the differernces between how they work under the hood are. An overview of the query types can be seen in the documentation on the GraphQL schema

Below I outline some of the key query keywords, Experience Edge builds on top of standard GraphQL query syntax and adds some additional options. 

Layout query

The layout query is what the traditional JSS SDK approach utilises to render pages, with the "rendered" keyword getting the pre-generated page content JSON that was calculated at publish time for your page/route. It is when using this pre-rendered content that the restrictions called out above about republishing "affected items" really bite. If you have edited a data source item used anywhere on your page that is being exposed through this type of query then you need to republish any pages that reference the data source as well as the item itself

query {
  layout(site: "experienceedge", routePath: "/", 
  			language: "en") {
    item {
Example layout query syntax

Item query

The Item query queries individual Sitecore items and returns a projection of the item fields. Unlike the layout example above, if you are leveraging an Item query to target an item from a particular page in our front end app, you don't need to have re-published the page itself. My mental model for this is that on publish, a "record" is created for the item data itself, as well as a separate record for the page where the rendering pipeline has been executed to assemble the page items based on the presentation details.

The downside of this approach is that it doesn't leverage the publish time rendering optimisations and doesn't align as closely with JSS SDK patterns. There isn't any particular reason however you couldn't use this alongside server side rendering or static site generation to get a workable outcome.

query {
  item(path: "/sitecore/content/Experience-Edge/home", 
  			language: "en") {
    ... on AppRoute {
      pageTitle {
    field(name: "pageTitle") {
      ... on TextField {
Example item query syntax

Search query

The search query allows you to build search queries to project data across multiple different items in one return set. Like the Item query this allows you to return data based on items that aren't tied to what has been rendered for a page at publication time.

query {
    where: {
      AND: [
          name: "_templates"
          value: "0929f436c3f3500a9f8bd1c57a67a192"
          operator: CONTAINS
          name: "_path"
          value: "7ab00eca411249818420666fc9110faf"
          operator: CONTAINS
    first: 5
    orderBy: { name: "pageTitle", direction: ASC }
  ) {
    results {
    pageInfo {
Example search query syntax

The complication

But what about data sources for modules on your page -- such as a content tile or carousel? Normally you want to have these as a rendering with it's own data source to allow content authors to easily feature content items from elsewhere on the site on home or landing pages. Currently with Experience Edge if the content editor changes a "shared" data source item that is used across the site, this will cause you a problem. This really came to light in our case when our testers would change the data source item for a module on the homepage of our new site (which uses JSS) and publish it, only for it not to appear. However when we did something similar on our Product Description Page (PDP), which leverages a BFF call which retrieves Sitecore content via an "Item" GraphQL query - publishing the data source was all that was required

Needing to have content authors remember to publish not just a data item, but the page that references it is a bit of a pain. It is even worse when that data source item is shared and referenced by potentially multiple pages in this site. What makes it even worse is that when we get to our BAU state we don't want content authors manually publishing anything at all (for performance reasons) -- we want to have a regular smart publish job that collects all the items that have been changed and publishes them. If we have only changed our data source items, then smart publish will never update the pages and the content will never show on the page. 

Related item publishing

Our first thought to try and work around this was to leverage the "related items" options when performing publishing. Surely, we reasoned, there is a relationship between the page and the datasource item in the links database and this would come up as a related item when calculating the graph of items for publishing purposes. Alas it turns out that the related items publish behaviour isn't designed to work this way out of the box - it is aimed more at publishing a page and picking up changes in related sub-items, rather than publishing a sub-item and finding the pages that use it.

Related items publishing by default only looks for the following

  • Clones – items that are clones of the selected item.
  • Media items – media items that are referenced by the selected item.
  • Content items - data sources that are referenced by the selected item.
  • Design items - renderings, layouts and templates used by the selected item.
  • Aliases – items that are aliases of the selected item.

Our approach

Fortunately, as is usually the case in Sitecore, there is a way to extend the platform to have it do what you need to achieve! Speaking with Sitecore Support they recommended that we create a custom Sitecore.Publishing.Pipelines.PublishItem.AddItemReferences processor linked into the <getItemReferences> pipeline. Our plan is to have this query the links database to find any items that are linking to the data source item being changes and make sure they are added into the publishing queue.


Popular posts from this blog

Cloud hosting Sitecore - Disaster Recovery

Cloud hosting Sitecore - High Availability