DEV Community

Cover image for Your headless CMS workflow doesn't have to suck
Steve Sewell for Builder.io

Posted on • Originally published at builder.io

Your headless CMS workflow doesn't have to suck

So you've got this nice homepage, and all this stuff is coming from data from a CMS, so other team members can use a UI to update the heading, the button text, and stuff like that:

function Home({ cmsData }) {
  return <>
    <Hero image={cmsData.image}>
      <Heading>{cmsData.heading}</Heading>
      <Button href={cmsData.ctaLink}>{cmsData.ctaText} </Button>
    </Hero>
    <Columns>
      <Product product={cmsData.product1} />
      <Product product={cmsData.product2} />
    </Columns>
  </>
}
Enter fullscreen mode Exit fullscreen mode

But here's where you may start running into problems…

Next thing you know, the marketing team wants a second button. So you add a new couple fields to the CMS, it's a little ugly because now you have ctaLink1 and ctaLink2, but whatever, I guess:

        <Button href={cmsData.ctaLink}>{cmsData.ctaText}</Button>
+        <Button href={cmsData.ctaLink2}>{cmsData.ctaText2}</Button>
        <Columns>
          <Product product={cmsData.product1} />
Enter fullscreen mode Exit fullscreen mode

Oh, but now we only need that button sometimes, so we'll say, okay, if we have that link, include it, otherwise omit it.

A week later now we want to have a subheading too.

The next week, we want a third product placement.
This is getting… cumbersome.

Screenshot of the original code with arrows all over it of people asking for changes

Another week later, we want to arrange the whole layout…

function Home({ cmsData }) {
   return (
     <>
-      <Hero image={cmsData.image}>
-        <Heading>{cmsData.heading}</Heading>
-      </Hero>
-      <Button href={cmsData.ctaLink}>{cmsData.ctaText}</Button>
       <Columns>
         <Product product={cmsData.product1} />
         <Product product={cmsData.product2} />
+        {cmsData.product3 && <Product product={cmsData.product3} />}
       </Columns>
+      <Hero image={cmsData.image}>
+        <Heading>{cmsData.heading}</Heading>
+        {cmsData.subHeading && <Heading>{cmsData.subHeading}</Heading>}
+      </Hero>
+      <Button href={cmsData.ctaLink}>{cmsData.ctaText}</Button>
+      {cmsData.ctaLink && (
+        <Button href={cmsData.ctaLink}>{cmsData.ctaText}</Button>
+      )}
     </>
   )
 }
Enter fullscreen mode Exit fullscreen mode

“But sometimes we still want the old layout, so make it optional”

Gif of flipping a table

This is a mess

This tight coupling of our content and code is causing a complete mess. And not just of our code - our workflow is a mess, JIRA is a mess with tickets, the works.

This can be an indication that a structured data model may not be the best solution for our needs

Visual models: an alternative to structured data

An alternative to a structured data model is a visual model that can dynamically render the contents of this page, allowing the teams who need to make changes to do so withouttime.

import { BuilderComponent } from '@builder.io/react'

function Home({ cmsData }) {
  // Dynamically renders the page via a component composition that comes
  // via an API call
  return <BuilderComponent model="home" content={cmsData} />
}
Enter fullscreen mode Exit fullscreen mode

A visual model, flips things on its head where you register components that are available to be used and what their props are.

And once your design system is registered, your non-technical teams can rearrange the components in any configuration to keep things on brand and performant, but flexible enough so if they want to add or remove a button or a product or rearrange the components that comes over an API, and I don't have to hardcode all of that, especially as it's always changing.

import { BuilderComponent, registerComponent } from '@builder.io/react'

function Home({ cmsData }) {
  return <BuilderComponent model="home" content={cmsData} />
}

// Register your components to be used by the dynamic renderer,
// and in the visual editor
registerComponent(Hero, {
  inputs: [{ name: 'image', type: 'image' }],
})
registerComponent(Product, {
  inputs: [{ name: 'product', type: 'product' }],
})
registerComponent(Button, {
  inputs: [
    { name: 'text', type: 'text' },
    { name: 'link', type: 'url' },
  ],
})
Enter fullscreen mode Exit fullscreen mode

With a visual model, you get a more component-driven approach that involves a visual interface, where you can add, remove, and rearrange your components, and input their props visually.

Using a visual model, we can recreate that entire page, and all edits, in a few seconds just by dragging and dropping your components:

Example Gif of using the Builder.io drag and drop editor to drag and drop with your components

It’s just components

Meme of "Wait it's just components?" "Always has been" space guys

The whole idea here is to keep your code component-driven, and to decouple marketing content and your code.

Certainly, major parts of your sites and apps should be in code. But for areas that are largely of interest to other teams — like your homepage, landing pages, and sections over other pages — it can be better to get those areas out of your codebase entirely.

This can allow you, as the developer, to focus on components, and your other teams to rearrange them like lego blocks as needed.

The core idea is that you should be able to use your current components as-is. If you already have a Hero, or Button, all you need is registerComponent() and teammates can use them.

The exact details of these components could look for instance something like this:

import { registerComponent } from '@builder.io/react'

export function Hero(props) {
  return <div className="hero">
     <Image className="hero-image" src={props.image} />
     {props.children}
  </div>
}

registerComponent(Hero, {
  inputs: [{ name: 'image', type: 'image' }],
})
Enter fullscreen mode Exit fullscreen mode

Or, of course using any number of dependencies:

import { registerComponent } from '@builder.io/react'
import { Button as MuiButton } from '@mui/material'

export function Button(props) {
  return <MuiButton href={props.link}>{props.text}</MuiButton>
}

registerComponent(Button, {
  inputs: [
    { name: 'text', type: 'text' },
    { name: 'link', type: 'url' },
  ],
})
Enter fullscreen mode Exit fullscreen mode

API-driven component composition

In this format, component compositions are just data in a database. This is how non-devs on your team can create new pages and layouts (or whatever you allow with roles and permissions), and save them in a structured way.

You can assign special fields to visual entries that need to be filled out, and query on them accordingly:

import { BuilderComponent, builder } from '@builder.io/react'

export async function getStaticProps() {
  return { 
    props: {
      cmsData: await builder.get('home', {
        locale: 'en-us',
        query: {
          // Filter on anything
        }
      }).promise()
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Under the hood, the data is just a tree describing a set of components and their props:

{ 
  "data": {
    "yourCustomFieldName": "yourCustomFieldValue",
    "blocks":  [
      {
        "component": {
          "name": "Hero",
          "options": {  "text": "https://..." }
        },
        "children": [
          { 
            "component": { 
              "name": "Button", 
              "options": { "text": "Click me", "url": "..." }
            }
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Because the data is pure JSON, it can support any frontend technology. React, Vue, Svelte, Solidjs, Qwik, PHP, Rails, you name it.

You can read more about how API-driven visual models work here.

Note that this technique is also sometimes called server-driven UI, for instance you can read how it is implemented by Airbnb here.

A decoupled workflow

Ultimately, our goal is to move away from an interdependent workflow:

A messy diagram of a complicated workflow involving multiple teams and tons of changes

What we don’t want is multiple teams to have to be involved to accomplish a basic task. If a marketing team needs to rearrange a page as a test, what we want to avoid is having to create tickets, us developers having to develop the tickets, cut a release, get feedback that actually they want another set of tweaks, and repeat.

This is ugly and requires a lot of process and interdependency. What we want, instead, is a decoupled workflow:

A simpler diagram of marketing able to make changes as they need on their own, and devs can change their code as they need on their own

I like to think of it as separation of concerns, but for people. Each team, ideally, should be autonomous in their own way. Developers should write and ship components. Marketers should arrange those components to meet their marketing needs.

When to use structured data models vs visual models

Don’t get me wrong, there is still a valuable place for structured data. Even Builder.io, which first introduced the concept of visual models, supports structured data models first class.

Structured data models work well for anything that should intentionally have a very restrictive and data-oriented structure.

Some great use cases for structured data models:

Visual models are better for parts of your site or app that are very content-oriented, and generally managed by non-technical teams.

They are generally large regions, such as full pages or large sections like heros, where the goal is to display marketing-driven content to your visitors.

This includes things like:

The beauty here, though, is you can mix and match as you choose. It’s very common that a given site will use multiple model types across it, even on the same page.

Basic diagram representing the above bullets - illustrating the various use cases and with the type of model pointing to each

Conclusion

Using structured data to manage content on your site can be convenient and easy to setup, but can cause challenges over time as things need to change and evolve.

While structured data is amazing at describing how your site or app exists today, it doesn’t leave much flexibility to change and evolve over time without constant updates to code, begging the question if coupling content and code so tightly is the right model for all things.

Adopting visual models, in addition to structured data models, can help improve your team workflows and better decouple content and code, allowing you to maintain a more component-driven focus as a developer.

While Builder.io is who introduced this concept, I hope to see more CMSs introducing visual models to their platforms one day too.

About Me

Hi! I'm Steve, CEO of Builder.io.

We created the concept of visual models, because we've felt the pains of the current state of headless CMSs and needed a better solution.

You may find Builder interesting or useful:

Gif of Builder.io

Top comments (0)