How to Build a WordPress Theme from Scratch
WordPress themes give WordPress users the ability to completely change the look of a WP website, as well as add functionality to it. In this three-part series, we’ll introduce WordPress themes, showing how they work, how they’re structured, the PHP architecture behind them, and other relevant information. We’ll then embark on a journey to build a WordPress theme.
A Word or Two on the Basics
WordPress was conceived as a blog engine, or a simple, blogging-oriented content management system. It was initially released in 2003, by Matt Mullenweg and Mike Little. Ever since then, its user base hasn’t stopped growing. WordPress is a PHP-powered web application that uses MySQL as its database, and is usually run behind a server program, such as NGINX or Apache.
WordPress is basically just a bunch of PHP files that work together as an application. A PHP interpreter uses these files to produce web pages to web visitors. It produces HTML, to be more precise.
The role of the templating engine of WordPress is to enable us to write (primarily) presentational instructions — instructions on how exactly to structure and style the HTML content that WordPress will output. These instructions are encapsulated in WordPress themes.
Each theme consist of a folder with PHP, CSS, and sometimes JavaScript files. The files that every WordPress theme must have — at the minimum — are style.css
and index.php
. This is the technical minimum required for the theme to function, but no serious WordPress theme stays with only these two files.
Basic template and Partials Files
The minimum index.php
file catches all the queries that don’t have their respective specialized template files within a theme. front-page.php
, home.php
, page.php
, taxonomy.php
, author.php
, archive.php
are some of the templates that we can use in our themes to further structure specific pages or queries in our theme.
For example, the archive.php
file will specify the HTML output structure when a visitor requests some of the pages that display list of posts. page.php
will specify how to display individual pages, and so on.
Partials files encapsulate repeatable parts of pages in WordPress website. For example, the header and footer are usually consistent across all pages on a website, so WordPress themes separate these page parts into header.php
and footer.php
. comments.php
will be used to display comments wherever applicable.
These files are then required from the template files that we explained.
This way, we adhere to the DRY principle, and don’t repeat the code in all those files.
Template Hierarchy
In the WordPress templating system, there’s a hierarchy of template files that WordPress will try to use for each request. This hierarchy is based on specificity. WordPress will try to use the most specific file for each request, if it exists. If it doesn’t exist, it will look up the next, less specific file, and so on.
To explain this on a page request — when the visitor visits a specific page on a WordPress website — WordPress will first try to find the template the page author assigned to it in the wp-admin
backend. That can be a completely custom template file, even completely static.
If there’s no such template, or it wasn’t assigned, WordPress will try to find a template file with a slug of that particular page in its filename. That would look like page-mypageslug.php
— whatever our mypageslug
may be.
The next file WordPress will try to use will be a file with that particular page’s ID in its filename — like page-48.php
.
If none of those page-specific files exist, WordPress will try to use page.php
— used for all the pages, unless otherwise specified.
If it doesn’t find page.php
, it will try to use singular.php
. This template file is used when — for posts — single.php
is not found, and for pages when page.php
is not found.
Now, if none of these are found in our page request example, WordPress will fall back to index.php
.
This, briefly, explains the WordPress template hierarchy. All of these template files we mentioned will usually incorporate (require) partials like header.php
, footer.php
and others as needed. They can also specify their specific partials to use — for example, a page-specific header.
The next thing you need to be familiar with to understand theming is the WordPress post type.
WordPress Post Types
The content in WordPress exists in the form of post types. The built-in types are posts and pages. These are the logical ones. WordPress also has a built-in attachment post type, navigation menus and revisions. These are also, technically, post types.
We can also specify our own post types, whether in our themes or our plugins. We need use the following:
register_post_type( $post_type, $args )
Here, we specify the post type name, how it’s structured, how it’s represented in wp-admin
, and so on.
When we have this defined, we can also create template files specific for this post type. Custom post types, like pages and posts, have their own template hierarchy.
More on Template Hierarchy
More about the template hierarchy can be found here.
style.css
style.css
is a crucial file in every WordPress theme, and its function goes beyond styling. This file is used to provide basic theme information to WordPress. Without this, WordPress would not be able to register a theme as a theme.
The WordPress Codex provides more information about all the details of this file. For the sake of brevity, we’ll review just some of them. In this CSS file’s header comments, we provide WordPress information on the following:
- theme name
- author
- description
- theme URI
- version
- license
- and other details
WordPress uses these details to display the theme properly in the back end.
The meta header in the Twenty Seventeen theme, for example, looks like this:
/*Theme Name: Twenty SeventeenTheme URI: https://wordpress.org/themes/twentyseventeen/Author: the WordPress teamAuthor URI: https://wordpress.org/Description: Twenty Seventeen brings your site to life with header video and immersive featured images. With a focus on business sites, it features multiple sections on the front page as wellas widgets, navigation and social menus, a logo, and more. Personalize its asymmetrical grid with a custom color scheme and showcase your multimedia content with post formats. Our default theme for 2017 works great in many languages, for any abilities, and on any device.Version: 1.7License: GNU General Public License v2 or laterLicense URI: http://www.gnu.org/licenses/gpl-2.0.htmlText Domain: twentyseventeenTags: one-column, two-columns, right-sidebar, flexible-header, accessibility-ready, custom-colors,custom-header, custom-menu, custom-logo, editor-style, featured-images, footer-widgets, post-formats, rtl-language-support, sticky-post, theme-options, threaded-comments, translation-ready
This theme, like WordPress, is licensed under the GPL.Use it to make something cool, have fun, and share what you’ve learned with others.*/
This said, style.css
file is, obviously, used to style our WordPress pages.
WordPress Action and Filter Hooks
WordPress — and WordPress themes and plugins — heavily rely on action hooks and filter hooks. These are part of an event-riven architecture. A simple explanation of this would be that, in the process of execution of the page connected to visitors’ web request, there are certain registered points — hooks — that enable us to fire actions at those points. We hook PHP functions to those points to be executed. Those are action hooks. Filter hooks are focused on processing — changing — pieces of data within the execution cycle. So, if the filter hook is registered for any piece of data (PHP variable), we can hook our own function and change, or process, this piece of data.
Although unorthodox, compared to the omnipresent MVC software pattern, the reliance on the hook system in WordPress has played a big role in the popularization of WordPress, as it has made it very simple to plug new functionalities into it, without the need to touch — or even to deeply understand — the core
codebase itself.
Hooks are explained here at greater length, and we’ll delve into it more in other parts of this guide.
The Loop
The Loop is the elementary part of the WordPress request cycle. As the WordPress Codex puts it:
The Loop is PHP code used by WordPress to display posts. Using The Loop, WordPress processes each post to be displayed on the current page, and formats it according to how it matches specified criteria within The Loop tags.
The WordPress loop is, basically, a PHP while
loop that displays posts according to visitors’ request, and specified criteria in the template file in which it’s specified.
Within the loop, whether it outputs just a single post or a page, or a list of them, we can output various parts of the post, like title and content. We can specify any HTML output or structure we want. We can also use conditional tags, and so on.
The most rudimentary example of the loop might look something like this:
if ( have_posts() ) { while ( have_posts() ) { the_post(); // // Post Content here // } // end while} // end if
We can use multiple loops on one page (in which case we need to reset them), and there are specific WordPress functions that exist for use inside it.
Conditional Tags
WordPress Conditional Tags are snippets of PHP code that enable us to display pieces of content only when certain conditions are satisfied. For example, we have the site-wide header.php
, which is used on every page of the website, but we may want certain HTML snippets to be displayed only on the home page or some other page.
So, in the first example, we would use is_front_page()
, and it would look something like this:
if (is_front_page()){ # do something}
There are lots of conditional tags provided, and they allow for big flexibility in WordPress development.
These are the building blocks of WordPress, but there are also taxonomies — which can be built in or custom ones we can specify — and terms within these taxonomies. We use them to categorize and sort our posts.
Creating the Bare Minimum Theme
The first thing we’ll do is install a plugin that will enable us to batch create WordPress posts and other content. This way, we’ll be able to quickly populate our development website without losing too much time. One plugin that serves this purpose is FakerPress by Gustavo Bordoni, available in the WordPress plugin repository.
# wp plugin install faker --activate --allow-root
We quickly install and activate the plugin via WP-CLI.
Now, when we log in to the admin dashboard, we’ll see that FakerPress is installed, and we can create all sorts of content in batch, including any custom post types we have.
Now, using this plugin, we’ll create some fake content. This is the result, using the default TwentySeventeen WordPress theme:
Now, we quickly dive in and set up a bare minimum theme that consists of the catch-all index.php
file, and style.css
, which we need for the WordPress templating system to recognize the theme:
/*Theme Name: Botega Simple ThemeTheme URI: https://botega.co.ukAuthor: Tonino JankovAuthor URI: https://botega.co.ukDescription: Basic WordPress theme for Sitepoint theme building tutorialText Domain: bsimpleVersion: 1.0.0License: GNU General Public License v2 or later*/
This is the style.css
, which consists only of meta CSS comments for now. These comments are required.
This is the index.php
file. It will catch all the requests for now:
<?php/** * * @package Botega_Scratch_Theme */?>
<!DOCTYPE html><html <?php language_attributes(); ?>><head> <title><?php bloginfo('name'); ?></title> <link rel="stylesheet" href="<?php bloginfo('stylesheet_url'); ?>"> <?php wp_head(); ?></head><body>
<header> <h1><?php bloginfo('name'); ?></h1> <h3><?php bloginfo('description'); ?></h3> </header>
<?php if ( have_posts() ) : /* Start the Loop */ while ( have_posts() ) : the_post(); endwhile; endif; ?>
</body>
We now upload and activate the minimal theme we have. I activate it using WP-CLI:
# wp theme activate bsimple --allow-root
The theme is now visible to WordPress, and is active:
We haven’t provided a screenshot, so the display in the backend is basic.
If we visit our website now in the browser, this is what we’ll see:
Obviously, we have work to do.
If we view the source code of the home page, we’ll see that the wp_head()
function has outputted a lot of default WordPress tags in the <head>
, like CSS, JavaScript, link
and meta
tags.
The bloginfo()
function is used to output website information.
Our home page is empty, because we aren’t outputting anything inside the Loop — a pattern that WordPress uses in all of its templates to output content.
The Codex page about the Loop goes deep into details about it. A typical structure for the loop — which is based on PHP while control structure — looks like this:
<?phpif ( have_posts() ) { while ( have_posts() ) { the_post(); // // Post Content here // } // end while} // end if?>
We need to fill that while
loop with content — or with content-outputting WordPress tags.
If we change our loop, by adding the_title()
, the_excerpt()
, and we add HTML markup and the_ID()
, to look like this:
<?phpif ( have_posts() ) : while ( have_posts() ): the_post(); ?>
<div id="post-<?php the_ID(); ?>"> <h2><?php the_title(); ?></h2> <div class="post-excerpt"><?php the_excerpt(); ?></div></div>
<?php endwhile; endif;?>
We’ll now get a list of posts on our home page, with no style applied:
WordPress shows A blog page — an archive page for all the blog posts — by default.
If we now visit single post URL — something like http://my-website.com/2018/11/14/sapiente-ad-facilis-quo-repellat-quos/
— we’ll see something like this:
Our loop, albeit very crude, actually works.
Structuring Our Theme into Files and Applying Bootstrap Markup
We’ll now implement partials, like header.php
and footer.php
and various specialized templates, all using Twitter Bootstrap markup, so that we can style it more easily.
Starting with index.php
, we replace all the content before and after the loop with get_header()
and get_footer()
functions:
<?php/** * * @package Botega_Scratch_Theme */
get_header(); ?>
<?php if ( have_posts() ) : while ( have_posts() ): the_post(); ?>
<div id="post-<?php the_ID(); ?>"> <h2><?php the_title(); ?></h2> <div class="post-excerpt"><?php the_excerpt(); ?></div> </div>
<?php endwhile; endif; ?>
<?php get_footer(); ?>
This means we need to provide all that content in the partials we mentioned.
In line with what we said — that we’ll use Twitter Bootstrap theme — our header.php
file will look like this:
<?php/** * The header for our theme. * * @package Botega_Scratch_Theme * */?><!DOCTYPE html><html><head><meta charset="<?php bloginfo( 'charset' ); ?>"><meta name="viewport" content="width=device-width, initial-scale=1">
<?php wp_head(); ?></head>
<body <?php body_class(); ?>>
<nav class="navbar navbar-default navbar-custom navbar-fixed-top"> <div class="container-fluid"> <div class="navbar-header page-scroll"> <a href="<?php echo esc_url( home_url( '/' ) ); ?>" rel="home" class="navbar-brand"> ➥<?php bloginfo( 'name' ); ?></a> </div>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1"> <ul class="nav navbar-nav navbar-right"> <?php wp_nav_menu( array( 'theme_location' => 'primary', 'items_wrap' => '%3$s' ) ); ?> </ul> </div> </div> </nav>
<div class="container"> <div class="row">
Our footer.php
file will look like this:
<?php/** * Footer template partial * * @package Botega_Scratch_Theme * */?> </div> <!-- /.row --> </div> <!-- /.container -->
<!-- Footer --> <footer> <div class="container"> <div class="row"> <div class="col-lg-8 col-md-10 mx-auto"> </div> </div><!-- /.row --> </div><!-- /.container --> </footer><!-- /footer -->
<?php wp_footer(); ?>
</body></html>
We are using Bootstrap classes in our HTML tags, and wp_head() and wp_footer() fire wp_head
and wp_footer
action hooks.
The next thing we’ll do is include the CSS and JavaScript from clean bootstrap template from startbootstrap.com, which comes with an MIT license, so we can freely use it. This way, our theme will come with predefined styles, responsiveness, and we’ll still be able to style it further.
functions.php
functions.php
is a file that comes with any serious WordPress theme. This is a file that acts as a poor man’s plugin archive. It allows us to include any custom functionality in our theme.
We’ll firstly use this file to include Bootstrap and our bootstrap theme’s styles and scripts:
function bsimple_scripts() { wp_enqueue_style( 'bsimple-style', get_stylesheet_uri() ); wp_enqueue_style( 'bsimple-clean', get_template_directory_uri() . '/css/clean-blog.min.css' ); wp_enqueue_style( 'bsimple-bootstrap', get_template_directory_uri() . '/css/bootstrap.min.css' ); wp_enqueue_style( 'bsimple-fontawesome', get_template_directory_uri() . '/css/fa-all.min.css' ); wp_enqueue_style( 'bsimple-font1', "https://fonts.googleapis.com/css?family=Lora:400,700, ➥400italic,700italic" ); wp_enqueue_style( 'bsimple-font2', "https://fonts.googleapis.com/css?family=Open+Sans:300italic,4 ➥00italic,600italic,700italic,800italic,400,300,600,700,800" );
wp_enqueue_script( 'bsimple-jq', get_template_directory_uri() . '/js/jquery.min.js'); wp_enqueue_script( 'bsimple-bootstrap', get_template_directory_uri() . '/js/bootstrap.bundle.min.js'); wp_enqueue_script( 'bsimple-clean', get_template_directory_uri() . '/js/clean-blog.min.js');}
add_action( 'wp_enqueue_scripts', 'bsimple_scripts' );
This is a WordPress-idiomatic way of including scripts and styles in a theme. It allows us to specify that the position of the scripts will be enqueued (header vs footer) and the priority of enqueuing. We can even specify the dependency of each particular resource on the other. This will ensure resources will be loaded in the right order.
We’re using the wp_enqueue_scripts
action hook here. We can learn more about it in the Codex. (We covered action hooks in the previous article.)
Inside our custom bsimple_scripts()
function — which we hook to wp_enqueue_scripts
action hook — we use two WordPress functions to load our scripts and styles — wp_enqueue_script() and wp_enqueue_style(). Arguments for these functions — as specified in its linked reference pages — allow us to fully leverage flexibility that we mentioned.
We can see that we’re loading styles from the Internet (Google fonts) and from our theme folder. Therefore, we create css
, js
and webfonts
directories in our theme folder, and copy our Bootstrap theme’s CSS, JavaScript files, and FontAwesome icon-font files.
We also copy our index.php
file to archive.php
, page.php
and single.php
files, which we’ll modify.
Now our theme file structure will look something like this:
Adjusting the Markup
If we now visit our home page, we’ll see the menu on the top — though it and the page are still a mess - because the following line in our header is still outputting the menu wrapped in div
and its own ul
tags, so it isn’t affected by our bootstrap styles:
<?php wp_nav_menu( array( 'theme_location' => 'primary', 'items_wrap' => '%3$s' ) ); ?>
To solve this, we first need to go to our wp-admin
dashboard and create — in the customizer — a new menu. we’ll name it Top Menu.
After we’ve done this, we’ll go to our header.php
file remove these lines:
<ul class="nav navbar-nav navbar-right"> <?php wp_nav_menu( array( 'theme_location' => 'primary', 'items_wrap' => '%3$s' ) ); ?></ul>
In their place we put these lines:
<ul class="navbar-nav ml-auto"> <?php wp_nav_menu(array( 'menu' => 'Top Menu', 'items_wrap'=>'%3$s', 'container' => false, )); ?></ul>
This will remove the div
tag and the duplication of the ul
tag for us, but we still need to apply nav-item
and nav-link
to our menu items (to li
and a
tags respectively). How will we go about this? wp_nav_menu
does not provide arguments for this. We’ll use the nav_menu_link_attributes
and nav_menu_css_class
filter hooks. We put this into our functions.php
file:
function add_menu_link_class( $atts, $item, $args ) { if($args->link_class) { $atts['class'] = $args->link_class; } return $atts;}add_filter( 'nav_menu_link_attributes', 'add_menu_link_class', 1, 3 );
function add_menu_list_item_class($classes, $item, $args) { if($args->list_item_class) { $classes[] = $args->list_item_class; } return $classes;}add_filter('nav_menu_css_class', 'add_menu_list_item_class', 1, 3);
Now we can specify new attributes in our wp_nav_menu
in our header.php
:
<ul class="navbar-nav ml-auto"> <?php wp_nav_menu(array( 'menu' => 'Top Menu', 'items_wrap'=>'%3$s', 'container' => false, 'list_item_class' => "nav-item", 'link_class' => "nav-link", )); ?></ul>
Now our top menu links can take advantage of styles already defined in our Bootstrap theme’s CSS.
Dynamic Header
To be able to use a dynamic header — that is, a different header for the front page, for other selected pages, or for archives — we’ll define a dynamic_header()
function in our functions.php
file, where we’ll output our header markup dependent on the page the visitor loads.
So now our header.php
file will end like this:
</nav>
<?php dynamic_header(); ?>
<div class="container"> <div class="row">
We’ll also define that function like this:
if(!function_exists('dynamic_header')){
function dynamic_header(){
global $post;
?>
<?php if (is_front_page()){ ?>
<header class="masthead" style="background:#ccc;"> <div class="overlay"></div> <div class="container"> <div class="row"> <div class="col-lg-8 col-md-10 mx-auto"> <div class="site-heading"> <h1 class="site-title"><a href="<?php echo esc_url( home_url( '/' ) ); ?>" ➥rel="home"><?php bloginfo( 'name' ); ?></a></h1> <span class="subheading"><?php get_bloginfo( 'description', 'display' );?> </span> </div> </div> </div> </div> </header>
<?php } else if (is_home()){ ?>
<header class="masthead" style="background:#ccc;"> <div class="overlay"></div> <div class="container"> <div class="row"> <div class="col-lg-8 col-md-10 mx-auto"> <div class="site-heading"> <h1 class="site-title"><a href="<?php echo esc_url( home_url( '/' ) ); ?>" ➥rel="home"><?php bloginfo( 'name' ); ?></a></h1> <span class="subheading"><?php get_bloginfo( 'description', 'display' );?> </span> </div> </div> </div> </div> </header>
<?php } else if (is_page()){ ?>
<header class="masthead" style="background:#ccc;"> <div class="overlay"></div> <div class="container"> <div class="row"> <div class="col-lg-8 col-md-10 mx-auto"> <div class="site-heading"> <h1 class="site-title"><a href="<?php echo esc_url( home_url( '/' ) ); ?>" ➥rel="home"><?php bloginfo( 'name' ); ?></a></h1> <span class="subheading"><?php get_bloginfo( 'description', 'display' );?> </span> </div> </div> </div> </div> </header>
<?php } else if (is_single()){ ?>
<header class="masthead" style="background:#ccc;"> <div class="overlay"></div> <div class="container"> <div class="row"> <div class="col-lg-8 col-md-10 mx-auto"> <div class="site-heading"> <h1 class="site-title"><a href="<?php echo esc_url( home_url( '/' ) ); ?>" ➥rel="home"><?php bloginfo( 'name' ); ?></a></h1> <span class="subheading"><?php get_bloginfo( 'description', 'display' );?> </span> </div> </div> </div> </div> </header>
<?php } else { ?>
<header class="masthead" style="background:#ccc;"> <div class="overlay"></div> <div class="container"> <div class="row"> <div class="col-lg-8 col-md-10 mx-auto"> <div class="site-heading"> <h1 class="site-title"><a href="<?php echo esc_url( home_url( '/' ) ); ?>" ➥rel="home"><?php bloginfo( 'name' ); ?></a></h1> <span class="subheading"><?php get_bloginfo( 'description', 'display' );?> </span> </div> </div> </div> </div> </header>
<?php }}
}
To be able to use all the current URL or post data — like in the loop — we declare a $post
variable global
. Then we just fill different page or request cases with filler header HTML, which we’ll finish later. This sets the foundation for a truly dynamic header.
We need to make sure that our front page — with dynamic top menu — will look good even when the user is logged in. WordPress shows an admin bar when visitors are logged in, even when they visit the front page. Because it has position: fixed
, it overlays the top zone on our website, covering whatever is there, so we need to specify an offset for our top menu.
We’ll add this to our style.css
:
body.logged-in.admin-bar #mainNav { margin-top: 32px;}
@media screen and (max-width: 782px) { body.logged-in.admin-bar #mainNav { margin-top: 46px; }}
This makes sure the #mainNav
— our menu container — has enough offset from the top, so it isn’t covered when user is logged in. WordPress adds logged-in
and admin-bar
classes to body
in these cases, so we can easily target it.
We can see that we address two cases in our CSS — one default, and another one for smaller screens. This is because WordPress outputs a wider admin bar on mobile devices, so we need to provide a 46px offset.
On mobile, we should now have a responsive, JavaScript-powered dropdown menu:
Conclusion
In this second part on creating a WordPress theme from scratch, we created a very basic WordPress theme, and we included Bootstrap styles and scripts into it. We adjusted the menu output to fit our predefined styles. We also separated header and footer output into their respective partials.
The functions.php
file — a crucial file in theme development — is another topic we introduced and leveraged. Header output has been separated into its own function, which will use particulars of page visit, and site-owner–defined variables to determine the final output.