Let's Build A WordPress Theme From Scratch Using Timber And Bootstrap

23 April 2018Junaid Qadir7 min read2316 Words
Let's Build A WordPress Theme From Scratch Using Timber And Bootstrap


If you are like "Dude, It's already 2018 and you are still writing about WordPress theme development from scratch ?" Well yes, why not! But trust me its different, its modern and it uses Timber! Read on...


If you happen to work in a digital agency, you have pretty much idea how heavy chunk of developers' time is spent creating themes unique to each client. And I understand the pain of developing a WordPress theme from scratch. Even though It doesn't feel that difficult after creating dozens and dozens of websites using vanilla WordPress. Right? But It is whatsoever no fun we all know. Luckily there is a smarter way.

Developing themes using vanilla WordPress felt like a chore, therefore the good folks at Boston based Upstatement, developed a plugin for WordPress that makes theme development a cinch.

Thank you Jared and team for such an awesome tool.

Enter Timber

Timber, along with the solid templating engine, Twig with it's object-oriented approach, makes a wonderful and a pretty powerful library which makes WordPress theme development easy and more enjoyable.

With Timber and Twig in your tool-belt, you end up nothing but writing clean, maintainable and modular code in a comparatively shorter amount of time.

Enough Praise, Let's Get To Work!

OK, let's stop the praise train for now and get down to business. So, we'll develop a basic functional blog theme based on this [Bootstrap 4.x blog example] with the following features:

  • Main Navigation
  • About Section sidebar
  • List paginated blog posts on the homepage
  • Search box
  • Search results page
  • Blog single page and Pages

It will be a striped-down version of the example given on Bootstrap website.


We will build our theme using these tools: * Node.js (with NPM) * Bootstrap 4.x (jQuery and popover.js are dependencies well') * Laravel Mix (BrowserSync) * Composer * Timber (Twig is a dependency and will be installed automatically)

Theme Setup

Timber is a WordPress plug-in as well as a composer package. So, we will be using it as a composer package and include it in our theme which saves us from managing it as an external (to the theme) dependency via something like this plugin.

To set up our brand new theme, assuming we have installed and configured a fresh copy of the latest version of WordPress, in your themes directory (wp-content/themes), create a folder and name it whatever you want or lets call it awe-theme . Like so,

1mkdir awe-theme

Now create a a file inside awe-theme called style.css and paste this:

2Theme Name: Awesome Theme
3Theme URI: https://example.com/awe-theme
4Author: [Your Name Here]
5Author URI: https://example.com
6Version: 1.0.0
7Description: A clean minimal personal blog theme.

Change Theme Name and other details as you like.

This comment block will tell WordPress that hey I'm a theme and these are my details. For any theme there are two files which are required, style.css and index.php otherwise WordPress will complain about the presence of a broken theme.

Stylesheet is missing

Template is missing

Finally, when we create both files, it will display our theme to be previewed or activated.

Theme with missing snapshot

If you notice a snapshot of the theme is missing but still we can use the theme. If you want, you can add a file named screenshot.png with dimensions 1200px x 900px to the theme's directory along with those two files. Pretty simple and straight-forward, Huh.

Now that WordPress has Identified our theme, let's install the prerequisites.

Install Timber

Make sure you have installed node.js, npm and composer on your system. First lets initialize composer so that it can autoload our PHP packages, Timber library in our case.

1composer init -n \
2--name=your-vendor/awe-theme \
3--type=wordpress-theme \
4--license=MIT \
5--require="timber/timber *"

This command will create composer.json with the information we provided. Go ahead and open the file, it looks something like this

2"name": "your-vendor/awe-theme",
3"type": "wordpress-theme",
4"require": {
5"timber/timber": "*"
7"license": "MIT"

We are not done yet, run composer install to complete the installation of composer and our package Timber. After its done installing, you get a vendor directory in your theme directory.

Install Front-End Libraries

Next up, we will install front-end libraries Bootstrap, jQueryand Laravel Mix. But first lets create our package.json file. Run this command:

1npm init -y

You should see output similar to this:

1Wrote to /var/www/html/junaidqadir/
5"name": "awe-theme",
6"version": "1.0.0",
7"description": "",
8"main": "index.js",
9"scripts": {
10"test": "echo \"Error: no test specified\" && exit 1"
12"keywords": [],
13"author": "",
14"license": "ISC"

Now lets install all the required libraries with this single command:

1npm install laravel-mix bootstrap jquery popover.js \
2browser-sync browser-sync-webpack-plugin --save-dev

This will take a while to install so be patient.

Configure Laravel Mix

If you've never heard of Laravel Mix, its a wrapper around webpack which provides an elegant API for defining basic build steps for a project. It was developed for Laravel but using it outside Laravel is super easy. Let me show you how easy it is. Before that lets create directories and blank files for our style and script assets like this:

Assets structure

Create a new file and name it webpack.mix.js in your theme folder. Now paste this and save it:

1let mix = require('laravel-mix');
2mix.js('assets/src/js/app.js', 'assets/dist/')
3.sass('assets/src/scss/app.scss', 'assets/dist/');

Then edit your package.json file and replace this:

1"scripts": {
3"test": "echo \"Error: no test specified\" && exit 1"

with this:

1"scripts": {
2"dev": "NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
3"watch": "NODE_ENV=development node_modules/webpack/bin/webpack.js --watch --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
4"production": "NODE_ENV=production node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"

and save it. That's it. We just configured Laravel Mix.

Now we have three power-packed commands at our disposal which will be really useful throughout our development. * dev - Compiles all the scripts and other assets without optimization * watch - Compiles the assets and refreshes your browser as soon as you make any change to the code * production - generates optimized version of all the assets

These commands can be used like this:

1#npm run [command name]
2npm run dev

Configure Browsersync

This step is optional but really useful and nice to have. It will save you some precious time between making a change to your code and seeing it on the browser throughout your development process.

Open up webpack.mix.js file and just before the .sass("assets/src/scss/app.scss", "assets/dist/") line, paste this:

2 proxy: "junaidqadir.local",
3 files: [
4 "./assets/dist/*",
5 "./assets/src/js/**/*.js",
6 "./assets/src/scss/**/*.scss",
7 "./assets/src/img/**/*.+(png|jpg|svg)",
8 "./**/*.+(html|php)",
9 "./views/**/*.+(html|twig)"
10 ]

So the complete file should look something like this:

1let mix = require("laravel-mix");
3 .js("assets/src/js/app.js", "assets/dist/")
4 .sass("assets/src/scss/app.scss", "assets/dist/")
5 .browserSync({
6 proxy: "junaidqadir.local",
7 files: [
8 "./assets/dist/*",
9 "./assets/src/js/**/*.js",
10 "./assets/src/scss/**/*.scss",
11 "./assets/src/img/**/*.+(png|jpg|svg)",
12 "./**/*.+(html|php)",
13 "./views/**/*.+(html|twig)"
14 ]
15 });

Now, replace the proxy URL with your development URL for example http://localhost/my-wp-site.

Run npm run watch to start watching for file changes.

This setup is pretty much reusable and can be reused for most of your real life projects. Same holds true for the next section. If you plan your structure well you will end up reusing things instead of re inventing the well again and again.

Let's Dive In To Theme Development

We will develop our theme based on this Bootstrap 4.x blog example In order to use Timber or any other package we must include vendor/autoload.php to functions.php so that its available to the theme.

Go ahead and create a file called functions.php then paste this:

3require_once __DIR__ . '/vendor/autoload.php';

Layout Composition

Timber by default, looks for twig files or views in a views directory in the theme root. Our theme will have a navigation, a footer, an optional sidebar and a content area which we will break in to twig partials. I like to organize my views into self-explanatory directories under the views directory for ease: * views/layouts * views/pages * views/partials

layouts are twig files which are extended (or used) by pages or partials in our case.

Twig has a concept they call Template Inheritance. It helps us break a page in to reusable parts. We will heavily use extends and block tags to break our pages in to composable partials. You can read more about it here.

Partial Views Or Partials

We start by extracting partials from the blog example and save them in twig files then we'll include them in our master layout we call base.twig.

Header Partial View

1{# File: views/partials/header.twig #}
3<header class="blog-header py-3">
4 <div class="row flex-nowrap justify-content-between align-items-center">
5 <div class=" col-sm-12 col-md-4 pt-1">
6 </div>
7 <div class="col-sm-12 col-md-4 text-center">
8 <a class="blog-header-logo text-dark" href="{{ site.link }}">{{ site.name }}</a>
9 </div>
10 <div class=" col-sm-12 col-md-4 d-flex justify-content-end align-items-center">
11 <form class="form-inline" method="get" action="/">
12 <div class="form-group">
13 <label for=""></label>
14 <input type="search"
15 name="s" id="" class="form-control form-control-sm" placeholder="Enter your search">
16 </div>
17 </form>
18 </div>
19 </div>
23##### Navigation Partial View
24We will leave `nav` simple. It will fetch only the top level of the menu.
27{# File: views/partials/nav.twig #}
29<div class="nav-scroller py-1 mb-2">
30 <nav class="nav d-flex justify-content-center">
31 {% for menuItem in menu.get_items %}
32 <a class="p-2 text-muted" href="{{ menuItem.link }}">{{ menuItem.title }}</a>
33 {% endfor %}
34 </nav>

Post Teaser Partial View

Then we have what we call the post teaser.

1{# File: views/partials/post-tease-card.twig #}
3<div class="card flex-md-row mb-4 box-shadow h-md-250">
4 <div class="card-body d-flex flex-column align-items-start">
5 <h3 class="mb-0">
6 <a class="text-dark" href="{{ post.link }}">{{ post.title }}</a>
7 </h3>
8 <div class="mb-1 text-muted">{{ post.date }}</div>
9 <p class="card-text mb-auto">This is a wider card with supporting text below as a natural
10 lead-in to
11 additional content.</p>
12 <div class="mb-2 mt-2">
13 {% for category in post.categories %}
14 <span><small class="badge-pill badge-success">{{ category.title }}</small></span>
15 {% endfor %}
16 </div>
17 <a href="{{ post.link }}">Continue reading</a>
18 </div>
19 <img class="card-img-right flex-auto d-none d-lg-block" src="holder.js/200x250?theme=thumb"
20 alt="Card image cap"/>

Sidebar Partial View

Next up, we have the sidebar. If added, it will display the site name and description as well as social links. The social links are hard-coded. So go ahead and add your all of your social links in this partial.

1{# File views/partials/sidebar.twig #}
3<div class="p-3 mb-3 bg-light rounded">
4 <h4 class="font-italic">About {{ site.name }}</h4>
5 <p class="mb-0">{{ site.description }}</p>
7<div class="p-3">
8 <h4 class="font-italic">Elsewhere</h4>
9 <ol class="list-unstyled">
10 <li><a href="#">GitHub</a></li>
11 <li><a href="#">Twitter</a></li>
12 <li><a href="#">Facebook</a></li>
13 </ol>

Pagination Partial View

1{# File: views/partials/pagination.twig #}
3{% if pagination.pages is not empty %}
4 <ul class="pagination justify-content-center p-0 m-0 mt-3 mb-3">
5 {% if pagination.pages|first and pagination.pages|first.current != 1 %}
6 <li class="page-item">
7 <a href="{{ pagination.prev.link }}" class="page-link">Previous</a>
8 </li>
9 {% else %}
10 <li class="page-item disabled">
11 <a href="{{ pagination.prev.link }}" class="page-link" tabindex="-1">Previous</a>
12 </li>
13 {% endif %}
14 {% for pager in pagination.pages %}
15 {% if pager.current %}
16 <li class="page-item active">
17 <a href="{{ pager.link }}" class="page-link">{{ pager.title }}</a>
18 </li>
19 {% else %}
20 <li class="page-item">
21 <a href="{{ pager.link }}" class="page-link">{{ pager.title }}</a>
22 </li>
23 {% endif %}
24 {% endfor %}
26 {% if(pagination.next) %}
27 <li class="page-item">
28 <a href="{{ pagination.next.link }}" class="page-link">Next</a>
29 </li>
30 {% else %}
31 <li class="page-item disabled">
32 <a class="page-link">Next</a>
33 </li>
34 {% endif %}
35 </ul>
36{% endif %}

That's all for partials. Now we'll move to layouts.

Layouts Views

We have two layouts, a wide one without sidebar we call it base.twig and the second one with a sidebar we call it base-sidebar.twig. base.twig will be used for the homepage, pages and search results page, whereas the base-sidebar.twig will be used for article single page. But you can do whatever and however you want and use whichever base/layout with whichever page you like.

1{# File views/layouts/base.twig #}
3<!doctype html>
4<html lang="en">
6 {{ wp_head }}
7 <meta charset="utf-8"/>
8 <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"/>
9 <meta name="description" content="{{ site.description }}"/>
10 <meta name="author" content=""/>
11 <link rel="icon" href="{{ site.theme.link }}/favicon.ico"/>
12 <title>{{ site.title }}</title>
13 <link href="https://fonts.googleapis.com/css?family=Playfair+Display:700,900" rel="stylesheet"/>
14 <link href="{{ site.theme.link }}/assets/dist/app.css" rel="stylesheet"/>
17<div class="container">
18 {% include 'partials/header.twig' %}
19 {% include 'partials/nav.twig' %}
21<main role="main" class="container">
22 {% block content %}
23 {% endblock %}
25<footer class="blog-footer">
26 <p>Blog template built for <a href="https://getbootstrap.com/">Bootstrap</a> by <a href="https://twitter.com/mdo">@mdo</a>.
27 </p>
28 <p>
29 <a href="#">Back to top</a>
30 </p>
32{{ wp_footer }}
33<script src="{{ site.theme.link }}/assets/dist/app.js"></script>

If you notice we are pulling site/page title, description as well as theme URL from timber context which we will discuss when we discuss the PHP files. And also notice how we are including the header and nav twig files using the include tag. This is similar to include() function in PHP. Then we define a block using the block and endblock tag named content. It means we can put anything here from another twig file by using the extends tag and defining a block with the same name. We will see it in a moment.

Now lets see the base with a sidebar.

1{# File views/layouts/base-sidebar.twig #}
3<!doctype html>
4<html lang="en">
6 {{ wp_head }}
7 <meta charset="utf-8"/>
8 <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"/>
9 <meta name="description" content=""/>
10 <meta name="author" content=""/>
11 <link rel="icon" href="{{ site.theme.link }}/favicon.ico"/>
12 <title>Blog Template for Bootstrap</title>
13 <link href="https://fonts.googleapis.com/css?family=Playfair+Display:700,900" rel="stylesheet"/>
14 <link href="{{ site.theme.link }}/assets/dist/app.css" rel="stylesheet"/>
17<div class="container">
18 {% include 'partials/header.twig' %}
19 {% include 'partials/nav.twig' %}
21<main role="main" class="container">
22 <div class="row">
23 <div class="col-md-8 blog-main">
24 {% block content %}
25 {% endblock %}
26 </div>
27 <aside class="col-md-4 blog-sidebar">
28 {% include 'partials/sidebar.twig' %}
29 </aside>
30 </div>
32<footer class="blog-footer">
33 <p>Blog template built for <a href="https://getbootstrap.com/">Bootstrap</a> by <a
34 href="https://twitter.com/mdo">@mdo</a>.
35 </p>
36 <p><a href="#">Back to top</a></p>
38<script src="{{ site.theme.link }}/assets/dist/app.js"></script>

Compare this with the base.twig you'll see the difference.

Pages Views

We have three views which we have categorized in to pages because of their nature. The rule I have set for calling any view a page and putting it under views/pages directory is simple, if it extends a layout, it's page otherwise its a partial and is placed under views/partials.

Index View

1{# File views/pages/index.twig #}
3{% extends 'layouts/base.twig' %}
4{% block content %}
5 <div class="row mb-2 mt-5">
6 {% for post in posts %}
7 <div class="col-md-12">
8 {% include 'partials/post-tease-card.twig' %}
9 </div>
10 {% endfor %}
11 </div>
12 {% include 'partials/pagination.twig' %}
13{% endblock %}

Notice we have defined a block named content in the base.twig already. This means we have made base.twig aware that we can replace this part with whatever we like and whenever we like. So index.twig wants to display its own markup between {% block content %} and {% endblock %} by defining the block here and putting our markup.

Post Single View

1{# File views/pages/single.twig #}
3{% extends 'layouts/base.twig' %}
4{% block content %}
5 <div class="blog-post">
6 <h2 class="blog-post-title">{{ post.title }}</h2>
7 <p class="blog-post-meta">January 1, 2014 by <a href="{{post.author.link}}">{{post.author.name}}</a></p>
8 {{ post.content | shortcodes }}
9 </div>
10 {% if post.comment_status != 'closed' %}
11 <section class="comments">
12 <div class="respond">
13 <h3 class="h2">Comments</h3>
14 {{ comment_form }}
15 </div>
16 <div class="responses mt-5">
17 {% for cmt in post.get_comments() %}
18 {% include "partials/comment.twig" with {comment:cmt} %}
19 {% endfor %}
20 </div>
22 </section>
23 {% endif %}
24{% endblock %}

This will display a single post with title, date, author name and URL and content of the post.

Page View

1{# File: views/pages/page.twig #}
3{% extends 'layouts/base.twig' %}
4{% block content %}
5 <div class="blog-post">
6 <h2 class="blog-post-title mb-3">{{ post.title }}</h2>
7 {{ post.content | shortcodes }}
8 </div>
9{% endblock %}

This is similar to single but this is for pages. For WordPress, page is a post with post type of page. so you can use the same file for both or if you want to make posts appear different than pages you should separate them.

Search View

1{# File views/pages/search.twig #}
3{% extends 'layouts/base.twig' %}
4{% block content %}
5 <div class="row h-100 justify-content-center align-self-center mr-0" style="overflow:hidden">
6 <div class="my-auto text-center text-muted p-0 m-0">
7 <h2>Found {{ posts|length }} results for your query "{{ search_term }}"</h2>
8 </div>
9 </div>
10 <div class="row mb-2 mt-5">
11 {% for post in posts %}
12 <div class="col-md-12">
13 {% include 'partials/post-tease-card.twig' %}
14 </div>
15 {% endfor %}
16 </div>
17{% endblock %}

View Controllers

Now that all our views are created, we will move towards the PHP files responsible to render these views. I like to call them controllers because they do control the views. We have a controller for each of our page view.

Note that by default WordPress reads the .php files/templates from theme's directory in a certain order. To get a better understanding of which template (.php files) of a theme are called and when, carefully read this section from the WordPress documentation and also check this interactive diagram to help you better understand how template hierarchy in WordPress works.

Now, This is the minimum code we need to load our theme:

3use Timber\Timber;
5$context = Timber::get_context();
6Timber::render( 'pages/index.twig', $context );

Timber::get_context() is the sauce of Timber library. It gives you almost everything about your website as well as the current theme. Which includes the current user, all the posts, site details like title, description, body classes, all the details about the current theme (as Said earlier) and what not, all in an Object-Oriented manner, which can be accessed from the twig file with little to no effort!

For example, to get the site's title, you write site.title and to get the current theme URL, you'd write site.theme.link. Can't be more simple than this!

Index Controller

Open up index.php we created in the start and paste this:

3/* File: index.php */
5use Timber\Timber;
7 global $paged;
8 if ( ! isset( $paged ) || ! $paged ) {
9 $paged = 1;
10 }
11 $context = Timber::get_context();
12 $args = [
13 'post_type' => 'post',
14 'posts_per_page' => 10,
15 'paged' => $paged,
16 ];
17 $context['posts'] = new \Timber\PostQuery( $args );
18 $templates = [
19 'pages/index.twig',
20 ];
21 Timber::render( $templates, $context );

As I explained in the earlier that get_context() function besides other useful bits of information, also has a list of posts, but if you notice I've still called PostQuery() with $args in the index.php why? Because I want to tell WordPress, "hey, I want posts with post_type post and ten of them and yes, enable pagination as well". This will override the posts in the context and with paginated ones. The code is pretty self explanatory.

The rest of the controllers are similar they just call the relevant view.

Single Controller

3/* File: single.php */
5use Timber\Timber;
7$context = Timber::get_context();
8$context['post'] = Timber::query_post();
9Timber::render( 'pages/single.twig', $context );

Page Controller

3 /* File: page.php */
5use Timber\Timber;
7$context = Timber::get_context();
8$context['post'] = Timber::query_post();
9Timber::render( 'pages/page.twig', $context );

Search Controller

3{# File: search.php #}
5use Timber\Timber;
7$context = Timber::get_context();
8$search_term = get_search_query();
9$context['title'] = 'Search results for ' . $search_term;
10$context['posts'] = new \Timber\PostQuery();
11$context['search_term'] = $search_term;
13Timber::render( 'pages/search.twig', $context );

Are We Missing Something?

Looks like we are. Because we are using a navigation menu and also featured image in our theme and never told WordPress about it. This is a good time we do in functions.php. Go ahead and open it and paste this:

3require_once __DIR__ . '/vendor/autoload.php';
5class mySite extends Timber\Site {
6 public function __construct() {
7 add_theme_support( 'post-thumbnails' );
8 add_theme_support( 'menus' );
9 add_filter( 'timber_context', array( $this, 'add_to_context' ) );
10 parent::__construct();
11 }
13 function add_to_context( $context ) {
14 $context['menu'] = new Timber\Menu('main-menu');
15 $context['site'] = $this;
17 return $context;
18 }
21new mySite();

Notice we have defined a class named mySite. This allows us to configure our Timber theme and add features like menu, thumbnail and many more.

Also notice in views/partials/nav.twig we are calling menu.get_items(). menu is referring to $context['menu'] which is assigned a instance of Timber\Menu which loads the menu we have defined in our WordPress dashboard.

Thats pretty much all. If you want you can move this class to its own file for the sake of keeping functions.php light and clean.

Home Work

To get you hands-on, I have left the footer in the base.twig and base-sidebar. All you have to do is extract it out in to a footer.twig and include it in both layouts just like we did with header.twig. The second assignment is add the featured image to the post's single view. At the moment we only display the featured image on the articles list on the home page.

Finally thank you very much for taking the time to read till here. If you have followed along, great! If not you can check out the code on GitHub.

Share this post
How do you feel about this article?