Dummy’s Guide to Roots Sage Theme in WordPress

Recently I’ve mostly been using Understrap as a theme for WordPress, however the Understrap GitHub repository has shown a disturbing lack of maintenance for the last year or so. With Bootstrap 5 on the horizon, and WordPress and PHP constantly moving forward, I don’t want to be stuck in a potential development dead-end using Understrap. If you are using Understrap, don’t panic — it’s still a viable option — but it is worth looking at your options in case it ceases to be in the future.

One of the alternatives that I’ve seen mentioned is the Sage starter theme, so I decided to have a go porting the theme for this website from Understrap to Sage. This article aims to be a rough guide for people getting started with Sage, especially if they’re coming from Understrap or a similar bare-bones Bootstrap-based theme for WordPress.

  1. Conventions
  2. Concepts
  3. Installing a Fresh Sage
  4. Building the Theme
  5. Customizing the Templates

    1. Layouts
    2. Templates
    3. Custom Templates
    4. A Bootstrap Header and Navbar
  6. Styling

    1. Bootstrap Tweaking
    2. Custom Components
    3. Sage Linting
  7. Scripting
  8. Advanced Custom Fields
  9. Conclusion


  • command — something typed at the command line.
  • filename — a filename. If it starts with ./, it is a filename relative to the theme’s root directory.
  • code — PHP, Javascript, SASS, or CSS code.

I’ll be assuming you have command-line access to the machine you’ll be developing the theme on, and it’s running some flavour of Debian (Ubuntu, for example).


Typically with Understrap, you’d upload the Understrap theme as-is, with no modifications at all if possible, then make a copy of the Understrap Child theme, and add your customizations to that. With Sage, we’re not going to do that. We install a copy of Sage into a directory, and make our modifications to it directly. How this works out long term, trying to keep up to date with newer versions of Sage, well, that is a topic for a future blog.

With Understrap Child, you’d run gulp compile and that would compile your theme into a directory called ./dist, and you would deploy that as your theme. With Sage, you will deploy the whole project directory as your theme. This will likely include some files (such as the entire contents of the ./node_modules subdirectory) which aren’t actually needed in the deployed theme. You can choose whether to delete them from the publicly deployed copy (but keep them in your working copy) or just leave them in. Retaining them shouldn’t cause you any issues, but take care to avoid leaving any hard-coded API keys, passwords, etc buried in your code! Just to be extra confusing, Sage will create a subdirectory called ./dist which will contain your compiled CSS and Javascript, but very little else.

Oh, and Sage will be confusing in one other respect. Most WordPress themes, you’d expect to find files called ./style.css, ./functions.php, and ./screenshot.png. In Sage, these are called ./resources/style.css, ./resources/functions.php, and ./resources/screenshot.png — yes, they’re hidden away in a subdirectory. While unorthodox, WordPress is able to find them there. Just leave them there.

Installing a Fresh Sage

First off, you need to make sure you have PHP Composer installed. Run composer --version and check you have it. If you don’t, then install it. Now run:

$ composer create-project roots/sage

Composer will download a bunch of stuff and then take you through a setup wizard. Most of the questions should have fairly obvious answers. When it asks you which framework you want to use, choose Bootstrap. And when it asks if you want to send them anonymous usage data, I recommend choosing “no”, because I’ve found it tends to hang and time out if you choose “yes”. 🤷

It will have created a directory for you, probably called sage but you can rename it whatever you like. I recommend checking this into a source code repository; perhaps using GitHub.

Next you need to cd into that directory, and unless otherwise stated, all subsequent commands will be executed inside that directory.

Now you need to make sure you’ve got yarn installed. Yarn is kind of like a cross between make and apt-get for Node.js projects. Check if you’ve already got yarn by running yarn --version. If not, you should be able to install a local copy of yarn using:

$ npm i yarn
+ yarn@1.22.10
added 1 package and audited 1 package in 1.444s
found 0 vulnerabilities
$ ln -s ./node_modules/yarn/bin/yarn ./yarn

If you don’t have npm then go Google for how to set up Node.js and Node Package Manager first!

If your yarn was installed locally, then in future commands, you’ll need to run ./yarn with a leading dot-slash whenever I say to run yarn.

Finally you will use yarn to install all the other dependencies.

$ yarn install

Building the Theme

It’s very easy to build the theme:

$ yarn build

When you’re ready for production, you should use this instead, which crunches the CSS and Javascript files down a lot smaller:

$ yarn build:production

You will typically need to rebuild after any SASS/Javascript changes.

Customizing the Templates

The templates are written using Blade, which is the templating language of Laravel. This is a powerful template language which allows for PHP to be embedded into it. All templates reside in ./resources/views


Layouts define the top-level structure of your pages. Layouts are not the templates (things like single.php, search.php, etc) themselves though. Rather, the templates will specify which layout they use.

Here’s my main layout, ./resources/views/layouts/app.blade.php:

<!doctype html>
<html {!! get_language_attributes() !!}>
  <body @php body_class() @endphp>
    @php do_action('get_header') @endphp
    <div class="wrap container" role="document">
      @if (App\display_sidebar())
        <div class="content row">
          <main class="main col-md-8">
          <aside class="sidebar col-md-4">
        <div class="content row">
          <main class="main col-md-12">
    @php do_action('get_footer') @endphp
    @php wp_footer() @endphp

A few parts that are worth explaining:

  • @include('partials.blah') — include a partial template from ./resources/views/partials
  • @yield('content') — specifies where the template itself (like single.php) can inject a section of content
  • @php ... @endphp — this is how you embed PHP into a Blade template
  • @if (App\display_sidebar()) — the display_sidebar() function is defined in ./app/helpers.php (which just calls ./app/filters.php) and you can alter it to provide some logic about when to display your main sidebar

Something you’ll notice about the default Sage layouts is that they won’t use standard Bootstrap classes like container and col-md-8. This is because Sage tries to stay CSS-framework-agnostic; assuming you’re using Bootstrap, you’ll need to edit those into the layouts yourself.

You might want to define additional layouts for, for example, a headerless page, a footerless page, a page with the sidebar on the other side, etc.


The templates themselves do a lot less work in Sage than they do in most themes. All a template needs to do is choose a layout, and provide output for any sections the layout wants to @yield. My ./resources/views/page.blade.php is seriously as simple as just this:


  @while(have_posts()) @php the_post() @endphp

Easy peasy. Sage already comes with most of the standard WordPress templates you need, and you likely won’t even need to customize them!

Custom Templates

WordPress allows you to provide custom templates for particular pages on your site; like your home page or “About Us” page might have a different look than most of the other pages, and you can choose which template they use from a list of custom named templates. These are pretty easy in Sage. Here’s my ./resources/views/template-blank.blade.php which I can use for pages where I don’t want a header or footer:

  Template Name: Blank


  @while(have_posts()) @php the_post() @endphp

As you can see, the only differences it has from standard pages is the special comment at the top to give it a name, and it uses a different layout — @extends('layouts.blank') indicates that it uses the layout in ./resources/views/layouts/blank.blade.php.

A Bootstrap Header and Navbar

Most of the Sage partials should be pretty easy, but if you want a Bootstrap-style header, here’s a good place to start:

<header class="banner">
  <div class="container">
    <div class="navbar navbar-expand-md">
      <a class="navbar-brand" href="{{ home_url('/') }}">{{ get_bloginfo('name', 'display') }}</a>
      <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar-primary" aria-controls="navbar-primary" aria-expanded="false" aria-label="Toggle navigation">
        <span class="navbar-toggler-icon"><i class="fa fa-bars"></i></span>
      @if (has_nav_menu('primary_navigation'))
        {!! wp_nav_menu($primarymenu) !!}

Note that I’m using Font Awesome for the mobile menu icon; you can use something else if you prefer.

The wp_nav_menu($primarymenu) part will call back to ./app/Controllers/App.php to display the menu. You’ll need to add a method there to do that:

public function primarymenu () {
    $args = [
        'theme_location'    => 'primary_navigation',
        'depth'             => 2,
        'container'         => 'div',
        'container_class'   => 'collapse navbar-collapse',
        'container_id'      => 'navbar-primary',
        'menu_class'        => 'navbar-nav ml-auto',
        'fallback_cb'       => 'wp_bootstrap4_navwalker::fallback',
        'walker'            => new \App\wp_bootstrap4_navwalker(),
    return $args;

And you’ll need to install a walker to provide Bootstrap-4-style code for the menu.

$ composer require "mwdelaney/sage-bootstrap4-navwalker"

Of course, if you don’t want a boring Bootstrap-style header like I’ve got, then feel free to get creative with your header!


Your SASS styles are in ./resources/assets/styles. These get compiled into ./dist by Yarn.

Bootstrap Tweaking

To start with, you probably want to tweak bootstrap with your preferred colours, fonts, etc.

If you’re importing fonts and other external CSS files, add them to ./resources/assets/styles/common/_global.scss. Mine includes a few Google Font API @imports plus:

@import url("https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css");

Next set variables in ./resources/assets/styles/common/_variables.scss. Obvious things to set include the primary and secondary colour, fonts, etc. (In Understrap child, the equivalent would be ./sass/theme/_child_theme_variables.scss.)

$primary:   #b5d5a1;
$secondary: #f2cab7;

$font-family-display:    "Oswald", "Impact", sans-serif;
$font-family-sans-serif: "Raleway", "Helvetica Neue", "Arial", sans-serif;
$headings-font-family:   $font-family-display;

After changing styles, remember to run:

$ yarn build

Custom Components

You will likely have your own site-specific styles and tweaks which don’t really fit into _variables.scss. You can add these as custom components. Create a new SASS partial in ./resources/assets/styles/components and link to it in ./resources/assets/styles/main.css.

For tweaks to your header and footer partials, and other standard sections of the site, see ./resources/assets/styles/layouts too — sometimes a tweak might fit in better there.

Remember that all your SASS gets compiled into a single CSS file, so you can break things up and make them modular — this won’t result in lots of additional HTTP requests for the client.

(In Understrap child, you would likely be putting these into ./sass/theme/_child_theme.scss.)

Sage Linting

Sage enforces some pretty restrictive syntax rules on your SASS via a linter. Personally, I found this more annoying than helpful. Here’s how to disable that.


This is the area I’ve played around with the least. In ./resources/assets/scripts/routes you will find two files home.js and common.js where I would recommend doing the bulk of your scripting. Each of these contains an init() function which will run as soon as the DOM has loaded; it is blank, so you can define the contents! They also contain a finalize() function which runs after init(). home.js runs on just the homepage, and common.js runs on all pages (including the homepage).

You can define other routes for other specific pages if you need to, but it’s likely those two will be sufficient.

Well-behaved third-party Javascript libraries can be imported in ./resources/assets/scripts/main.js or placed in ./resources/assets/scripts/autoload. You’ll find that jQuery and Bootstrap are already there. I’m not 100% sure when one technique to load them is preferred over the other.

Advanced Custom Fields

There are no doubt other ways I could do this, but it has long been a habit of mine to define a few fields using Advanced Custom Fields and use them in my themes. Here’s how we’ll do it.

First make sure the ACF WordPress plugin is installed and activated. Once we’ve started depending on it in the theme, WordPress will not be happy if it’s not available!

Next, we’ll install ACF Builder, which allows fields to be programmatically generated by some very concise PHP.

$ composer require "stoutlogic/acf-builder"

Now add this at the end of ./app/setup.php to automatically load any field sets:

 * Initialize ACF Builder
add_action('init', function () {
    collect(glob(config('theme.dir').'/app/fields/*.php'))->map(function ($field) {
        return require_once($field);
    })->map(function ($field) {
        if ($field instanceof FieldsBuilder) {

And now we create a directory called ./app/fields where our fieldsets use.

Here’s the fieldset I’m using, which applies to WordPress posts and pages. It’s called ./app/fields/page.php. Check out the ACF Builder wiki for more options to build custom fields.


namespace App;

use StoutLogic\AcfBuilder\FieldsBuilder;

$page = new FieldsBuilder('page');

    ->setLocation('post_type', '==', 'post')
    ->or('post_type', '==', 'page');

    ->addTrueFalse('hide_title', [
        'label' => 'Hide Title',

    ->addTrueFalse('hide_footer', [
         'label' => 'Hide Footer',

    ->addTrueFalse('hide_breadcrumbs', [
        'label' => 'Hide Breadcrumbs',

    ->addTextarea('custom_javascript', [
        'label' => 'Custom Javascript',

    ->addTextarea('custom_css', [
        'label' => 'Custom CSS',

    ->addImage('fullwidth_banner', [
        'label' => 'Banner Image',
        'return_format' => 'array',

    ->addTextarea('administrator_notes', [
        'label' => 'Admin Notes',

return $page;

And then I will modify my ./resources/views/partials/head.blade.php to be:

  <meta charset="utf-8">
  <meta http-equiv="x-ua-compatible" content="ie=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    $id = get_the_ID();
    if ( is_home() ) {
      $id = get_option( 'page_for_posts' );
    $custom_javascript = get_field( 'custom_javascript', $id );
    $custom_css        = get_field( 'custom_css', $id );
    if ( ! empty($custom_css) ) {
      echo "\n";
      echo '<style id="custom_css" type="text/css">' . $custom_css . '</style>';
    if ( ! empty($custom_javascript) ) {
      echo "\n";
      echo '<script id="custom_javascript" type="text/javascript">' . $custom_javascript . '</script>';

Now each page can have its own custom CSS and Javascript inserted into the <head>.

Implementing the other fields is left as an exercise for the reader.


Sage is a very well-organized theme. Once you know where everything lives, it’s very easy to plug all your custom PHP, Javascript, and CSS into the theme, built it and go. If you are porting a theme from Understrap or another barebones Bootstrap-based WordPress theme, it’s not going to simply be a matter of copying a couple of files from your old theme to your new one, but it’s also not likely to take you many, many days.

This was a pretty long article and took quite a while to write, so if you want to show appreciation, you can buy me a coffee or sponsor me on GitHub.

If you need help updating your WordPress theme to use Sage, or modernizing your WordPress install, I am available for hire at pretty reasonable rates.


If you have any questions or ideas for improving this article, please comment below. You’ll need a GitHub login!