Make Fast, Versatile Jamstack Themes with Page Builder Components
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?
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:
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.
Phase 1
CMS models
In the data model for this project, any given flexible_page
has the following 3 fields:
title
- a plaintext stringfeatured_image_url
- a plaintext stringsections
- 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
.
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.
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
.
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:
type
- a plaintext string; value alwayssection_cta
title
- a plaintext stringsubtitle
- a plaintext stringactions
- a list
A section_cards
schema should define the following 3 fields:
type
- a plaintext string; value alwayssection_cards
title
- a plaintext stringcards
- 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
.
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.)
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.
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:
type
- a plaintext string; value alwaysaction
text
- a plaintext stringurl
- a plaintext string
A card
schema should define the following 4 fields:
type
- a plaintext string; value alwayscard
image
- a plaintext stringtitle
- a plaintext stringexcerpt
- 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
.
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
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
- Not on Stackbit yet? Bring your site
- Have a theme already? Section it to use components