Pre-Prelude
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…
Prelude
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.
Prerequisites
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,
mkdir awe-theme
Now create a a file inside awe-theme
called style.css
and paste this:
/*
Theme Name: Awesome Theme
Theme URI: https://example.com/awe-theme
Author: [Your Name Here]
Author URI: https://example.com
Version: 1.0.0
Description: 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.
Finally, when we create both files, it will display our theme to be previewed or activated.
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.
composer init -n \
--name=your-vendor/awe-theme \
--type=wordpress-theme \
--license=MIT \
--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
{
"name": "your-vendor/awe-theme",
"type": "wordpress-theme",
"require": {
"timber/timber": "*"
},
"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
, jQuery
and Laravel Mix
. But first lets create our package.json
file. Run this command:
npm init -y
You should see output similar to this:
Wrote to /var/www/html/junaidqadir/
wp-content/themes/awe-theme/package.json:
{
"name": "awe-theme",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
Now lets install all the required libraries with this single command:
npm install laravel-mix bootstrap jquery popover.js \
browser-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:
Create a new file and name it webpack.mix.js
in your theme folder. Now paste this and save it:
let mix = require('laravel-mix');
mix.js('assets/src/js/app.js', 'assets/dist/')
.sass('assets/src/scss/app.scss', 'assets/dist/');
Then edit your package.json
file and replace this:
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
with this:
"scripts": {
"dev": "NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
"watch": "NODE_ENV=development node_modules/webpack/bin/webpack.js --watch --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
"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:
#npm run [command name]
npm 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:
.browserSync({
proxy: "junaidqadir.local",
files: [
"./assets/dist/*",
"./assets/src/js/**/*.js",
"./assets/src/scss/**/*.scss",
"./assets/src/img/**/*.+(png|jpg|svg)",
"./**/*.+(html|php)",
"./views/**/*.+(html|twig)"
]
});
So the complete file should look something like this:
let mix = require("laravel-mix");
mix
.js("assets/src/js/app.js", "assets/dist/")
.sass("assets/src/scss/app.scss", "assets/dist/")
.browserSync({
proxy: "junaidqadir.local",
files: [
"./assets/dist/*",
"./assets/src/js/**/*.js",
"./assets/src/scss/**/*.scss",
"./assets/src/img/**/*.+(png|jpg|svg)",
"./**/*.+(html|php)",
"./views/**/*.+(html|twig)"
]
});
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:
<?php
require_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
{# File: views/partials/header.twig #}
<header class="blog-header py-3">
<div class="row flex-nowrap justify-content-between align-items-center">
<div class=" col-sm-12 col-md-4 pt-1">
</div>
<div class="col-sm-12 col-md-4 text-center">
<a class="blog-header-logo text-dark" href="{{ site.link }}">{{ site.name }}</a>
</div>
<div class=" col-sm-12 col-md-4 d-flex justify-content-end align-items-center">
<form class="form-inline" method="get" action="/">
<div class="form-group">
<label for=""></label>
<input type="search"
name="s" id="" class="form-control form-control-sm" placeholder="Enter your search">
</div>
</form>
</div>
</div>
</header>
```
##### Navigation Partial View
We will leave `nav` simple. It will fetch only the top level of the menu.
````twig
{# File: views/partials/nav.twig #}
<div class="nav-scroller py-1 mb-2">
<nav class="nav d-flex justify-content-center">
{% for menuItem in menu.get_items %}
<a class="p-2 text-muted" href="{{ menuItem.link }}">{{ menuItem.title }}</a>
{% endfor %}
</nav>
</div>
Post Teaser Partial View
Then we have what we call the post teaser.
{# File: views/partials/post-tease-card.twig #}
<div class="card flex-md-row mb-4 box-shadow h-md-250">
<div class="card-body d-flex flex-column align-items-start">
<h3 class="mb-0">
<a class="text-dark" href="{{ post.link }}">{{ post.title }}</a>
</h3>
<div class="mb-1 text-muted">{{ post.date }}</div>
<p class="card-text mb-auto">This is a wider card with supporting text below as a natural
lead-in to
additional content.</p>
<div class="mb-2 mt-2">
{% for category in post.categories %}
<span><small class="badge-pill badge-success">{{ category.title }}</small></span>
{% endfor %}
</div>
<a href="{{ post.link }}">Continue reading</a>
</div>
<img class="card-img-right flex-auto d-none d-lg-block" src="holder.js/200x250?theme=thumb"
alt="Card image cap"/>
</div>
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.
{# File views/partials/sidebar.twig #}
<div class="p-3 mb-3 bg-light rounded">
<h4 class="font-italic">About {{ site.name }}</h4>
<p class="mb-0">{{ site.description }}</p>
</div>
<div class="p-3">
<h4 class="font-italic">Elsewhere</h4>
<ol class="list-unstyled">
<li><a href="#">GitHub</a></li>
<li><a href="#">Twitter</a></li>
<li><a href="#">Facebook</a></li>
</ol>
</div>
Pagination Partial View
{# File: views/partials/pagination.twig #}
{% if pagination.pages is not empty %}
<ul class="pagination justify-content-center p-0 m-0 mt-3 mb-3">
{% if pagination.pages|first and pagination.pages|first.current != 1 %}
<li class="page-item">
<a href="{{ pagination.prev.link }}" class="page-link">Previous</a>
</li>
{% else %}
<li class="page-item disabled">
<a href="{{ pagination.prev.link }}" class="page-link" tabindex="-1">Previous</a>
</li>
{% endif %}
{% for pager in pagination.pages %}
{% if pager.current %}
<li class="page-item active">
<a href="{{ pager.link }}" class="page-link">{{ pager.title }}</a>
</li>
{% else %}
<li class="page-item">
<a href="{{ pager.link }}" class="page-link">{{ pager.title }}</a>
</li>
{% endif %}
{% endfor %}
{% if(pagination.next) %}
<li class="page-item">
<a href="{{ pagination.next.link }}" class="page-link">Next</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link">Next</a>
</li>
{% endif %}
</ul>
{% 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.
{# File views/layouts/base.twig #}
<!doctype html>
<html lang="en">
<head>
{{ wp_head }}
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"/>
<meta name="description" content="{{ site.description }}"/>
<meta name="author" content=""/>
<link rel="icon" href="{{ site.theme.link }}/favicon.ico"/>
<title>{{ site.title }}</title>
<link href="https://fonts.googleapis.com/css?family=Playfair+Display:700,900" rel="stylesheet"/>
<link href="{{ site.theme.link }}/assets/dist/app.css" rel="stylesheet"/>
</head>
<body>
<div class="container">
{% include 'partials/header.twig' %}
{% include 'partials/nav.twig' %}
</div>
<main role="main" class="container">
{% block content %}
{% endblock %}
</main>
<footer class="blog-footer">
<p>Blog template built for <a href="https://getbootstrap.com/">Bootstrap</a> by <a href="https://twitter.com/mdo">@mdo</a>.
</p>
<p>
<a href="#">Back to top</a>
</p>
</footer>
{{ wp_footer }}
<script src="{{ site.theme.link }}/assets/dist/app.js"></script>
</body>
</html>
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.
{# File views/layouts/base-sidebar.twig #}
<!doctype html>
<html lang="en">
<head>
{{ wp_head }}
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"/>
<meta name="description" content=""/>
<meta name="author" content=""/>
<link rel="icon" href="{{ site.theme.link }}/favicon.ico"/>
<title>Blog Template for Bootstrap</title>
<link href="https://fonts.googleapis.com/css?family=Playfair+Display:700,900" rel="stylesheet"/>
<link href="{{ site.theme.link }}/assets/dist/app.css" rel="stylesheet"/>
</head>
<body>
<div class="container">
{% include 'partials/header.twig' %}
{% include 'partials/nav.twig' %}
</div>
<main role="main" class="container">
<div class="row">
<div class="col-md-8 blog-main">
{% block content %}
{% endblock %}
</div>
<aside class="col-md-4 blog-sidebar">
{% include 'partials/sidebar.twig' %}
</aside>
</div>
</main>
<footer class="blog-footer">
<p>Blog template built for <a href="https://getbootstrap.com/">Bootstrap</a> by <a
href="https://twitter.com/mdo">@mdo</a>.
</p>
<p><a href="#">Back to top</a></p>
</footer>
<script src="{{ site.theme.link }}/assets/dist/app.js"></script>
</body>
</html>
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
{# File views/pages/index.twig #}
{% extends 'layouts/base.twig' %}
{% block content %}
<div class="row mb-2 mt-5">
{% for post in posts %}
<div class="col-md-12">
{% include 'partials/post-tease-card.twig' %}
</div>
{% endfor %}
</div>
{% include 'partials/pagination.twig' %}
{% 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
{# File views/pages/single.twig #}
{% extends 'layouts/base.twig' %}
{% block content %}
<div class="blog-post">
<h2 class="blog-post-title">{{ post.title }}</h2>
<p class="blog-post-meta">January 1, 2014 by <a href="{{post.author.link}}">{{post.author.name}}</a></p>
{{ post.content | shortcodes }}
</div>
{% if post.comment_status != 'closed' %}
<section class="comments">
<div class="respond">
<h3 class="h2">Comments</h3>
{{ comment_form }}
</div>
<div class="responses mt-5">
{% for cmt in post.get_comments() %}
{% include "partials/comment.twig" with {comment:cmt} %}
{% endfor %}
</div>
</section>
{% endif %}
{% endblock %}
This will display a single post with title, date, author name and URL and content of the post.
Page View
{# File: views/pages/page.twig #}
{% extends 'layouts/base.twig' %}
{% block content %}
<div class="blog-post">
<h2 class="blog-post-title mb-3">{{ post.title }}</h2>
{{ post.content | shortcodes }}
</div>
{% 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
{# File views/pages/search.twig #}
{% extends 'layouts/base.twig' %}
{% block content %}
<div class="row h-100 justify-content-center align-self-center mr-0" style="overflow:hidden">
<div class="my-auto text-center text-muted p-0 m-0">
<h2>Found {{ posts|length }} results for your query "{{ search_term }}"</h2>
</div>
</div>
<div class="row mb-2 mt-5">
{% for post in posts %}
<div class="col-md-12">
{% include 'partials/post-tease-card.twig' %}
</div>
{% endfor %}
</div>
{% 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:
<?php
use Timber\Timber;
$context = Timber::get_context();
Timber::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:
<?php
/* File: index.php */
use Timber\Timber;
global $paged;
if ( ! isset( $paged ) || ! $paged ) {
$paged = 1;
}
$context = Timber::get_context();
$args = [
'post_type' => 'post',
'posts_per_page' => 10,
'paged' => $paged,
];
$context['posts'] = new \Timber\PostQuery( $args );
$templates = [
'pages/index.twig',
];
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
<?php
/* File: single.php */
use Timber\Timber;
$context = Timber::get_context();
$context['post'] = Timber::query_post();
Timber::render( 'pages/single.twig', $context );
Page Controller
<?php
/* File: page.php */
use Timber\Timber;
$context = Timber::get_context();
$context['post'] = Timber::query_post();
Timber::render( 'pages/page.twig', $context );
Search Controller
<?php
{# File: search.php #}
use Timber\Timber;
$context = Timber::get_context();
$search_term = get_search_query();
$context['title'] = 'Search results for ' . $search_term;
$context['posts'] = new \Timber\PostQuery();
$context['search_term'] = $search_term;
Timber::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:
<?php
require_once __DIR__ . '/vendor/autoload.php';
class mySite extends Timber\Site {
public function __construct() {
add_theme_support( 'post-thumbnails' );
add_theme_support( 'menus' );
add_filter( 'timber_context', array( $this, 'add_to_context' ) );
parent::__construct();
}
function add_to_context( $context ) {
$context['menu'] = new Timber\Menu('main-menu');
$context['site'] = $this;
return $context;
}
}
new 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.
Leave a Reply