Craft CMS Content Builder: The Client Experience

A Customizable Blog Solution

Most clients want a blog or some similar component of their website that lets them add content on a periodic basis. Unfortunately most blogs are bland and don’t have any life to them, with design patterns that are simple and restricted to a basic container. They’re mostly just words on a page with a picture here and there. On a lot of sites that’s perfectly okay. But others need some extra love to make the blog much more effective.

When we designed choosesantacruz.com, the Economic Development Office site for the City of Santa Cruz, we needed to come up with a solution for an effective and engaging blog. For the user, the front-end needed character that matched other pages of the site. And for the client, we needed to create something that allowed for a lot of customization while making it easy for them to stay within our design guidelines. Lastly, it needed to be intuitive to use.

The Content Builder

Powered by Craft’s Matrix Field we created the Content Builder. The Content Builder consists of a variable amount of matrix field block types. The actual amount depends on the site and how many different design patterns are needed. For choosesantacruz.com there are 14 different block types, everything from a simple copy block, to a more complex blockquote over a fullbleed background image. We also included options like overlay color & opacity, a photo grid, and even a Pardot form block to embed a Pardot form on the page.

Screenshot from incredibly helpful Blueprint plugin page in the CMS.

As the client builds their page, they’re able to select block types from three different categories – Copy, Images, and Other.

Here’s what a blank slate of the Content Builder looks like for the client when creating an entry:

The Matrix Field block type group organization is courtesy of the awesome Pimp My Matrix plugin, which looks like this:

Thanks to Craft’s Live Preview feature, we can use the Content Builder while watching the page come together, and test different layouts as we add the content.

Live Preview in action

A huge factor is making this intuitive so that the client actually uses the different options. To accomplish this, we included in-depth instructions so the client knows exactly what to change in order to make an element look a certain way.

The Code

When we first thought of the Content Builder we wanted to make it into a plugin for Craft. But the more we thought about it, the less it made sense, because each site will have a completely different set of design patterns. And those variations sometimes come with vastly different HTML, CSS, and JavaScript.

So instead of a plugin, the Content Builder is more of a philosophy; an idea. It’s taking existing patterns and features that already exist across the site, as well as some new ones, and putting them all together in a single field.

One thing that stays fairly consistent across each site will be the basic Twig {% switch %} statement that handles the different content blocks. 

How We Did It

We keep the Content Builder code in a partial Twig file, coincidentally named content-builder.twig.

So in our entry template file that has the Content Builder we use a line like this to include the Content Builder partial:

{% include '_partials/content-builder' %}

A super slimmed-down version of content-builder.twig with no HTML structure would look like this:

{% include '_partials/content-builder' %}

A super slimmed-down version of content-builder.twig with no HTML structure would look like this:

{% if entry.contentBuilder|length %}

  {% for content in entry.contentBuilder %}

    {% switch content.type %}
      
      {% case 'copy' %}

        {{ content.copy }}

      {% case 'fullbleedImage' %}
        
        <img src="{{ content.image.first.url }}" alt="{{ content.image.first.title }}" style="width: 100%;">
      
      {% case 'video' %}
        
        {{ content.video }}

      {# ...continues checking all the contentBuilder block types. Don't have a default switch. #}

    {% endswitch %}

  {% endfor %}

{% endif %}

While a full version of content-builder.twig may look like this:

{% if entry.contentBuilder|length %}

  <section class="js-content-builder content-builder">

    {% for content in entry.contentBuilder %}

      {% switch content.type %}
        {% case 'copy' %}

          <div class="builder-{{ content.type }} builder-section container tight">
            
            {{ content.copy }}

          </div>

        {% case 'copyOnFullbleedImage' %}
          
          {# content.sectionHeight is meant to override the default height set in the CSS of the content block type #}
          <div class="builder-{{ content.type }} builder-section {{ content.sectionHeight }}">
            
            <div class="bg-fullbleed" style="background-image: url({{ content.image.first.url }});"></div>

            {# Set background overlay variables #}
            {% set overlayColor = content.overlayColor %}
            {% set overlayOpacity = content.overlayOpacity %}
            {% set overlayResult = overlayColor ~ ', ' ~ overlayOpacity %}

            <div class="bg-overlay" style="background-color: rgba({{ overlayResult }});"></div>
            
            {# inner-container is set to display: table-cell; height: 100%; while it's parent is set to display: table; #}
            <div class="inner-container" style="vertical-align: {{ content.copyVerticalAlignment }};">
              <div class="container tight" style="color: #fff;"> {# Temp inline styles #}

                  {{ content.copy }}

              </div>
            </div>

          </div>

        {% case 'blockquote' %}

          <div class="builder-{{ content.type }} builder-section container tight">

            <blockquote>
              <p>{{ content.quote }} {% if content.quoter|length %} &mdash; <strong>{{ content.quoter }}</strong>{% endif %}</p>
            </blockquote>

          </div>

        {% case 'blockquoteOnFullbleedImage' %}

          <div class="builder-{{ content.type }} {{ content.sectionHeight }} builder-section">
            
            <div class="bg-fullbleed" style="background-image: url({{ content.image.first.url }});"></div>

            {# Set background overlay variables #}
            {% set overlayColor = content.overlayColor %}
            {% set overlayOpacity = content.overlayOpacity %}
            {% set overlayResult = overlayColor ~ ', ' ~ overlayOpacity %}

            <div class="bg-overlay" style="background-color: rgba({{ overlayResult }});"></div>

            <div class="inner-container" style="vertical-align: {{ content.copyVerticalAlignment }};">
              <div class="container tight" style="color: #fff;">

                <blockquote>
                  <p>{{ content.quote }} {% if content.quoter|length %} &mdash; {{ content.quoter }}{% endif %}</p>
                </blockquote>

              </div>
            </div>

          </div>

        {% case 'internalBreakoutLink' %}

          <div class="builder-{{ content.type }} builder-section container tight">
            
            {% for link in content.entry %}
              <p><strong>{{ content.accompanyingText|default('Read More') }}:</strong> <em><a href="{{ link.url }}">{{ link.title }}</a></em></p>
            {% endfor %}

          </div>

        {% case 'externalBreakoutLink' %}

          <div class="builder-{{ content.type }} builder-section container tight">
            
            <p><strong>{{ content.accompanyingText|default('Read More') }}:</strong> <em><a href="{{ content.externalUrl }}" target="_blank">{{ content.linkTitle }}</a></em></p>

          </div>

        {% case 'largeImage' %}

          <div class="builder-{{ content.type }} builder-section container tightish">
            
            <figure>
              <img src="{{ content.image.first.url }}" alt="{{ content.image.first.title }}">
            </figure>

          </div>

        {% case 'imageGallery' %}

          <div class="builder-{{ content.type }} builder-section container tight">
            
            <div class="my-simple-gallery">
              {% for image in content.images %}
              
                <figure class="builder-gallery-thumbnail{% if not loop.first %} hide-me{% endif %}">
                  <a class="js-builder-gallery" href="{{ image.getUrl }}" aria-label="Open Photo Gallery">
                    <img src="{{ image.getUrl() }}" alt="{{ image.title }}">
                  </a>
                </figure>
              {% endfor %}
            </div>

            <p class="builder-gallery-open-title">Click on photo to view gallery.</p>

            {# 
              This will need JS. PhotoSwipe is cool. 
              In the figure class attribute I'm adding a class to hide all the images except the first one.
              This way the first image serves as a trigger to open the gallery if using something like PhotoSwipe.
            #}

          </div>

        {% case 'fullbleedImage' %}

          <div class="builder-{{ content.type }} builder-section">

            <figure>
              {# Width 100% forces the image to fill up the width of whatever element it is in. Suggest not putting in a container/wrapper. #}
              <img src="{{ content.image.first.url }}" alt="{{ content.image.first.title }}" style="width: 100%;">
            </figure>

          </div>

        {% case 'video' %}

          <div class="builder-{{ content.type }} builder-section container tightish">
            
            {# Ensure you setup somthing like FitVids for responsive video embeds with Vimeo & YouTube #}
            {{ content.video }}

          </div>

        {% case 'codeSnippet' %}

          <div class="builder-{{ content.type }} builder-section container">
            
            {# 
              Used for tracking codes or scripts that embed stuff. 
              Removing '|raw' will make the code show on the page and will not run.
            #}
            {{ content.code|raw }}

          </div>

      {% endswitch %}

    {% endfor %}

  </section>

{% endif %}

The Content Builder received a great reception from the content producers of choosesantacruz.com. They use it frequently and have tried all of the different options available in the Content Builder. And they've been able to create some awesome pages—check out their Journal.

Ready to learn more about how we can help you make managing your website a breeze? Let's chat!

Let's Chat

We'd love to hear about your goals and how we can help you reach them.

Get Started