Timber and Twig Reignited My Love for WordPress

Avatar of TJ Fogarty
TJ Fogarty on (Updated on )

I’ve spent a good portion of my professional career working with WordPress, and I’ve come to learn a couple of things: Popularity breeds contempt, and to dismiss something based solely on the critiques of others is a missed opportunity.

WordPress and I started out great, but as time went on we became more and more distant. It’s not that we were spending less time together, but it felt like we were just going through the motions. The spark was fading, yet I still knew it was a very important part of what I do. Sometimes change is needed, and that’s when Timber reintroduced us.

I get that WordPress isn’t the perfect tool for everything or everyone, but then what is? There’s a time and a place for it, and I’m hoping to map out some roads you might have missed.

What is Twig?

Twig is a wonderful template language for PHP. Don’t let the name fool you; it’s anything but brittle.

If you’ve ever seen any JavaScript template languages, a-la Handlebars or Moustache, then this will look familiar. If not, don’t fret; Twig is a joy to behold by giving you a concise, accessible syntax to work with. Just have a look this example from their home page:

<?php echo $var ?>
<?php echo htmlspecialchars($var, ENT_QUOTES, 'UTF-8') ?>
{{ var }}
{{ var|escape }}
{{ var|e }}         {# shortcut to escape a variable #}

Tell me that didn’t spark some excitement!

To echo a variable, you just wrap it in double curly braces while omitting the dollar sign e.g. {{ var }}. You can also access the attributes of a variable like so: {{ user.name }}

Control structures appear within {% ... %} blocks, and are just as friendly to wield:

{% for post in posts %}
  {{ post.title }}
{% endfor %}

You can learn more from their documentation.

Imagine writing your WordPress templates like this…

What is Timber?

Timber is the plugin that unites WordPress with Twig. Along with the benefits of using Twig, this lets you separate the PHP from your HTML templates. This in turn give you a more breathable environment for you to develop your theme as we’ll explore in a moment.

Let’s have a look at an example from the documentation, and then we’ll step through it:

$context = Timber::get_context();
$context['post'] = new TimberPost(); // It's a new TimberPost object, but an existing post from WordPress.
Timber::render('single.twig', $context);

The `single.twig` file:

<article>
  <h1 class="headline">{{post.post_title}}</h1>
  <div class="body">
    {{post.content}}
  </div>
</article>

Starting with `single.php`, the first thing we’re doing is fetching the context of the theme with Timber::get_context();. This object will contain such things as your menus, wp_head, and wp_footer. Later on we’ll look at how to add to this if you need anything else globally accessible in your theme. You’ll be using this line a lot.

Next we’re going to need to get the post we want to display in our template. Using new TimberPost(); will figure out which post to fetch.

Finally, we want to display something. Timber’s render function will call on our Twig file and pass through the data we’ve just collected.

One final bombshell, and I hope it’s not too much: Timber comes with baked-in support for Advanced Custom Fields.

Why use Twig?

After a while with WordPress, you might find you can’t see the wood for the trees amidst a confusing dance between PHP and HTML. This isn’t to say all themes are like this, but you can see how easily it can happen.

What I found rather lovely about Timber is that it mitigates this potential problem by creating space between the two. You’re never mingling PHP with HTML, but they’re still talking to each other. You may find the conversations more meaningful as this gives the best parts of WordPress the opportunity to shine through.

For example, our `single.php` file is just 3 lines, and that’s only for dealing with our data. By the time that data reaches our view, we’re happy knowing we have all we need – all that’s left is to mark it up.

Getting Started

Installation

So let’s get stuck in! You can install this via composer if you’re so inclined, but for the sake of brevity I’m going to install it via Plugins > Add New, and do a keyword search for Timber and Twig. Alternatively, you can grab it from the Plugin Directory and upload it to your site.

Starter Theme

Timber ships with a barebones starter theme, which is perfect for hitting the ground running. Once the plugin is installed, you’ll find the following folder which you can copy to your themes folder. This should be located in `wp-content/plugins/timber/timber-starter-theme`. Or, if you just want to have a peek, you can browse the starter theme repository.

You’ll notice on first glance that there’s nothing out of the ordinary going on here. Where’s the magic at? It’s alright to feel apprehensive at this point, so lets dig a little deeper.

functions.php

The purpose of your `functions.php` file will largely remain the same. You can use it how you normally would, be it for declaring custom post types, or defining your custom functions. However, with the Timber Starter Theme, you’ll notice a class declaration awaits inside. The most notable method of this class is add_to_context.

From here we can add items that we want to access throughout our entire theme. Say for example I created a menu within the Admin Dashboard with a slug of primary-menu, I could add it to the global context like so:

$context['primary_menu'] = new TimberMenu('primary-menu');

Now I can access it like this:

<nav role="navigation">
  <ul>
  {% for item in primary_menu.get_items %}
    <li class="{{item.classes | join(' ')}}">
      <a href="{{item.get_link}}">{{item.title}}</a>
    </li>
  {% endfor %}
  </ul>
</nav>

Not too shabby! Another example I’d like to share is when using something like this multi-environment config. This creates a constant for denoting the current environment such as development, staging or production. I can pull this information into my theme easily by adding the following to my add_to_context method:

$context['env'] = WP_ENV;

Within my templates I can call {{ env }} to tell me which environment my site is running on.

Extends and Blocks

Let’s have a look at `index.php`.

$context = Timber::get_context();
$context['posts'] = Timber::get_posts();
$templates = array( 'index.twig' );
if ( is_home() ) {
  array_unshift( $templates, 'home.twig' );
}
Timber::render( $templates, $context );

Ok, so most of this we’ve seen before. There’s a little extra going on here with having an array of templates. All this is doing is adding `home.twig` to the start of the array if is_home() returns true.

Lets say is_home() returns false, so that’ll tell it to render `index.twig`. Twig files live within the `views` directory, so we’ll begin there, in our `index.twig` file:

{% extends "base.twig" %}

{% block content %}
  {% for post in posts %}
    {% include 'tease.twig' %}
  {% endfor %}
{% endblock %}

There’s a couple of new things to introduce here, namely extends and block. If you’re not sure what those do, I’m sure you’ll twig it soon enough.

When we call extends, we’re saying “use this layout for any content that I declare in this template”. Then within `base.twig` you’ll have something like this:

<!doctype html>
<html>
  <head>
  ...
  </head>
  <body>
  ...
  {% block content %}
    No content found!
  {% endblock %}
  </body>
</html>

Usually with WordPress we might just include our footer and header in each template, but here it’s all in the one file which we tell our template to extend.

You can name your blocks whatever you wish. That string of “No content found!” will only display if nothing is supplied in a content block. In the Timber Starter Theme you’ll see they have extra blocks declared for parts of the header and footer, but it’s all about using what makes sense for you.

Custom Template

Enough of this loose talk, it’s time to hand-roll our own template! This walkthrough will make use of Advanced Custom Fields, as well as a few extra features of Timber + Twig that are sure to delight.

We’re going to create a very simple example of a page that has a banner image with some text laid across it. A hero element, if you will. Our hero will reveal the expressiveness of Twig to us, and challenge our mental model of WordPress as we know it. We’ll imagine our hero element has been defined using Advanced Custom Fields. You can call a custom field with the following: post.get_field('field_name');. If you’re using WordPress’ own custom fields then it’s as easy as post.field_name.

There’s a couple of ways to go about creating a custom page template, and because in my case I won’t always know the final URL, I’m going down the “Custom PHP File” route. Finally, let’s start typing things!

base.twig

For our base layout, I’ve combined Timber’s starter theme layout file with HTML5 Boilerplate’s index file:

<!doctype html>
<html class="no-js" {{site.language_attributes}}>
  <head>
    <meta charset="{{site.charset}}">

    <title>   
      {% if wp_title %}
        {{ wp_title }} - {{ site.name }}
      {% else %}
        {{ site.name }}
      {% endif %}
    </title>
    
    <meta name="description" content="{{site.description}}">
    
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <link rel="stylesheet" href="{{ site.theme.link }}/style.css">
  </head>
  <body>
    
    <nav role="navigation">
      <ul>
        {% for item in primary_menu.get_items %}
          <li class="nav-item {{item.classes | join(' ')}}">
            <a class="nav-link" href="{{item.get_link}}">{{item.title}}</a>
          </li>
        {% endfor %}
      </ul>
    </nav>
      
    <div class="wrapper">
      {% block content %}
       No content found!
      {% endblock %}
    </div>

  </body>
</html>

Calling on {{ site.theme.link }} can be pretty useful, and if you’re using Wiredep + Gulp, you can look at this snippet to see how you might blend it into your workflow.

Custom Template Files

As we’ve seen already, your PHP file is going to be simple. A few lines in `your-custom-page.php` should do it:

<?php
/*
 * Template Name: Your Custom Page
 */

$context = Timber::get_context();
$post = new TimberPost();
$context['post'] = $post;
Timber::render( 'your-custom-page.twig', $context );

That’s all we need! Our hero `your-custom-page.twig` file awaits…

{% extends 'base.twig' %}

{% block content %}

<section>
  <h1>{{ post.get_field('hero_title') }}</h1>
  
  <img src="{{ TimberImage(post.get_field('hero_image')).src }}" alt="{{ TimberImage(post.get_field('hero_image')).alt }}">
</section>

{% endblock %}

We’re using TimberImage here to grab the information we need about the image. In this case it’s the URL and the alt text.

We could leave it there… but as we’re already on a roll, we’re going to take advantage of the resize filter. This takes 3 arguments: width, height, and crop. We’re only going to be specifying the width.

Let’s refactor our hero image to be a bit more responsive:

<img alt="{{ TimberImage(post.get_field('hero_image')).alt }}"
  srcset="
    {{ TimberImage(post.get_field('hero_image')).src | resize(1600)}} 1600w,
    {{ TimberImage(post.get_field('hero_image')).src | resize(1000)}} 1000w,
    {{ TimberImage(post.get_field('hero_image')).src | resize(700)}} 700w
 "
  src="{{ TimberImage(post.get_field('hero_image')).src | resize(1000)}}"
  sizes="100vw"
>

Yes, you can do that.

Conclusion

WordPress has much to offer, and I’m guilty of taking it for granted. Loving what you do is a reward in itself, and I believe that taking some time to approach something in a new way is a great opportunity to cultivate some new skills, and to re-invigorate motivation. I can’t tell you how many sites I’ve built with WordPress, but I can tell you the exact moment my mind was blown when I first used Timber.

Resources