When creating a theme for any website, one may typically take into account the importing of existing content, creation of new pages, and ongoing editing. These classic content updates can be done by developers, editors, customers, and other stakeholders responsible for the finished product. To them, the limits of any given page are defined first and foremost by the flexibility of your theme: What does your content model make available to them and what level of flexibility lies therein.

Sometimes, the day-to-day realities of your clients, your marketing team, or your business yield unforeseen requirements like landing pages, lead capture forms, pricing matrices, and other types of content. Unlike most Jamstack themes, classic page builders allow for this kind of flexibility and creativity, especially for ad-hoc projects or ones on a tight deadline.

In this tutorial, we’ll learn how to incorporate drag-and-drop-ready content models that will empower your clients and editors to spin-up spontaneous sections and pages using your Jamstack theme, with the versatility of a visual page builder.

If you've read about the virtues of content management systems -- particularly headless API-based CMS that promise easy reuse of content across multiple media -- you've probably seen a million tutorials about modeling a movie database, an e-commerce store, or a conference schedule. These are data-driven content models where you can use domain knowledge from the real world to imagine what properties you'll need to store about each record without overthinking the fact that the data might eventually end up on a web page.

But we can't entirely ignore the world's expectations of how web pages are "supposed to work" these days, can we? Page builders like Squarespace and Wix let content authors drag visual components onto and off of a screen, typing content straight into them. With Stackbit, you can provide this experience on the Jamstack. You'll just need to model a few extra content types -- ones whose properties model the way they should look, rather than what they represent in the real world.

We'll let you in on a little secret: most web pages are 1-dimensional (lines), not 2-dimensional (flat surfaces). You look at them on screens, so they feel 2-D, but no one likes horizontal scrolling, so for all practical purposes, they're 1-D.

Start imagining web sites as vertical lists of user-interface components stacked on top of each other. Open up a modern site authoring tool. Have you noticed that when you try to drag a new component onto a page, the default behavior is to allow it to be dragged above or below another component? Sure, some full-width component types offer the option to split them into 2 or 3 columns, but that's just a special case of nested lists, isn't it?

Illustration of adding a section in a web site builder

There's a structure to components in these tools, too. You can't simply add a 2nd photo to a testimonial in a "testimonials" section because you're indecisive about which photo best represents the quote. Page builders have a predefined set of properties that go with each component type. Some component types are very flexible, nesting lists inside lists under the covers, but at some point, the flexibility stops.

Build your own theme

Let's model and build a simple theme of our own that includes a flexible page builder. We'll make it visually editable with Stackbit Studio. We'll use Git file-based data storage, but we'll touch on a few examples of API-based data storage as well.

Our theme will have 5 components across 3 levels of nesting:

Illustration of the page structure

We'll build the theme in 3 phases, corresponding to these levels.

In the static site generator ("SSG"), level 1 holds everything else together. Every flexible_page record needs to be transformed into a full <html>...</html> web page with its own URL. In phase 1, we'll build the flexible_page content type and template, without worrying about the details within. The remaining content types' templates only output fragments of HTML used within these pages. We'll flesh them out, one level at a time, in phases 2 and 3.

Doodle of the project phases

Phase 1

CMS models

In the data model for this project, any given flexible_page has the following 3 fields:

  1. title - a plaintext string
  2. featured_image_url - a plaintext string
  3. sections - a list

Right now, we'll make sections a list of plaintext strings. Later, we'll update it to be a list of section_cta and section_cards components.

Using Git-based data storage, we don't strictly need a content management system ("CMS"), but using one is a great way to help you think in data models while designing a theme.

There are many ways to configure a schema to make a CMS aware of your data model. If you'd like, take a look at examples for Stackbit, Netlify CMS, and Sanity.io.

(Stackbit can infer your data model from a Stackbit schema, a headless CMS schema, or from your existing data, so you may not need to write an explicit schema at all.)

Data entry

To begin, let's add index.md to our filesystem by hand:

---
title: Home
featured_image_url: /assets/images/header_house.svg
sections:
  - section 1
  - section 2
---

Note that index.md doesn't have a body. It's 100% structured data, written in the YAML punctuation standard. Sectioning for a page builder relies on lists and objects, which YAML is optimized to store. Our data sits at the top of the file, a.k.a. the file's "front matter."

Don't worry about losing Markdown support by leaving the file body empty -- you can still boldface text that you store in front matter. YAML supports multi-line text values, so Markdown-inside-YAML isn't at all unusual.

SSG templates

Now we're at the exciting part: transforming our raw data into HTML.

We'll look at an example using Jekyll because its Liquid templating language is simple to show, but these concepts are the same in all popular SSGs like Hugo, Gatsby, NextJS, and Eleventy, just to name a few.

Ultimately, flexible_page.html is responsible for rendering a complete <html>-to-</html> web page for each flexible page record in our dataset. However, it delegates a lot of the sitewide code to base.html, which in turn delegates the navigation menu to nav.html.

Doodle of our level-1 SSG dependency graph

Check out flexible_page.html, the backbone of building each web page. Amazingly, it only has 14 lines of code. By the time we're done adding all of our components? 15 lines of code. Nice and easy to maintain!

Are you ready to see it? View the full codebase on GitHub and deploy your own copy with Stackbit. Stackbit will create a new GitHub repository and a new Netlify site for you. Then you will be redirected to Stackbit Studio where you will be able to edit your site.

Create with Stackbit

Animated GIF of Stackbit Studio screen capture editing a page built from phase 1 of the tutorial

In Stackbit Studio, rearranging section 1 and section 2 is drag-and-drop simple.

Adding a second page is as simple as clicking Add Page toward the top of the left-hand navigation panel, clicking Create Page, choosing the Flexible Page template, and entering a New page path like contact or about.

Fireworks

Phase 2

Now let's build out our two section types: section_cta and section_cards.

First clone the GitHub repostiory Stackbit created for you. You can find the link to your repostiory in the settings dialog. Then, checkout the preview branch. Stackbit Studio uses this branch to show you the site preview.

git clone <created-repo-url>
cd <repo-folder>
git fetch
git checkout preview

CMS models

In the data model for this project, a section_cta has the following 4 fields:

  1. type - a plaintext string; value always section_cta
  2. title - a plaintext string
  3. subtitle - a plaintext string
  4. actions - a list

A section_cards schema should define the following 3 fields:

  1. type - a plaintext string; value always section_cards
  2. title - a plaintext string
  3. cards - a list

Right now, we'll make actions and cards lists of plaintext strings. Later, we'll update them to be lists of action and card components, respectively.

A CMS schema configuration file might look like one of these examples for Stackbit, Netlify CMS, or Sanity.io. Note that in phase 2, the sections field of a flexible_page is no longer a list of strings. Instead, it is a list of section_cta and section_cards objects.

Data entry

Hand-edit index.md and any other files you've created, clearing out the contents of sections. We'll add the sections later in Stackbit.

---
title: Home
featured_image_url: /assets/images/header_house.svg
sections: []
---

SSG templates

Now that we've added two new component data types to our content model, we need to add two new Jekyll templates to render them into HTML: section_cta.html and section_cards.html.

Doodle of our level-2 SSG dependency graph

We'll make flexible_page.html call section_cta.html with an include command, similarly to the way base.html called nav.html.

There is one difference, however: nav.html relied upon two properties, sites.pages and page, that were implicitly passed to it by base.html. By contrast, we'll code both section_cta.html and section_cards.html to avoid using any data that the include call from flexible_page.html doesn't explicitly pass as a parameter named section.

This might seem like overkill when Jekyll would be more than happy to let component templates peek at the data held by flexible_page.html. Nevertheless, it's good idea to design SSG components as a "black box" to their calling contexts.

As a rule of thumb, if a component renders something that goes on every page, and if it only uses data that is inherently global to the workings of the SSG, like nav.html, it might be safe to let it rely upon its calling context’s data. Otherwise, pass the component the data it needs through parameters.

Just like in conventional programming, the principle of encapsulation lets you build SSG templates that are easy to modify and reuse.

For example, maybe you add employee to your content model as a data-driven content type. Then you add section_staff as an appearance-driven content type (fields: a type string of section_staff, a title string, and an employees list), which you declare to be a valid third type for sections within flexible_page. If a staff directory visual component can look exactly like any other card gallery, you might be able to let a section_staff.html template hand off HTML rendering to card.html or even to section_cards.html. (You'd still want a section_staff.html component for minor data-crunching activities, like making employee data look more like card data before being passed as a parameter.)

Illustrations of 2 possible SSG component structures that involve parameterized reuse

But that’s getting a little ahead of ourselves. To implement phase 2 of SSG templating, we'll edit the end of flexible_page.html in the for loop. Delete this line of code:

<section class="phase1">{{ current_section }}</section>

Replace it with this:

{%- capture section_type -%}{{ current_section.type }}.html{%- endcapture -%}
{%- include {{ section_type }} section=current_section -%}

Then create section_cta.html and create section_cards.html.

If you don't want to change the files yourself, you can run the following command from the root of your project to copy the needed code from phase2 folder:

cp -Rf phase2/ .

If you did everything correctly, after running the git status command, you should see the followig output:

On branch preview
Your branch is up to date with 'origin/preview'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   src/_layouts/flexible_page.html
        modified:   src/flexible_pages/contact.md
        modified:   src/flexible_pages/index.md
        modified:   stackbit.yaml

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        src/_includes/section_cards.html
        src/_includes/section_cta.html

no changes added to commit (use "git add" and/or "git commit -a")

Commit your changes and push them to the remote preview branch:

git add .
git commit -m "tutorial phase 2"
git push

Go back to Stackbit Studio, you should now be able to add new Actions to the "Call to Action" section and new cards to the "Card Gallery" section. To add a section in Stackbit Studio, beneath Sections within the page you'd like to edit, in the left-hand navigation panel, click Add Content. Under Actions or Cards (depending upon the type of section you chose), click Add Content again to add buttons or panels within your new sections. You can fill in the rest of the details in the left-hand navigation panel or directly in the page preview.

Animated GIF of Stackbit Studio screen capture editing a page built from phase 2 of the tutorial

Phase 3

Finally, we'll build out our final section types: action and card.

CMS models

In the data model for this project, an action has the following 3 fields:

  1. type - a plaintext string; value always action
  2. text - a plaintext string
  3. url - a plaintext string

A card schema should define the following 4 fields:

  1. type - a plaintext string; value always card
  2. image - a plaintext string
  3. title - a plaintext string
  4. excerpt - a plaintext string

A CMS schema configuration file might look like one of these examples for Stackbit, Netlify CMS, or Sanity.io. In phase 3, the actions and cards fields of section_cta and section_cards are no longer list of strings, instead becoming lists of action or card objects, as appropriate.

Data entry

You could hand-clear any data found in actions or cards areas of your existing front matter files, but if you'd like, you can also replace all of your markdown files with index.md and contact.md as seen here.

SSG templates

We're ready to add our final two templates to Jekyll: action.html and card.html.

Doodle of our level-3 SSG dependency graph

First, edit the end of section_cta.html, in the for loop. Delete this line of code:

<div class="action">{{ current_action }}</div>

Replace it with this:

{%- include action.html action=current_action -%}

Next, edit the end of section_cards.html, in the for loop. Delete this line of code:

<div class="grdCell -4of12"><div class="card">{{ current_card }}</div></div>

Replace it with this:

{%- include card.html card=current_card -%}

Then create action.html and create card.html.

If you don't want to change the files yourself, you can run the following command from the root of your project to copy the needed code from phase3 folder:

cp -Rf phase3/ .

If you did everything correctly, after running the git status command, you should see the followig output:

On branch preview
Your branch is up to date with 'origin/preview'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   src/_includes/section_cards.html
        modified:   src/_includes/section_cta.html
        modified:   src/flexible_pages/contact.md
        modified:   src/flexible_pages/index.md
        modified:   stackbit.yaml

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        src/_includes/action.html
        src/_includes/card.html

no changes added to commit (use "git add" and/or "git commit -a")

Commit your changes and push them to the remote preview branch:

git add .
git commit -m "tutorial phase 3"
git push

Animated GIF of Stackbit Studio screen capture editing a page built from phase 3 of the tutorial

Once you're in Stackbit, be sure to play in the Studio and see how easy it is to point-and-click your way through a complete content overhaul:

  • Add, remove, & edit pages
  • Add, remove, edit, & reorder sections
  • Add, remove, edit, & reorder actions & cards within a section

Next steps

Now that you've built a small page builder, ask yourself:

Where can my theme benefit from drag-and-drop sections that empower end-users?

When you consider the versatility that can be demanded from your theme and incorporate containers and sections appropriately, you empower end-users to be creative and effective within your theme's constraints. It allows editors, designers, and anyone to quickly spin up pages that fit their often impromptu needs and provide visual feedback reflecting their work results. In this case, the scalability and freedom that the Jamstack provides to developers can go hand in hand with empowering clients, editors, and anyone with the flexibility they require to make modern websites an integral part of their strategy.

Helpful resources