Andrew Welch · Insights · #craftcms #twig #frontend

Published , updated · 5 min read ·


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

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

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.

Think of a templating language as the thin layer of frosting that covers the layer cake that is your website, and makes it presentable.

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.

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

Many high level frameworks are either over-engineered & unwieldy or so specific they aren’t that reusable in the end anyway.

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

Link SEO & popup 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 AJAXXHR.

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

Sea­side Farmer’s Mar­ket pop­up 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

Sea­side Farmer’s Mar­ket web page

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.

Link The overall 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

Twig Base Tem­plates Diagram

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.

You do not have to use the exact Twig base templating setup we use; but you may benefit from taking away and using the concepts presented

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…

Link 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

global-variables.twig blocks

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

Link BOILERPLATE: 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

base-web-layout.twig blocks

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.

Link BOILERPLATE: 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

base-ajax-layout.twig blocks

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

Link BOILERPLATE: 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

base-html-layout.twig blocks

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.

Link BOILERPLATE: 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

amp-base-html-layout.twig blocks

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.

Link 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

generic-page-layout.twig blocks

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

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.

Link 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

error-page-layout.twig blocks

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

Link 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

amp-generic-page-layout.twig blocks

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

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

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