Made in Builder.io

Upcoming webinar with Figma: Design to Code in 80% Less Time

Announcing Visual Copilot - Figma to production in half the time

Builder.io logo
Talk to Us
Platform
Developers
Talk to Us

Blog

Home

Resources

Blog

Forum

Github

Login

Signup

×

Visual CMS

Drag-and-drop visual editor and headless CMS for any tech stack

Theme Studio for Shopify

Build and optimize your Shopify-hosted storefront, no coding required

Resources

Blog

Get StartedLogin

‹ Back to blog

web development

Your headless CMS workflow doesn’t have to suck

January 13, 2023

Written By Steve Sewell

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>
  </>
}

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} />

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.

The above code with arrows pointing to it asking for tons of 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>
+      )}
     </>
   )
 }

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

Someone flipping their desk over

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

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 without requiring code changes and releases every time.

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} />
}

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' },
  ],
})

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:

A meme of "wait it's all just components" "always has been"

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' }],
})

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' },
  ],
})

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()
    }
  }
}

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": "..." }
            }
          }
        ]
      }
    ]
  }
}

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.

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

Diagram of a very complicated workflow of gathering requirements, planning code architecture, adding CMS fields, coding up, approving, pushing code, rolling back, explaining confusing fields, etc

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:

Diagram of a simpler structure where marketing updates content and pushes live themselves, developers update components and deploy them live themselves

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.

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.

Visual representation of the above bullets - a visual of things like landing pages, etc with "visual models" pointing to it, and things like navigation links and product details with structured data models pointing to it

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.

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:

Introducing Visual Copilot: convert Figma designs to code using your existing components in a single click.

Try Visual Copilot

Share

Twitter
LinkedIn
Facebook
Hand written text that says "A drag and drop headless CMS?"

Introducing Visual Copilot:

A new AI model to turn Figma designs to high quality code using your components.

Try Visual Copilot
Newsletter

Like our content?

Join Our Newsletter

Continue Reading
AI9 MIN
How to Build AI Products That Don’t Flop
WRITTEN BYSteve Sewell
April 18, 2024
Web Development13 MIN
Convert Figma to Code with AI
WRITTEN BYVishwas Gopinath
April 18, 2024
Web Development8 MIN
Server-only Code in Next.js App Router
WRITTEN BYVishwas Gopinath
April 3, 2024