Shopping Experiences (CMS)

Shopware comes with an extensive CMS system (referred to as "Shopping Experiences") built upon pages or layouts which can be reused and dynamically hydrated based on their assignments to categories or other entities.

In the following concept we'll take a look at the following aspects:

  • Structure of CMS pages

  • Hydration of dynamic content

  • Separation of content and presentation

We'll start by taking a rather abstract approach to content organisation and later translate that into more specific applications.

Structure

Every CMS page or layout (they're really technically the same) is a hierarchical structure made of sections, blocks, elements and additional configuration within each of those components. An exemplary CMS page printed in JSON would look similar to this:

{
cmsPage: {
sections: [{
blocks: [{
slots: [{
slot: "content",
type: "product-listing",
/* ... */
}]
}, /* ... */]
}, /* ... */]
}
}

It is a tree where the root node is a page. Each page can have multiple sections. Each section can contain multiple blocks. Each block can have zero or more slots where each slot contains exactly one element. Easy as that.

Let's go through these structural components step by step. We do this in a top-down manner, starting from the biggest element or the tree root:

Page

A page serves as a wrapper and contains all content information as well as a type which denotes whether it serves as a

  • Category / Listing page

  • Shop page

  • Static page

  • (at some point we will support CMS for Product pages as well)

Section

Defines a horizontal container segment within your page which can be either:

  • Two-Columns which we refer to as sidebar and content or

  • A single column

A section contains blocks which are usually stacked upon each other.

Block

A block represents a unit usually spanning an entire row which can provide custom layout and stylings. For UI purposes blocks are clustered into categories such as:

  • Text

  • Images

  • Commerce

  • Video

Each block can contain none up to multiple slots. A slot has a name and is just a container for one element. To be more clear, I will use an example - take the following block:

block: {
type: "text-hero",
slots: [{
type: "text",
slot: "content",
config: {
content: {
source: "static",
value: "Hello World"
}
},
}]
}

Pretty clear what this will look like - we will have a hero block, containing the text "Hello World". But having type: "text-hero" and "type": "text" in this nested structure might feel somewhat redundant you might think - of course there will be a text shown.

Let's take a step back and look at another example:

block: {
type: "text-hero",
slots: [{
type: "image",
slot: "content",
config: {
media: {
source: "static",
value: "ebc314b11cb74c2080f6f27f005e9c1d"
}
},
data: {
media: {
url: "https://my-shop-host.com/media/ab/cd/ef/image.jpg"
}
}
}]
}

Well, now we still have the text-hero block but suddenly it contains an image. That is due to the internal structure of our CMS and the generic nature of blocks. The slots defined by a block are abstract. In the examples shown above, the text-hero block only contains one slot, named content.

Elements

Elements are the "primitives" in our tree hierarchical of structural components. Elements have no knowledge of their context and usually just contain very little markup. Ultimately and most importantly, elements are rendered inside the slots of their "parent" blocks.

Types of elements comprise

  • text

  • image

  • product-listing

  • video

  • and more...

Configuration

Every component (section, block, element) contains a configuration which specifies detailed information about how it's supposed to be rendered. Such configuration can contain:

  • Product ID

  • Mapped field (e.g. category.description)

  • Static values

  • CSS config (properties, classes)

Static values will be passed to the page as-is. Mapped fields will be resolved at runtime based on the dynamic content hydration - described subsequently.

Hydration of dynamic content

Whereas the structure of a CMS page remains somewhat static, its content can be dynamic and context-aware. This way you can, for example, reuse the same layout for multiple category pages where product listing, header image and description are always loaded based on the category configuration.

Resolving process

The following diagram illustrates how that works using the example of a category:

Flow of resolving CMS page content

Let's go through the steps one by one.

  1. Load category This can be initiated through an API call or a page request (e.g. through the storefront).

  2. Load CMS layout Shopware will load the CMS layout associated with the category.

  3. Build resolver context This object will be passed on and contains information about the request and the sales channel context.

  4. Assemble criteria for every element Every CMS element within the layout has a type configuration which determines the correct page resolver to resolve its content. Together with the resolver context the resolver is be able to resolve the correct criteria for the element. All criterias are collected in a criteria collection. Shopware will optimize those criterias (e.g. by splitting searches from direct lookups or merging duplicate requests) and execute the resulting queries.

  5. Override slot configuration The resulting configuration determine the ultimate configuration of the slots that will be displayed, so Shopware will use it to override the existing configuration.

  6. Respond with CMS page Since the page data is finally assembled, it can be passed on to the view layer where it will be interpreted and displayed.

Extensibility

As you can see, the element resolvers play a central role in the whole process of getting the configuration (and by extension, content) of CMS elements.

Shopware allows registering custom resolvers by implementing a corresponding interface, which dictates the following methods:

  • getType() : string returns the matching type of elements

  • collect(CmsSlot, ResolverContext) : CriteriaCollectionprepares the criteria object

  • enrich(CmsSlot, ResolverContext, ElemetDataCollection) : void performs additional logic on the data that has been resolved

Separation of content and presentation

The CMS is designed in a way that doesn't fix it to a single presentation channel (fancy people refer to it as "headless"). What at first might seem like an unnecessary abstraction turns out to give us a lot of flexibility. Each presentation channel can have its own twist on interpreting the content and displaying it to the user. A browser can leverage the Shopware Storefront and display the HTML or use the resulting markup from a single page application that interprets the API responses. A native mobile application can strip out unnecessary blocks and only display texts and images as view components. A smart speaker simply reads out the content of elements with the voice type. You name it...

By default, Shopware provides the server-side rendered Storefront as a default presentation channel, but Shopware PWA also support CMS pages. Using the CMS through the API you'll have full flexibility of how to display your content.

All this comes at a price: The admin preview of your content is only as representative of your content presentation as your presentation channel resembles it. A major implication for headless frontends. For that reason, Shopware PWA has a functionality built into the plugin, which allows you to preview content pages right in the PWA.

Further reading