Breaking Free: Overcoming Presentation Lock-in Content Management Systems

Breaking free from CMS presentation lock-in boosts content agility and reusability. This post explores the challenges of extracting content from Sitecore's APIs for omnichannel strategies.

Breaking Free: Overcoming Presentation Lock-in Content Management Systems
Photo by Alban Martel / Unsplash

Content Management Systems (CMS) can be great repositories of your content. However, they can become web publishing tools where content becomes trapped in your presentation. When content needs to be distributed for something other than just a web page, getting data out of it becomes complex.

Take, for example, the page below. It has the following components on it and each one has been dragged and dropped onto a page editor and maybe assigned a data source:

  • Logo in the Header
  • Page Header
  • Rich Text
Mock Page

Separating content from presentation is not something that Sitecore does not lack in providing. The separation between data templates (schema) and layout (presentation) makes it possible to separate content from display.

Content Editor

However, Rich Text components on a page in Sitecore might do more harm than good. In the content tree, the Body item is bound only with the Rich Text component that makes the page content. Only the presentation details on the Page Example item can tell you what makes that page.

When Sitecore Headless came out, it came with endpoints to get at content that enabled content distribution beyond a web page. The Layout Service and Graph QL endpoints were introduced to enable the building of JAM Stack solutions. While these API endpoints provide access to the content, problems began to surface when its consumption for other distributions realized that the response was dirty. "Dirty" may be a strong word for it. But let's look at some examples.

Layout Service

This endpoint combines content and presentation in the response when pages are requested. The Layout Service is a JSON object of a page on the site with every component and then the content that fills in that component for that page. This is the heart of the JAM stack solution. Generating pages... For example:

"sitecore": {
  "context": {
    "pageEditing": "false",
    "site": {
      "name": "Site Name"
    },
    "pageState": "normal",
    "language": "en"
    ...
  },
  "route": {
    "fields": {
      "PageTitle": {
        "value": "Page Title Example"
      },
      "Image": {
        "src": "/-/media/site/page.jpg&rev=23eabea139834c6a8feae01d336727ba&hash=afaf6e21384d4266a7fc9d8f9d6d702b",
        "alt": "Page Image",
        "width": "1024",
        "height": "640"
      }
    }
    ...
    "placeholders": {
      "jss-header": [
        "uid": "f08a3c36-da04-42e1-9161-6ac8fc116c44",
        "componentName": "Logo",
        "dataSource": "{510b8dda-6005-4b68-b2aa-54b761d2745a}",
        "fields": {
          "Logo": {
            "value": {
              "src": "/-/media/site/logo.png?h=58&iar=0&w=85&rev=23eabea139834c6a8feae01d336727ba&hash=afaf6e21384d4266a7fc9d8f9d6d702b",
              "alt": "Logo"
            }
          },
          "MobileLogo": {
            "value": {
              "src": "/-/media/site/mobile-logo.png?h=454&iar=0&w=505&rev=23eabea139834c6a8feae01d336727ba&hash=afaf6e21384d4266a7fc9d8f9d6d702b",
              "alt": "Mobile Logo"
            }
          }
        }
      ],
      "jss-main": [
        {
          "uid": "0ad0118f-675e-4577-8a67-2410ba334361",
          "componentName": "Page Header",
          "dataSource": "",
          "params": {},
          "fields": {
            "PageTitle": {
              "value": "Page Title Example"
            }           
          }
        },
        {
          "uid": "291f38cb-b985-4035-9743-a85011905e1d",
          "componentName": "RichText",
          "dataSource": "{d0f0e357-5b7d-4510-86cc-e9b69c42acbc}",
          "params": {},
          "fields": {
            "Text": {
              "value": "<p><span class=\"heading3\">Page Subheading Example</span></p><p><span style=\"line-height: 32px; font-weight: 500; font-size: 16px;\">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</span></p>"
            }
          }
        }
      ],
      "jss-footer": []
    }
  }
}

If you were building a Mobile App, you would probably not use the Layout Service to retrieve the data this way. The response just seems heavy and a lot to parse through. It would make sense to go straight to Graph QL. Let's take a look at Graph QL and see how it stacks up.

Graph QL

This endpoint would be expected to the the right fit for distributing content out of the CMS. Why wouldn't it be the right tool for consumption by other mediums? Yet, we still have a problem.

Here is a query for Page Example:

query GetPage($contextItem: String!, $language: String!) {
  item(path: $contextItem, language: $language) {
    ... on Page {
      pageTitle {
        value
      }
      pageImage {
        src
        alt
      }
    }
  }
}

At first, the query is fairly straightforward until we get to the page content. But how do we construct our query to retrieve content?

There are only two options. First, assume the child items in the tree hierarchy determine the order in which rich text is consumed. In this example, our content is the Body item in the content tree under Page Example.

Page Example Content Tree

The query becomes as follows:

query GetPage($contextItem: String!, $language: String!) {
  item(path: $contextItem, language: $language) {
    ... on Page {
      pageTitle {
        value
      }
      pageImage {
        src
        alt
      }
      children {
        results {
          ... on Data {
            children {
              results {
                ... on RichText {
                  text {
                    value
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

But we know this is not sustainable. How do you enforce such a thing? The editor really shouldn't have to think about where the content goes so it can be consumed properly on Graph QL.

The second option is to get the page presentation. This is done by adding rendered as a property on the query to return this data.

query GetPage($contextItem: String!, $language: String!) {
  item(path: $contextItem, language: $language) {
    ... on Page {
      pageTitle {
        value
      }
      pageImage {
        src
        alt
      }
      rendered
    }
  }
}

Below is an example of a Graph QL response:

"pageTitle": {
    "value": "Page Title Example"
},
"pageImage": {
    "src": "/-/media/site/page.jpg&rev=23eabea139834c6a8feae01d336727ba&hash=afaf6e21384d4266a7fc9d8f9d6d702b",
    "alt": "Page Image"
},
"rendered": {
  "sitecore": {
    "context": {
      "pageEditing": "false",
      "site": {
        "name": "Site Name"
      },
      "pageState": "normal",
      "language": "en"
      ...
    },
    "route": {
      "fields": {
        "PageTitle": {
          "value": "Page Title Example"
        },
        "Image": {
          "src": "/-/media/site/page.jpg&rev=23eabea139834c6a8feae01d336727ba&hash=afaf6e21384d4266a7fc9d8f9d6d702b",
          "alt": "Page Image",
          "width": "1024",
          "height": "640"
        }
      }
      ...
      "placeholders": {
        "jss-header": [
          "uid": "f08a3c36-da04-42e1-9161-6ac8fc116c44",
          "componentName": "Logo",
          "dataSource": "{510b8dda-6005-4b68-b2aa-54b761d2745a}",
          "fields": {
            "Logo": {
              "value": {
                "src": "/-/media/site/logo.png?h=58&iar=0&w=85&rev=23eabea139834c6a8feae01d336727ba&hash=afaf6e21384d4266a7fc9d8f9d6d702b",
                "alt": "Logo"
              }
            },
            "MobileLogo": {
              "value": {
                "src": "/-/media/site/mobile-logo.png?h=454&iar=0&w=505&rev=23eabea139834c6a8feae01d336727ba&hash=afaf6e21384d4266a7fc9d8f9d6d702b",
                "alt": "Mobile Logo"
              }
            }
          }
        ],
        "jss-main": [
          {
            "uid": "0ad0118f-675e-4577-8a67-2410ba334361",
            "componentName": "Page Header",
            "dataSource": "",
            "params": {},
            "fields": {
              "PageTitle": {
                "value": "Page Title Example"
              }           
            }
          },
          {
            "uid": "291f38cb-b985-4035-9743-a85011905e1d",
            "componentName": "RichText",
            "dataSource": "{d0f0e357-5b7d-4510-86cc-e9b69c42acbc}",
            "params": {},
            "fields": {
              "Text": {
                "value": "<p><span class=\"heading3\">Page Subheading Example</span></p><p><span style=\"line-height: 32px; font-weight: 500; font-size: 16px;\">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</span></p>"
              }
            }
          }
        ],
        "jss-footer": []
      }
    }
  }
}

Calling rendered will stuff the Layout Service response in the Graph QL response. This does not get us any further in getting our content out of the CMS.

Summary

The problem statement is the following. It is easy to fall into the trap of data source-driven pages tightly coupled to a page's presentation definition. Sitecore Experience Accelerator (SXA) is built around this. The example presented could be easily remedied by moving the Body item as a field on the page item itself. But it is still Rich Text, which can present some more problems we didn't consider. I will go deeper into Rich Text in another post.

The next problem is we know that content models are not this simple. Once the site gets into Accordions, Tabs, or, dare I say, Carousels, we can only imagine the complexity of piecing all that content on a page.

The ideal solution here is to take time to build out a content model that fits the organization that is consuming it. Structured Content is really what is at the heart of this. We will dig into this with later posts. If the organization is good with a web publishing tool and you are a Sitecore customer, then SXA can fit right in. But, if the organization wants to be omnichannel, involve the right people to build a content model that embraces the portability of its content. A content model can be built correctly in Sitecore.

Resources

  1. Query examples with Sitecore Headless https://doc.sitecore.com/xp/en/developers/hd/22/sitecore-headless-development/query-examples-for-the-delivery-api.html