Andrew Welch · Insights · #frontend #matrix #craftcms

Published , updated · 5 min read ·


Please consider 🎗 sponsoring me 🎗 to keep writing articles like this.

Creating a Content Builder in Craft CMS

Using the Matrix block to cre­ate a con­tent builder” for your client is much bet­ter than just giv­ing them a Rich Text field

When a web­site is cre­at­ed, typ­i­cal­ly a graph­ic design­er cre­ates the look, and a fron­tend devel­op­er imple­ments it. There may be a few oth­er roles mixed in, such as peo­ple who do mar­ket­ing, SEO, UX, etc., but that’s the basic flow.

What’s miss­ing from this equa­tion is the client. With­out the con­tent from the client, the web­site is essen­tial­ly an emp­ty shell. The client needs to be able to cre­ate and mod­i­fy con­tent to suit their needs; that’s the entire point of using a Con­tent Man­age­ment Sys­tem (CMS) to begin with.

So how do we pro­vide the client with a good con­tent author­ing expe­ri­ence? In the past, the Rich Text field has been used as a way to pro­vide the client with the abil­i­ty to enter the for­mat­ted text and images that they might want on the page.

The prob­lem is that it pro­vides a hor­ri­ble con­tent author­ing expe­ri­ence, from the point of view that the client can end up mak­ing 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 talk­ing to a fron­tend devel­op­er, and they roll their eyes when speak­ing about what the client did to the site”. Well, no. You gave them the pow­er to do it, it’s your fault for not design­ing the con­tent entry in a way that does­n’t allow the client to screw it up”.

The peo­ple we build web­sites for may or may not be adept at tech­nol­o­gy or design, but it’s very like­ly they are quite good at their jobs. It’s some­what unfair to blame them for what they end up doing if they aren’t giv­en appro­pri­ate guid­ance and constraints.

Instead of a free-for-all Rich Text field, a bet­ter way is to cre­ate a con­tent builder” for your clients to use, with Craft CMS’s Matrix Block as a basis.

I was­n’t plan­ning to write this arti­cle, since there are a num­ber of good resources avail­able already on the Matrix Block as a con­tent builder, such as Craft CMS Con­tent Builder: The Client Expe­ri­ence and Matrix as a Lay­out Builder.

How­ev­er, I had sev­er­al peo­ple ask me about it recent­ly, so I fig­ured it could­n’t hurt to toss my hat into the ring. Plus, I think there is some­thing com­pelling about using this blog in a self-ref­er­en­tial way to show how this blog is built. So let’s get to it!

Link Enter the Matrix

The gen­er­al idea behind using the Matrix as a con­tent builder is that we cre­ate Block Types for each type of con­tent that the client might need to enter. This allows our design­ers to design how these blocks should look, and it gives the client a frame­work that they can use to pop­u­late them with­out going off into the weeds.

We are essen­tial­ly using our exper­tise to pro­vide a nice­ly designed box that the client can put the con­tent that makes their web­site unique into. We aren’t eschew­ing the Rich Text field entire­ly, but rather we’re pro­vid­ing it in a con­strained 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 con­tent builder used to cre­ate the blog you’re read­ing right now is a sim­ple one. But I think that also makes it good for illus­tra­tion pur­pos­es, so that the core prin­ci­ples are explained clear­ly. You do can far, far more with Matrix as a con­tent 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 want­ed to be able to have:

  • Rich Text blocks with for­mat­ting con­trols, and option­al titles
  • Image blocks with a few dif­fer­ent types of images
  • Quote blocks for pre­sent­ing quotes or ideas that I feel are impor­tant (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 tem­plate side of things, we have some arti­cle head­er infor­ma­tion, but the guts of the matrix block dis­play 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 iter­at­ing through all of the blogContent Matrix blocks, and using the Twig include direc­tive to bring in a tem­plate based on the block.type.handle of the Matrix block. Our tem­plates look like this:

Con­tent Builder Twig Templates

The blockPrefix is used so that I can have the same code pat­terns for the Google AMP ver­sion of the blog con­tent builder; the Twig tem­plate par­tials are just pre­fixed with _amp. The lazyImage vari­able is used so that the first image block we present is notlazy loaded image, but every­thing after it is.

I could have used ignore missing in the include direc­tive to tell it to grace­ful­ly fail if the tem­plate it’s try­ing to include does­n’t exist, but I’d rather have it throw a Twig error in this case, rather than have it silent­ly fail. It’s also pos­si­ble to have include use a default tem­plate if the tem­plate it’s look­ing for is miss­ing; use what­ev­er pat­tern works best for you.

The cool thing about a set­up like this is that if you want to add a new block type, you just cre­ate it in the AdminCP, and then cre­ate the appro­pri­ate­ly named Twig tem­plate par­tial, and away you go. Every­thing has a neat 1:1 cor­re­spon­dence, and is nice­ly mod­u­lar­ized. DRY, as the cool kids call it.

Link Show Me the Blocks!

So now that you’ve seen how we include these Twig tem­plate par­tials, let’s take a clos­er look at how they are built.

Image Block

{% 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 lit­tle crazy, it’s real­ly quite sim­ple. We have 3 dif­fer­ent types of images defined:

  • normal — Dis­plays the image inside of our con­tent div (with opti­mized respon­sive sizes)
  • fullWidth — Dis­plays the image at full bleed (with opti­mized respon­sive sizes)
  • infographic — Dis­plays the image entire­ly unaltered

If we want­ed to add more image types, or more con­trol over how the images are dis­played or what have you, it would be pret­ty easy to add them.

Text Block

<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 ren­ders out our Rich Text blocks (for which I pro­vide a lim­it­ed sub­set of edi­tor capa­bil­i­ties, to keep it from get­ting messy), with an option­al headline.

We use the fan­tas­tic Typogri­fy plu­g­in to ensure that we get nice curly quotes, no wid­ows, and oth­er typo­graph­i­cal fea­tures auto­mat­i­cal­ly, with­out the client hav­ing to know how to do them!

Quote Block

<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.

Code Block

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

This is the code I use to, well, present code! We’re using the awe­some Pris­mJS to dis­play it nice­ly for­mat­ted. I build the prism.min.js myself via Gulp to ensure it includes the lan­guages that I want syn­tax highlighted.

Link It’s a Wrap!

And that, as they say, is that! It’s a pret­ty sim­ple set­up, but hope­ful­ly some­one finds it use­ful as some­thing to con­sid­er in lieu of just slap­ping a Rich Text field into their pages, and wish­ing their client good luck!”

If noth­ing else, it’ll make your graph­ic design­ers hap­py, and increase the odds that when you look at the web­site a year or two after deliv­er­ing it to the client, that it has­n’t become an eye­sore that you’re ten­ta­tive about link­ing to from your Our Clients” page.