Andrew Welch
Published , updated · 5 min read · RSS Feed
Please consider 🎗 sponsoring me 🎗 to keep writing articles like this.
Creating a Content Builder in Craft CMS
Using the Matrix block to create a “content builder” for your client is much better than just giving them a Rich Text field
When a website is created, typically a graphic designer creates the look, and a frontend developer implements it. There may be a few other roles mixed in, such as people who do marketing, SEO, UX, etc., but that’s the basic flow.
What’s missing from this equation is the client. Without the content from the client, the website is essentially an empty shell. The client needs to be able to create and modify content to suit their needs; that’s the entire point of using a Content Management System (CMS) to begin with.
So how do we provide the client with a good content authoring experience? In the past, the Rich Text field has been used as a way to provide the client with the ability to enter the formatted text and images that they might want on the page.
The problem is that it provides a horrible content authoring experience, from the point of view that the client can end up making a mess of things.
And this isn’t their fault! It’s our fault as frontend developers for not doing our job.
I can’t tell you how many times I’ve been talking to a frontend developer, and they roll their eyes when speaking about what “the client did to the site”. Well, no. You gave them the power to do it, it’s your fault for not designing the content entry in a way that doesn’t allow the client to “screw it up”.
The people we build websites for may or may not be adept at technology or design, but it’s very likely they are quite good at their jobs. It’s somewhat unfair to blame them for what they end up doing if they aren’t given appropriate guidance and constraints.
Instead of a free-for-all Rich Text field, a better way is to create a “content builder” for your clients to use, with Craft CMS’s Matrix Block as a basis.
I wasn’t planning to write this article, since there are a number of good resources available already on the Matrix Block as a content builder, such as Craft CMS Content Builder: The Client Experience and Matrix as a Layout Builder.
However, I had several people ask me about it recently, so I figured it couldn’t hurt to toss my hat into the ring. Plus, I think there is something compelling about using this blog in a self-referential way to show how this blog is built. So let’s get to it!
Link Enter the Matrix
The general idea behind using the Matrix as a content builder is that we create Block Types for each type of content that the client might need to enter. This allows our designers to design how these blocks should look, and it gives the client a framework that they can use to populate them without going off into the weeds.
We are essentially using our expertise to provide a nicely designed box that the client can put the content that makes their website unique into. We aren’t eschewing the Rich Text field entirely, but rather we’re providing it in a constrained context.
Instead of the “kitchen sink” approach to using Rich Text fields, we’re providing a hand-crafted solution that fits the client’s needs exactly.
The content builder used to create the blog you’re reading right now is a simple one. But I think that also makes it good for illustration purposes, so that the core principles are explained clearly. You do can far, far more with Matrix as a content builder than I present here.
The first thing to do before any project is to plan what we need to build. For my blog, I wanted to be able to have:
- Rich Text blocks with formatting controls, and optional titles
- Image blocks with a few different types of images
- Quote blocks for presenting quotes or ideas that I feel are important (and to break up the design a bit)
- Code blocks to present the code examples
So those are the Matrix block types we create:
Then for the template side of things, we have some article header information, but the guts of the matrix block display is quite simple:
{% set lazyImage = false %}
{% for block in entry.blogContent %}
{% include ('blog/_partials/_' ~ blockPrefix ~ 'block_' ~ block.type.handle) %}
{% if block.type.handle == "image" %}
{% set lazyImage = true %}
{% endif %}
{% endfor %}
What we’re doing is just iterating through all of the blogContent Matrix blocks, and using the Twig include directive to bring in a template based on the block.type.handle of the Matrix block. Our templates look like this:
The blockPrefix is used so that I can have the same code patterns for the Google AMP version of the blog content builder; the Twig template partials are just prefixed with _amp. The lazyImage variable is used so that the first image block we present is not a lazy loaded image, but everything after it is.
I could have used ignore missing in the include directive to tell it to gracefully fail if the template it’s trying to include doesn’t exist, but I’d rather have it throw a Twig error in this case, rather than have it silently fail. It’s also possible to have include use a default template if the template it’s looking for is missing; use whatever pattern works best for you.
The cool thing about a setup like this is that if you want to add a new block type, you just create it in the AdminCP, and then create the appropriately named Twig template partial, and away you go. Everything has a neat 1:1 correspondence, and is nicely modularized. DRY, as the cool kids call it.
Link Show Me the Blocks!
So now that you’ve seen how we include these Twig template partials, let’s take a closer look at how they are built.
{% set image = block.image.first() %}
{% if image %}
{% switch block.imageType %}
{% case "normal" %}
<div class="wrap-fixed">
<section class="blog-section">
<div class="row top-xs">
<div class="col-xs-12 col-sm-10 col-sm-offset-1">
{% case "fullWidth" %}
<div class="wrap-fluid">
<section class="blog-section">
<div class="row top-xs">
<div class="col-xs-12">
{% case "infographic" %}
<div class="wrap-fixed">
<section class="blog-section">
<div class="row top-xs">
<div class="col-xs-12 col-sm-10 col-sm-offset-1">
{% endswitch %}
<div class="box blog-section">
<figure class="blog">
{% switch block.imageType %}
{% case "normal" %}
{% set transformedImages = craft.imager.transformImage(image, [
{ width: 1200, ratio: 2/1, jpegQuality: 75 },
{ width: 1024, ratio: 2/1, jpegQuality: 75 },
{ width: 768, ratio: 4/3, jpegQuality: 60 },
],{ format: 'jpg', allowUpscale: false, mode: 'crop', position: image.focusPctX ~ '% ' ~ image.focusPctY ~ '%', interlace: true }) %}
{% if lazyImage %}
<img class="scale-with-grid lazyload" src="{{ craft.imager.base64Pixel(2,1) }}" data-sizes="100vw" data-srcset="{{ craft.imager.srcset(transformedImages) }}" alt="{{ image.title }}" height="auto" width="100%">
{% else %}
<img class="scale-with-grid" src="{{ transformedImages[1].url }}" sizes="100vw" srcset="{{ craft.imager.srcset(transformedImages) }}" alt="{{ image.title }}" height="auto" width="100%">
{% endif %}
{% case "fullWidth" %}
{% set transformedImages = craft.imager.transformImage(image, [
{ width: 1200, ratio: 2/1, jpegQuality: 75 },
{ width: 1024, ratio: 2/1, jpegQuality: 75 },
{ width: 768, ratio: 4/3, jpegQuality: 60 },
],{ format: 'jpg', allowUpscale: false, mode: 'crop', position: image.focusPctX ~ '% ' ~ image.focusPctY ~ '%', interlace: true }) %}
{% if lazyImage %}
<img class="scale-with-grid lazyload" src="{{ craft.imager.base64Pixel(2,1) }}" data-sizes="100vw" data-srcset="{{ craft.imager.srcset(transformedImages) }}" alt="{{ image.title }}" height="auto" width="100%">
{% else %}
<img class="scale-with-grid" src="{{ transformedImages[1].url }}" sizes="100vw" srcset="{{ craft.imager.srcset(transformedImages) }}" alt="{{ image.title }}" height="auto" width="100%">
{% endif %}
{% case "infographic" %}
{% if lazyImage %}
<img class="scale-with-grid lazyload" src="{{ craft.imager.base64Pixel(image.width, image.height) }}" data-src="{{ image.url }}" alt="{{ image.title }}" height="{{ image.height }}" width="{{ image.width }}">
{% else %}
<img class="scale-with-grid" src="{{ image.url }}" alt="{{ image.title }}" height="{{ image.height }}" width="{{ image.width }}">
{% endif %}
{% endswitch %}
</figure>
{% if block.caption |length %}
<figcaption class="blog-image-caption">
<p class="blog-image-caption">{{ block.caption |typogrify }}</p>
</figcaption>
{% endif %}
</div>
</div>
</div>
</section>
</div>
{% endif %}
While this may look a little crazy, it’s really quite simple. We have 3 different types of images defined:
- normal — Displays the image inside of our content div (with optimized responsive sizes)
- fullWidth — Displays the image at full bleed (with optimized responsive sizes)
- infographic — Displays the image entirely unaltered
If we wanted to add more image types, or more control over how the images are displayed or what have you, it would be pretty easy to add them.
<div class="wrap-fixed">
<div class="row top-xs">
<div class="col-xs-12 col-sm-8 col-sm-offset-2">
<div class="box blog-wrapper blog-section">
{% if block.heading |length %}
<h3 class="blog">
<span class="blog-headline-link">
<a id="{{ block.heading |kebab }}" href="#{{ block.heading |kebab }}"> <i class="icon-link"></i></a>
</span>
{{ block.heading |smartypants }}
</h3>
{% endif %}
{{ block.text |typogrify }}
</div>
</div>
</div>
</div>
Not a whole lot to see here, this just renders out our Rich Text blocks (for which I provide a limited subset of editor capabilities, to keep it from getting messy), with an optional headline.
We use the fantastic Typogrify plugin to ensure that we get nice curly quotes, no widows, and other typographical features automatically, without the client having to know how to do them!
<div class="wrap-fixed">
<div class="row top-xs">
<div class="col-xs-12 col-sm-8 col-sm-offset-2">
<div class="box blog-wrapper blog-section">
{% if block.heading |length %}
<h3 class="blog">{{ block.heading |smartypants }}</h3>
{% endif %}
<blockquote class="blog-quote">{{ block.quote |typogrify }}</blockquote>
</div>
</div>
</div>
</div>
Just a quote, ma’am.
<div class="wrap-fixed">
<section class="blog-section">
<div class="row top-xs">
<div class="col-xs-12 col-sm-10 col-sm-offset-1">
<div class="box blog-code-wrapper">
<pre class="line-numbers"><code class="language-{{ block.codeType }}">{{ block.code }}</code></pre>
{% if block.caption |length %}
<aside class="blog-code-caption">
{% if block.codeLink |length %}
<h5 class="blog-code-caption"><i class="icon-code"></i> <a class="blog-code-caption" href="{{ block.codeLink }}" target="_blank">{{ block.caption }}</a></h5>
{% else %}
<h5 class="blog-code-caption"><i class="icon-code"></i> {{ block.caption }}</h5>
{% endif %}
</aside>
{% endif %}
</div>
</div>
</div>
</section>
</div>
Link It’s a Wrap!
And that, as they say, is that! It’s a pretty simple setup, but hopefully someone finds it useful as something to consider in lieu of just slapping a Rich Text field into their pages, and wishing their client “good luck!”
If nothing else, it’ll make your graphic designers happy, and increase the odds that when you look at the website a year or two after delivering it to the client, that it hasn’t become an eyesore that you’re tentative about linking to from your “Our Clients” page.