How to turn a headless CMS into a full-fledged content management system

Hi all! I'm Lesha Kuzmin, head of Frontend at AGIMA. In this article we will take a detailed look at headless CMS: what pitfalls there are, what to do with project architecture, integrations and dynamic pages. Let's look at site management not only from developers, but also from content managers. As a bonus, we'll go over SEO performance and server setup.

It will be useful for developers with experience in Koa, Express, Strapi and partially React. The article will also be useful for those who like to delve into documentation – I will show examples that will help you understand it.

What is Headless CMS and when should you use it?

Headless CMS is a flexible content management system. Among its features:

  • User-friendly interface for content managers.

  • Low Code Solution. You can quickly and easily expand functionality without involving developers.

  • Seamless (omnichannel) interaction interface, because Headless CMS work with different types of frontend. This helps maintain a single source of truth approach for all API consumers.

  • Multilingual. You can import solutions out of the box without any problems.

At first glance, Headless CMS is very similar to the standard approach with Django, Laravel or WordPress with a JSON API attached. But the point is that the headless approach has several additional advantages. I will highlight the main ones by comparing Strapi CMS and WordPress:

  • Complete and easy design customization

    The frontend in Strapi is a separate application. It is not related to the CMS itself, which gives greater flexibility for creating interfaces.

  • Content delivery speed

    It's controversial, but Google claims that sites developed using Headless CMS are faster than, for example, WordPress.

  • Safety

    WordPress has a lot of vulnerabilities, and therefore even a simple backend on it is not entirely secure.

  • Ease of maintenance and deployment.

    With Strapi, everything is simpler: we build a Docker image, push code changes to the repository, and if CI/CD is configured, everything is deployed without problems.

  • Multilingual

    In Strapi, multilingual integration is easier than in WordPress. Integration with third-party translation services is also supported: you can separate the area of ​​responsibility and not allow translators into the content management system, and in this case, they will use their usual tool.

The difference between traditional and headless approaches.

The difference between traditional and headless approaches.

Examples of using Headless CMS

Let's take an example our project for the World Class fitness club network.

We assessed the technical requirements, the stack, estimated the tools, resources and began to build the architecture.

To work on the customer’s website, we chose Strapi among all the Headless CMSs, although we also use Payload quite often. The main advantage is the ability to work with them in Standalone mode, which not all Headless CMS have.

Trying SSG

In the first iteration, we chose the SSG approach, that is, server-side page generation. This is the picture we got:

Let's take a closer look at the block with the diagram of connections between services:

We have Headless CMS, here it is Strapi. Strapi has lifecycle hooks that allow you to perform some action, for example when data changes. In our concept, Strapi Hooks pull a separate CI/CD, which triggers a rebuild of the application, and push a new build of the SSG application to the hosting. In between there is a UI Kit with components and their props. We consume components in SSG through the UI Kit, and consume props as circuits in Strapi.

What difficulties might there be with SSG?

The first is the application’s dependence on changes made to Strapi, and, accordingly, rebuilding for every change in the content system. Yes, it would be possible to localize the problem by covering with hooks only the part of the data that we will change, but SSG does not allow skipping assemblies when changing content.

Why did we even look towards SSG? It's simple, we didn't want a separate server application for the front end. This would lead to additional stress on the infrastructure. We needed to add new pages from the admin panel, which is where the main problem lies. It can also lead to reassembly cycles with frequent changes.

But this approach cannot be called completely unworkable. On the contrary, if it turns out that the content will not change frequently, then it will be justified, just with some restrictions.

The second factor is fault tolerance. Yes, in the modern world this sounds strange – you can simply wrap the application in Docker and give it to devops, who will set up the necessary level of automation. But here's a little more detail.

We planned to divide CI/CD into several stages, and in addition to the main role for building the application, two more were allocated. One of them was exporting JSON to describe components and allowed us to strictly adhere to contracts, and the second was for rebuilding routing, when we do not need to rebuild the entire application.

This is where the biggest problem lies. Services are interconnected and most often they simply cannot exist without each other.

Switching to SSR

We prepared a prototype of the previous solution with SSG, after which we were clearly convinced of our concerns.

Something needed to change. Since the requirements for the stack were limited by the customer, we decided to change the approach for rendering pages and switch to SSR – server-side rendering.

With this approach, it is easier to work with dynamic addresses, which also simplifies the data schemes: direct addresses of pages for routing assembly have disappeared from them, and simply names or page identifiers have appeared instead.

Here are the advantages of SSR compared to SSG:

  • Dynamic page rendering.
    Yes, but now we are consuming server resources.

  • Lack of routing assembly service.
    Now the routing is dynamic and is assembled independently.

  • Increased fault tolerance.
    Now we have two services instead of three. They run in Kubernetes, can communicate with each other through the network layer, and the speed of their interaction is quite high. This helps avoid long waits when assembling content pages.

  • No build queues.
    We got rid of reassembly queues, because now after updating the content, it immediately goes directly to the site.

Developer interaction

Now let's see how it all works in the interface. Here is our plan for working with CMS:

  • create a component or group of components in Strapi;

  • create a layout for the page;

  • we define dynamic zones and add available components to them that the content manager can select;

  • adding content;

  • We parse the request for data.

In the admin panel we will see a completely basic interface with Content-Type Builder. It has a separate block with components – click “create new component”.

Enter the component name and group. If the group doesn't exist, we can create it on the fly. This is convenient because this way we systematize the arrangement of components in the tree.

Next, we add the necessary fields within the diagrams and within the components themselves. Here we can also add dynamic components.

Add a slide component to the slider. We immediately choose a strategy for how we will use it – as a repeating component or as one displayed once.

We collect the basic scheme and save it so that a collection appears for our pages.

We also specify a name in the page collection, and Strapi automatically suggests what our API endpoint will be called. It can be renamed if suddenly needed.

Next you need to add dynamic zones. This is as easy as adding fields, because we are interacting with one interface.

To create a dynamic zone, you need to write its name.

The system will offer to create or select from the created components those that we will use in this specific dynamic zone.

We ended up with this diagram:

Here we see a question and answer section – FAQ and a slider. They are in the dynamic zone component.

What about the content manager?

Now let's see what a content manager does in a CMS. It goes to its Content Manager section, which has nothing to do with the Content-Type Builder.

This and the following screenshots were taken in Dev mode, so we see the Content-Type Builder section.  In Prod mode it is not available to the content manager.

This and the following screenshots were taken in Dev mode, so we see the Content-Type Builder section. In Prod mode it is not available to the content manager.

Go to the collection, create a new record and fill in the required fields. An expanded window with a dynamic zone appears at the bottom. We see that it now displays a dynamic FAQ component and a slider.

We can choose any of them. Let's select the slider and add slides within this component. Under each slide there is a separate Add an entry button, which allows you to add new items.

Here we can drag and swap components within a single component or across the entire dynamic zone. The order of the components will change, the page will be reassembled, and you will get dynamic content. From the point of view of working with a CMS, this is very convenient, since the content manager has access to change the interface.

The components have been added, and so has the data. How can we get them?

We've added data and created components. Now we need to get the component data and their order in order to render on the front end.

Let's go to settings, to roles, in our case it's Public. We open access to the API, informing that we have search options find and findOne.

We save, go to Postman and write the following request to get from the API all the data that we added:

http://localhost:1337/api/pages/1?populate[0]=components&populate[1]=components.FAQItem&populate[2]=components.BeautifulSlide&populate[3]=components.BeautifulSlide.image

Let's analyze the request in parts:

Here's the answer we'll get from Strapi:

Here we see an array with objects: this is the ID, its order and the name of the component with additional internal components – slides. In the FAQ block, Strapi also automatically generated the name of the component that we will work with in our front-end application.

In my opinion, this is not the most convenient way to work with components. And this is due to the complexity of forming the request itself, since in the above example we receive only a couple of components in the dynamic zone – imagine if there are several dozen of them. Therefore, I propose to consider another one.

Changing the strategy for working with components

To do this, you need to break down the components into diagrams:

  1. We create diagrams for basic components.

  2. Create a diagram for the page layout.

  3. We create a diagram to connect components and pages.

  4. Adding content.

  5. We analyze requests for data.

Here we also create additional schemes for questions and answers and for review sections, into which we add basic fields. But the most interesting thing is the basic components and their order along with the layouts. We'll look at them in more detail a little later.

Here we can also add slides, only now we do not set them within the dynamic zone, but create a new entry each time. One slide can be displayed both on one and on different pages, which gives some flexibility. All this is configured by connections within data schemas.

To obtain this data, go to the settings and allow access to all newly created schemes so that we can access them via the API.

What has changed in data acquisition?

Before we look at requests, let's first look at the general scheme of service interaction:

Our SSR or SSG application accesses a table with the order of components and, knowing the name of the layout, requests a list of components that need to be rendered. Having this list, we get data from the system, ask the UI Kit for its appearance and then assemble the page.

What the requests look like:

Let's expand the query to get component names. We want a list, so we ask for the names of the base components:

http://localhost:1337/api/components-orders?populate[0]=base_components

Now let's filter the request by page name. We enable the additional filter flag, informing that we have a layout table, and it has a slug field. It must be equal to index for our list to be displayed in the end:

http://localhost:1337/api/components-orders?populate[0]=base_components&filters[layouts][slug][$eq]=index

This list is no more interesting than the previous example. We need to transform it something like this:

In general, only the name and ID are enough here to understand which component to substitute and get the data.

For example, let's request them for feedback:

http://localhost:1337/api/my-review-items?populate[0]=image

Let's filter by ID from the connection:

http://localhost:1337/api/my-review-items?populate[0]=image&filters[my_review][id][$eq]=1

This way we will get all the data for reviews.

We refer directly to the items that are review. Next we inform you that we want to add an image there, because, unfortunately, there is no provision for embedding media content. Next, we apply filters and inform that the review of the identifier must be equal to one. To this response, Strapi returns JSON, which will contain all the necessary data.

What about SEO?

We compared the indicators new World Class website with the old site, and this is what we got:

The most interesting indicators for us are Performance, which increased by 20%, and the speed of rendering the first content. Yes, these are synthetic tests, and perhaps the x10 indicator is not entirely relevant, but the site really opens quickly now – check the link above for yourself.

How it works on the frontend. Examples on NuxtJS and NextJS

In these examples, I will omit the moment of receiving data on the application side. We will only consider examples for assembling page bundles.

This is how dynamic assembly works in NuxtJS:

Here we get a list of components in dynamic zones, which are iterated through, and using named slots we add the components we need to the template. As a result, just a few lines of code allow you to dynamically assemble a page with all the data that arrived from the content manager from Strapi.

With React it's a little more complicated. It requires the creation of mapping, because if we do dynamic import on the fly, then the entire UI Kit will end up in our application, and the bundle will become too large. Therefore, we have been looking for a solution to this problem for a long time.

We decided with the above-mentioned mapping: we created an object into which we lazily import our components. Yes, it’s more work than NuxtJS, but it’s worth it. The only thing we need to consider is a fallback with an empty component if it is not in our map. To avoid getting a render error, you can return null.

Component mapping for NextJS.

Component mapping for NextJS.

In NextJS the logic is more branched. Here we go to the dynamic component map, check that this component exists and it is not null, check if there is data for this component. If all is well, then we return the component itself, the key for it and the data from which we will collect it.

Since in the previous step we received an array of components, we can iterate over them on the fly and render them into the page.

There is a little more code, but in general the system is the same on NuxtJS and NextJS. Therefore, choose a framework that you are used to working with.

What to pay attention to:

  • Careful selection of hosting/server.

  • Configuring modern protocols.

    It is advisable to have at least HTTP/2 under the hood, because the latest NuxtJS and NextJS bundle contains a large number of files. Ideally, tweak it to the third HTTP. Then, using the UDP protocol, the browser itself will decide which content to download for display first. With this solution, sites work very quickly.

  • Modern compression formats.

    It is necessary to gradually move away from Xip towards Brotli, which sends a little less traffic. If we use it together with a modern compression format protocol, we can significantly speed up the delivery of content to users.

Total

When you need Headless CMS:

  • If necessary, deploy the Mock API.

    Headless saved us a couple of times when we needed to deploy a Mock API server, but FireMock didn’t want to work.

  • You need to quickly test the theory and launch an MVP.

    But keep in mind that what is temporary may remain forever.

  • To simplify the development process.

    It happens that a company does not have the opportunity to provide backend resources for the development of a particular feature. With Headless CMS, you can loop the process on front-end developers without additional labor costs.

PS Share your opinion about headless CMS in the comments. Which ones did you work with, what worked and what didn’t. I'll be happy to ask questions.

And also subscribe to our channel about team leadership, skills and technologies – https://t.me/ashutay.

What else to read

Similar Posts

Leave a Reply

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