DEV Community

loading...

An Effective Twig Base Templating Setup

gaijinity profile image Andrew Welch Originally published at nystudio107.com on ・17 min read

An Effective Twig Base Templating Setup

A good base tem­plat­ing set­up for your Craft CMS Twig tem­plates pro­vides a sta­ble, sol­id foun­da­tion on which to build your projects

Andrew Welch / nystudio107

Solid twig templating layer

Twig is a fan­tas­tic tem­plat­ing lan­guage that fea­tures mul­ti­ple inher­i­tance of lay­out tem­plates, and is opti­mized to be an easy to use pre­sen­ta­tion layer.

This arti­cle dis­cuss­es an effec­tive Twig base tem­plat­ing set­up that I have found to work extreme­ly well for me in my Craft CMS websites.

How­ev­er, even if you use anoth­er CMS that uses Twig like Dru­pal or Grav, or you use anoth­er tem­plat­ing lan­guage entire­ly like Blade or Antlers, the prin­ci­ples dis­cussed here still apply.

The key thing to note here is that Twig is a tem­plat­ing lan­guage, and as such it should not be used for com­pli­cat­ed busi­ness or inten­sive calculations.

Not that it can’t han­dle either (it can) but rather that it shouldn’t.

If you’re unclear as to why, read about why Twig was cre­at­ed to begin with in the Tem­plat­ing Engines in PHP article.

It’s all about that base

Over the years, on a vast array of soft­ware projects of all shapes and sizes, I’ve seen devel­op­ers chas­ing the holy grail of code reusability.

Often times it ends up being that they spend an inor­di­nate amount of time cre­at­ing ​“the one high lev­el frame­work to rule them all”, only to be con­fused when real­i­ty butts in its ugly head.

I try to be more prac­ti­cal about which things I will actu­al­ly re-use (and some would say less ambitious).

Web­sites cre­at­ed in Craft CMS tend to be more on the bespoke side of things, oth­er­wise they might be bet­ter done in a more cook­ie-cut­ter sys­tem anyway.

So what I re-use are very fun­da­men­tal things like the build sys­tem (dis­cussed in the An Anno­tat­ed web­pack 4 Con­fig for Fron­tend Web Devel­op­ment arti­cle), and a base tem­plat­ing system.

If you’re going to build any­thing of sub­stance, it’s cru­cial that the base it’s built on is robust.

Human tower base

So here’s what I want out of a base tem­plat­ing system:

  1. The abil­i­ty to use it unmod­i­fied on a wide vari­ety of projects
  2. One tem­plate that can be used both as a web page, and as pop­up modals via AJAX / XHR
  3. Imple­ment core fea­tures for me, with­out restrict­ing me in terms of flexibility
  4. Allow for cre­at­ing Google AMP pages, if the project war­rants it

Often I see devel­op­ers mak­ing tem­plates that inher­it from just one lay­out, or if they use mul­ti­ple lay­outs, it’s still a sin­gle inher­i­tance chain.

Twig allows for more than that. So let’s see how one approach to lever­age this might work.

SEO & pop­up modals

Many of the points men­tioned in the pre­vi­ous sec­tion are large­ly self-explana­to­ry, but point 2 deserves more expla­na­tion. It’s all about tem­plates work­ing both as web pages and pop­up modals loaded in via AJAX / XHR.

I fre­quent­ly work with Jonathan Melville of Code MDD on projects, and he often does designs that have con­tent in pop­up modals.

For exam­ple, if you go to the Sea­side Events page you’ll see a num­ber of events list­ed, and if you click on an event, you’ll see the event details in a pop­up modal:

Seaside farmers market popup modal

This is great, and gives it a nice app-ish feel, allow­ing the user to view mul­ti­ple events with­out leav­ing the orig­i­nal page.

But for SEO rea­sons, as well as for canon­i­cal page link­ing rea­sons, the same con­tent can also be found on its own unique page: Sea­side Farmer’s Mar­ket — Sat­ur­days in Novem­ber:

Seaside farmers market webpage

This is ide­al­ly what we want to be able to do auto­mat­i­cal­ly: have the same core con­tent be dis­playable both with and with­out the web page ​“chrome” around it.

And this is one of the things that the base tem­plat­ing set­up does.

The over­all structure

Here’s an overview of what this base tem­plat­ing sys­tem looks like. It may seem involved, but we’ll break it down:

Twig base templates diagram 2x

The orange round­ed rec­tan­gles rep­re­sent tem­plates that will be in your templates/_layouts/ direc­to­ry, and may vary from project to project.

The blue rec­tan­gles rep­re­sent boil­er­plate tem­plates that will be in your templates/_boilerplate/_layouts/ direc­to­ry, and won’t change from project to project.

If at this point you’re some­one who learns bet­ter by real-world exam­ples, the exact base tem­plat­ing sys­tem described here is used in the MIT-licensed dev​Mode​.fm web­site Github repo.

Feel free to check it out; it’s also used in the nystudio107/​craft boil­er­plate set­up.

Mean­while, every­one else, read on! We’re going to break down each template.

PROJECT: will pre­fix each tem­plate that may vary from project to project

BOIL­ER­PLATE: will pre­fix each tem­plate that stays the same from project to project

Here we go…

PROJECT: global-variables.twig

Due to Twig’s Pro­cess­ing Order & Scope, if we want to have glob­al vari­ables that are always avail­able in all of our tem­plates, they need to be defined in the root tem­plate that all oth­ers extends from.

Since these glob­als can vary from project to project, they are not part of the boil­er­plate, but they are required for the setup.


{# -- Root global variables that all templates inherit from -- #}
{# -- This allows for defining site-wide Twig variables as needed -- #}
{% spaceless %}

{# -- Prefetch & preconnect headers and links -- #}
{% set prefetchUrls = [
    alias("@assetsUrl"),
] %}
{# -- General global variables -- #}
{% set baseUrl = alias('@assetsUrl') ~ '/' %}
{% set gaTrackingId = getenv('GA_TRACKING_ID') %}

{# -- Twig output from the render; this must be in a block -- #}
{% block htmlPage %}
{% endblock %}

{% endspaceless %}

Blocks global variables

The global-variables.twig tem­plate has the fol­low­ing blocks that can be over­rid­den by its children:

  • htmlPage — a block that encom­pass­es the entire ren­dered HTML page

BOIL­ER­PLATE: base-web-layout.twig

Every web­page, whether a reg­u­lar web page or a Google AMP page inher­its from this tem­plate. The set­up may look a lit­tle weird, but it’s done this way so that child tem­plates can over­ride bits like the open­ing <html> tag if they need to:


{# -- Base web layout template that all web requests inherit from -- #}
{% extends "_layouts/global-variables.twig" %}

{%- block htmlPage -%}
    {% minify %}
    <!DOCTYPE html>
        {% block htmlTag %}
            <html lang="{{ craft.app.language |slice(0,2) }}">
        {% endblock htmlTag %}
        {% block headTag %}
            <head>
        {% endblock headTag %}
            {% include "_boilerplate/_partials/head-meta.twig" %}
            {# -- Page content that should be included in the <head> -- #}
            {% block headContent %}
            {% endblock headContent %}
            </head>

            {% block bodyTag %}
            <body>
            {% endblock bodyTag %}
                {# -- Page content that should be included in the <body> -- #}
                {% block bodyContent %}
                {% endblock bodyContent %}
            </body>
        </html>
    {% endminify %}
{%- endblock htmlPage -%}

Since this is a base tem­plate that all oth­er web pages inher­it from, if we want­ed to do full page caching using the Craft Cache tag, we could wrap that around the {% minify %} tags here.

Blocks base web layout

The base-web-layout.twig tem­plate has the fol­low­ing blocks that can be over­rid­den by its children:

  • htmlTag — the <html> tag, which child tem­plates might need to override
  • headTag — the <head> tag, which child tem­plates might need to override
  • headContent — what­ev­er tags need to go into the <head>
  • bodyTag — the <body> tag, which child tem­plates might need to override
  • bodyContent — what­ev­er tags need to go in the <body>

In addi­tion, the _boilerplate/_partials/head-meta.twig par­tial that con­tains boil­er­plate tags put into the <head> is includ­ed here as well.

BOIL­ER­PLATE: base-ajax-layout.twig

If the request is an AJAX / XHR request, we want to return just the page’s {% content %} block, with­out any of the web page ​“chrome” around it.

This is exact­ly what this tem­plate does:


{# -- Base layout template that all AJAX requests inherit from -- #}
{% extends "_layouts/global-variables.twig" %}

{% block htmlPage %}
    {% minify %}
        {# -- Primary content block -- #}
        {% block content %}
            <code>No content block defined.</code>
        {% endblock content %}
    {% endminify %}
{% endblock htmlPage %}

Blocks base ajax layout

The base-ajax-layout.twig tem­plate has the fol­low­ing blocks that can be over­rid­den by its children:

  • content — the core con­tent that is rep­re­sent­ed on the page

BOIL­ER­PLATE: base-html-layout.twig

This is the base HTML lay­out that all HTML requests inher­it from:


{# -- Base HTML layout template that all HTML requests inherit from -- #}
{% extends craft.app.request.isAjax() and not craft.app.request.getIsPreview()
    ? "_boilerplate/_layouts/base-ajax-layout.twig"
    : "_boilerplate/_layouts/base-web-layout.twig"
%}

{% block htmlTag %}
    <html class="fonts-loaded" lang="{{ craft.app.language |slice(0,2) }}" prefix="og: http://ogp.me/ns# fb: http://ogp.me/ns/fb#">
{% endblock htmlTag %}

{# -- Page content that should be included in the <head> -- #}
{% block headContent %}
    {# -- Any <meta> tags that should be included in the <head> #}
    {% block headMeta %}
    {% endblock headMeta %}

    {# -- Any <link> tags that should be included in the <head> #}
    {% block headLinks %}
    {% endblock headLinks %}

    {# -- Inline and polyfill JS #}
    {% include "_boilerplate/_partials/head-js.twig" %}

    {# -- Any JavaScript that should be included before </head> -- #}
    {% block headJs %}
    {% endblock headJs %}

    {# -- Inline and critical CSS #}
    <style>
        [v-cloak] {display: none !important;}
        {# -- Any CSS that should be included before </head> -- #}
        {% block headCss %}
        {% endblock headCss %}
    </style>
    {% include "_boilerplate/_partials/critical-css.twig" %}

{% endblock headContent %}

{# -- Page content that should be included in the <body> -- #}
{% block bodyContent %}
    {# -- Page content that should be included in the <body> -- #}
    {% block bodyHtml %}
    {% endblock bodyHtml %}

    {#-- Site-wide JavaScript --#}
    {{ craft.twigpack.includeSafariNomoduleFix() }}
    {{ craft.twigpack.includeJsModule("app.js", true) }}
    {{ craft.twigpack.includeJsModule("styles.js", true) }}

    {# -- Any JavaScript that should be included before </body> -- #}
    {% block bodyJs %}
    {% endblock bodyJs %}
{% endblock bodyContent %}

Blocks base html layout

The base-html-layout.twig tem­plate has the fol­low­ing blocks that can be over­rid­den by its children:

  • headMeta — Any <meta> tags that should be includ­ed in the <head>
  • headLinks — Any <link> tags that should be includ­ed in the <head>
  • headJs — Any JavaScript that should be includ­ed before </head>
  • headCss — Any CSS that should be includ­ed before </head>
  • bodyHtml — Page con­tent that should be includ­ed in the <body>
  • bodyJs — Any JavaScript that should be includ­ed before </body>

In addi­tion, the _boilerplate/_partials/head-js.twig & _boilerplate/_partials/critical-css.twig boil­er­plate par­tials are includ­ed here as well.

BOIL­ER­PLATE: amp-base-html-layout.twig

This is the base AMP HTML lay­out that all AMP HTML requests inher­it from:


{# -- Base AMP HTML layout template that AMP web requests inherit from -- #}
{% extends craft.app.request.isAjax() and not craft.app.request.getIsPreview()
    ? "_boilerplate/_layouts/base-ajax-layout.twig"
    : "_boilerplate/_layouts/base-web-layout.twig"
%}

{% do seomatic.script.container().include(false) %}
{% do craft.webperf.includeBeacon(false) %}

{% block htmlTag %}
    <html ⚡ lang="{{ craft.app.language |slice(0,2) }}" class="fonts-loaded">
{% endblock htmlTag %}

{# -- Page content that should be included in the <head> -- #}
{% block headContent %}
    {# -- Any <meta> tags that should be included in the <head> #}
    {% block headMeta %}
    {% endblock headMeta %}

    {# -- Any <link> tags that should be included in the <head> #}
    {% block headLinks %}
    {% endblock headLinks %}

    {# -- Google AMP JavaScripts #}
    {% include "_boilerplate/_partials/amp-head-js.twig" %}

    {# -- Any JavaScript that should be included before </head> -- #}
    {% block headJs %}
    {% endblock headJs %}

    {# -- Boilerplate & custom AMP CSS #}
    {% include "_boilerplate/_partials/amp-boilerplate-css.twig" %}
    <style amp-custom>
    {# -- Any CSS that should be included before </head> -- #}
    {% block headCss %}
    {% endblock headCss %}
    </style>
{% endblock headContent %}

{# -- Page content that should be included in the <body> -- #}
{% block bodyContent %}
    {# -- Page content that should be included in the <body> -- #}
    {% block bodyHtml %}
    {% endblock bodyHtml %}

    {# -- AMP Analytics --#}
    {% include "_boilerplate/_partials/amp-analytics.twig" %}

    {# -- Any JavaScript that should be included before </body> -- #}
    {% block bodyJs %}
    {% endblock bodyJs %}
{% endblock bodyContent %}

Blocks amp base html layout

The amp-base-html-layout.twig tem­plate has the fol­low­ing blocks that can be over­rid­den by its children:

  • headMeta — Any <meta> tags that should be includ­ed in the <head>
  • headLinks — Any <link> tags that should be includ­ed in the <head>
  • headJs — Any JavaScript that should be includ­ed before </head>
  • headCss — Any CSS that should be includ­ed before </head>
  • bodyHtml — Page con­tent that should be includ­ed in the <body>
  • bodyJs — Any JavaScript that should be includ­ed before </body>

N.B.: these blocks are all pur­pose­ful­ly the same as the ones used in the base-html-layout.twig template.

In addi­tion, the _boilerplate/_partials/amp-head-js.twig, _boilerplate/_partials/amp-boilerplate-css.twig & amp-analytics.twig boil­er­plate par­tials are includ­ed here as well.

PROJECT: generic-page-layout.twig

This is a gener­ic page lay­out that I’ve found suits most of the projects I build, and my oth­er tem­plates extends it.

For sim­i­lar pages, I can even extend this lay­out to get every­thing it offers, plus what I need for anoth­er sub­set of pages. For exam­ple, see the generic-page-layout.twig below.

How­ev­er, if I have oth­er pages that require rad­i­cal­ly dif­fer­ent lay­outs, I’ll just cre­ate anoth­er lay­out tem­plate that extends _boilerplate/_layouts/base-html-layout.twig and away we go!


{# -- Layout template for HTML pages -- #}
{% extends "_boilerplate/_layouts/base-html-layout.twig" %}

{# -- Any <meta> tags that should be included in the <head> #}
{% block headMeta %}
{% endblock headMeta %}

{# -- Any <link> tags that should be included in the <head> #}
{% block headLinks %}
{% endblock headLinks %}

{# -- Any CSS that should be included before </head> -- #}
{% block headCss %}
    {% include "_inline-css/site-fonts.css" %}
{% endblock headCss %}

{# -- Page body -- #}
{% block bodyHtml %}
    <div id="page-container" class="overflow-hidden leading-tight">
        <confetti></confetti>
        <div id="content-container" class="bg-repeat header-background">

            {# -- Info header, including _navbar.twig -- #}
            {% include "_partials/info-header.twig" %}

            <main>
                <div class="container mx-auto pb-8">
                    {# -- Primary content block -- #}
                    {% block content %}
                    {% endblock %}
                </div>
            </main>
        </div>

        {# -- Content that appears below the primary content block -- #}
        {% block subcontent %}
        {% endblock %}

        {# -- Info footer -- #}
        {% include "_partials/info-footer.twig" %}

        {# -- HTML Footer -- #}
        {% include "_partials/global-footer.twig" %}
    </div>
{% endblock bodyHtml %}

Blocks generic page layout

The generic-page-layout.twig tem­plate has the fol­low­ing blocks that can be over­rid­den by its children:

  • content — Pri­ma­ry con­tent block
  • subContent — Con­tent that appears below the pri­ma­ry con­tent block

Markup in the subContent block will appear on web pages, but not on pages loaded in via AJAX / XHR.

In addi­tion, it includes a few par­tials for the head­er, foot­er, etc., but you can have it do what­ev­er makes the most sense to you.

PROJECT: error-page-layout.twig

Here we fur­ther extends the generic-page-layout.twig with anoth­er lay­out tem­plate that’s specif­i­cal­ly intend­ed for error pages.

Because we have a num­ber of dif­fer­ent error pages that dis­play dif­fer­ent con­tent, but have the same basic lay­out, this is the per­fect oppor­tu­ni­ty to con­sol­i­date them in anoth­er lay­out template.

Instead of repli­cat­ing the con­tent for each error page, we can have the error pages extends error-page-layout.twig and have very light­weight error pages.

The same idea of an inher­i­tance chain can be used in sim­i­lar situations.


{# -- Layout template for error pages -- #}
{% extends "_layouts/generic-page-layout.twig" %}

{% block content %}
{% endblock %}

{% block subcontent %}
    <section>
        <div class="container mx-auto py-8">
            <div class="text-center p-8 mb-8">
                <h1 class="font-mono italic font-bold text-5xl pt-4">
                    {{ entry.errorHeadline ?? 'Error' }}
                </h1>
                <p class="font-sans text-xl pt-4">
                    {{ (entry.errorText ?? 'An error has occurred.') |nl2br }}
                </p>
            </div>
        </div>
    </section>

{% endblock %}

{# -- Any JavaScript that should be included before </body> -- #}
{% block bodyJs %}
{% endblock bodyJs %}

Blocks generic page layout

The error-page-layout.twig tem­plate has the fol­low­ing blocks that can be over­rid­den by its children:

  • content — Pri­ma­ry con­tent block
  • subContent — Con­tent that appears below the pri­ma­ry con­tent block

Markup in the subContent block will appear on web pages, but not on pages loaded in via AJAX / XHR.

PROJECT: amp-generic-page-layout.twig

This is the Google AMP gener­ic page tem­plate, which mir­rors the blocks and method­ol­o­gy from the generic-page-layout.twig tem­plate, but is sep­a­rat­ed out to allow for the unique tags that Google AMP requires:


{# -- Layout template for AMP HTML pages -- #}
{% extends "_boilerplate/_layouts/amp-base-html-layout.twig" %}

{# -- Any <meta> tags that should be included in the <head> #}
{% block headMeta %}
{% endblock headMeta %}

{# -- Any <link> tags that should be included in the <head> #}
{% block headLinks %}
{% endblock headLinks %}

{# -- Any JavaScript that should be included before </head> -- #}
{% block headJs %}
{% endblock headJs %}

{# -- Any CSS that should be included before </head> -- #}
{% block headCss %}
    {% include "_partials/amp-inline-css.css" %}
    {% include "_inline-css/site-fonts.css" %}
{% endblock %}

{# -- Page body -- #}
{% block bodyHtml %}
    {% include "_partials/amp-navbar.twig" %}
    <div id="page-container" class="overflow-hidden leading-tight">
        <div id="content-container" class="bg-repeat header-background">
            {# -- Info header, including _navbar.twig -- #}
            {% include "_partials/amp-info-header.twig" %}

            <main>
                <div class="container mx-auto pb-8">
                    {# -- Primary content block -- #}
                    {% block content %}
                    {% endblock %}
                </div>
            </main>
        </div>

        {# -- Content that appears below the primary content block -- #}
        {% block subcontent %}
        {% endblock %}

        {# -- Info footer -- #}
        {% include "_partials/amp-info-footer.twig" %}

        {# -- HTML Footer -- #}
        {% include "_partials/global-footer.twig" %}
    </div>
{% endblock bodyHtml %}

Blocks generic page layout

The amp-generic-page-layout.twig tem­plate has the fol­low­ing blocks that can be over­rid­den by its children:

  • content — Pri­ma­ry con­tent block
  • subContent — Con­tent that appears below the pri­ma­ry con­tent block

Markup in the subContent block will appear on web pages, but not on pages loaded in via AJAX / XHR.

dev​Mode​.fm page: a real world example

So how does this all look with a real world exam­ple? Why, I’m glad you asked! Let’s have a look at the dev​Mode​.fm home page, which extends generic-page-layout.twig:


{% extends "_layouts/generic-page-layout.twig" %}

{% set includeAudioMeta = false %}

{% block headLinks %}
    {{ parent() }}
    <link rel="amphtml" href="{{ siteUrl('/amp') }}">
{% endblock headLinks %}

{% block content %}
    {% include "_partials/_meta-schema-radio-series.twig" with {
        "showInfo": showInfo,
    } only %}
    <section>
        <div>
            {% for episode in craft.entries.section("episodes").limit(1).all() %}
                <div class="flex flex-wrap">
                    {% include "episodes/_partials/_display_episode.twig" with {
                        "episode": episode,
                        "showInfo": showInfo,
                        "includeAudioMeta": includeAudioMeta,
                        "autoPlay": false,
                    } only %}
                </div>
            {% endfor %}
        </div>
    </section>
{% endblock %}

{% block subcontent %}
    {% include "episodes/_partials/_display_recent_episodes.twig" with {
        "showInfo": showInfo,
    } only %}
{% endblock %}

{# -- Any JavaScript that should be included before </body> -- #}
{% block bodyJs %}
    {{ craft.twigpack.includeJsModule("player.js", true) }}
    {{ craft.twigpack.includeJsModule("episodes.js", true) }}
{% endblock bodyJs %}

You can com­pare this to the dev​Mode​.fm ren­dered home page.

The index.twig tem­plate over­rides just four blocks:

  • headLinks — Here we add a link to point browsers at the Google AMP ver­sion of this page
  • content — The con­tent of this page, in this case the cur­rent episode sum­ma­ry & audio player
  • subContent — The episodes list­ing com­po­nent, dis­played under the content
  • bodyJs — Adds some JavaScript to han­dle the play­er & episodes list­ing to the page, cour­tesy of Twig­pack

You can see that this makes the actu­al tem­plates that we write pret­ty clean. And if this page was ever request­ed via AJAX / XHR, it’d return just the content block.

The Google AMP ver­sion of the home­page tem­plate is very similar:


{% extends "_layouts/amp-generic-page-layout.twig" %}

{% if entry is not defined %}
    {% set entry = craft.entries({
        "uri": " __home__",
    }).one() %}
{% endif %}

{% do seomatic.helper.loadMetadataForUri(entry.uri) %}
{% do seomatic.script.container().include(false) %}

{% block headCss %}
    {{ parent() }}
    {{ craft.twigpack.includeFile("@webroot/dist/criticalcss/amp_index_critical.min.css") }}
{% endblock headCss %}

{% block content %}
    <section>
        <div>
            {% for episode in craft.entries.section("episodes").limit(1).all() %}
                <div class="flex flex-wrap">
                    {% include "episodes/_partials/_amp_display_episode.twig" with {
                        "episode": episode,
                        "showInfo": showInfo,
                    } only %}
                </div>
            {% endfor %}
        </div>
    </section>
{% endblock %}

{% block subcontent %}
    {% include "episodes/_partials/_amp_display_recent_episodes.twig" with {
        "showInfo": showInfo,
    } only %}
{% endblock %}

{% block bodyJs %}
{% endblock bodyJs %}

It’s explic­it­ly load­ing the appro­pri­ate entry, because it won’t be auto-inject­ed for us by Craft, and then it loads the appro­pri­ate meta­da­ta for the route via seomatic.helper.loadMetadataForUri() and excludes all scripts via seomatic.script.container().include(false) because Google AMP does­n’t allow for them.

It’s also using Twig­pack to include the full CSS for the page inline (as per Google AMP spec) but oth­er than that… it’s the same as the reg­u­lar web page example.

All about that Bass

While you cer­tain­ly could just start using my boil­er­plate, odds are good you’ll want to cus­tomize some things to suit your tastes.

That’s total­ly fine. What’s impor­tant is the struc­ture and method­ol­o­gy, not the spe­cif­ic imple­men­ta­tion details.

The point of a mod­u­lar­ized sys­tem like this is that if you want­ed to add, say, a way to out­put the same con­tent in JSON for­mat, you could. Just slap in anoth­er lay­out in the right place, and away you go.

Enjoy the oblig­a­tory ​“All About That Bass” and have an excel­lent day!

Links:

Further Reading

If you want to be notified about new articles, follow nystudio107 on Twitter.

Copyright ©2020 nystudio107. Designed by nystudio107

Discussion (0)

pic
Editor guide