Learning Sage starter theme for WordPress

Creating custom WordPress websites with sage 9 starter theme in 2019 is a fun and modern way to develop. Lets face it, building web apps or sites with modern technologies such as React, Angular or Vue has a nice development experience. This is what I have found with sage 9 starter theme has done for my development experience within WordPress.

When first diving in to the theme’s file and folders (it’s architecture) you soon feel at home, if coming from Laravel or even a JavaScript background. The guys at sage have done a great job in creating this starter theme, with a fresh modern workflow experience, this is what WordPress has been lacking for a while. So diving in and learning this theme may be difficult for some, but worth it in the end. Just like learning anything really, put in the time and reap the rewards, and once you have created a few sites with sage you won’t look back. But I will say, if you are new to WordPress theme development, something like underscores or understrap may be a better starting theme and then make the switch to sage (well that’s how I did it).

Why would you use Sage starter theme for WordPress?

Here is a couple of reasons why I started using sage starter theme over other starter themes in WordPress.

1. The modern layout and feel, from the folder structures to the built in tools. With tools already added for your convenience, you can start coding you custom theme within minutes. Webpack compiles (minfies) all you js and css at build time.

2. The introduction of Blade templates, this is a dynamic way of building your themes template files with blade keeping your code DRY (don’t repeat yourself). Learn more about blade templates here

3. Baked in CSS libraries like Bootstrap, Foundation or Tailwind. No configuration (enqueue_scripts) needed to add your CSS library (all done for you).

4. Adding your dependencies with ease using npm, for fontawesome svg’s via the advanced JavaScript API to take advantage of tree shaking. Learn more here

Bootstrap 4 with Sage starter theme

When installing sage starter theme I choose bootstrap 4 as my CSS library. While the project files are building you are prompt to choose a library, or you could choose none if you wish. Once all the project files are loaded you can start using bootstrap classes straight out of the gate. The CSS file structure in sage is awesome it does use SCSS, so using variables and all that good stuff is available.

Installing Bootstrap 4 Nav-Walker for your sage theme

To include the bootstrap 4 navigation responsive menu to your sage theme, add the following files:

Step 1. Locate your header.php file

 views/partials/header.php 
<header class="banner">
  <div class="container">
    <nav class="navbar navbar-expand-lg navbar-light bg-light" role="navigation">
      <button class="navbar-toggler" type="button" data-toggle="collapse"
              data-target="#navbarSupportedContent" aria-controls="bs-example-navbar-collapse-1" aria-expanded="false"
              aria-label="Toggle navigation">
        <span class="navbar-toggler-icon"></span>
      </button>
      <a class="navbar-brand" href="{{ home_url('/') }}">{{ get_bloginfo('name', 'display') }}</a>
      <div class="collapse navbar-collapse nav-primary ml-auto"  id="navbarSupportedContent">
        @if (has_nav_menu('primary_navigation'))
          {!! wp_nav_menu(['theme_location' => 'primary_navigation', 'walker' => new wp_bootstrap_navwalker(), 'menu_class' => 'navbar-nav nav ml-auto']) !!}
        @endif
      </div>
      <!-- <form class="form-inline collapse navbar-collapse pull-right">
      <button class="btn btn-outline-success" type="button">Main button</button>
      </form> -->
    </nav>
  </div>
</header>

Bootstrap Nav-Walker class

Next, you add the custom wp_bootstrap_navwalker class create a file in the app directory named wp_bootstrap_navwalker.

app/wp_bootstrap_navwalker.php

<?php
/**
 * Class Name: wp_bootstrap4_navwalker
 * GitHub URI: https://github.com/twittem/wp-bootstrap-navwalker
 * Description: A custom WordPress nav walker class to implement the Bootstrap 3 navigation style in a custom theme using the WordPress built in menu manager.
 * Version: 2.0.4
 * Author: Edward McIntyre - @twittem
 * License: GPL-2.0+
 * License URI: http://www.gnu.org/licenses/gpl-2.0.txt
 **/

 class wp_bootstrap_navwalker extends Walker_Nav_Menu {
    /**
     * @see Walker::start_lvl()
     * @since 3.0.0
     *
     * @param string $output Passed by reference. Used to append additional content.
     * @param int $depth Depth of page. Used for padding.
     */
    public function start_lvl( &$output, $depth = 0, $args = array() ) {
        $indent = str_repeat( "\t", $depth );
        $output .= "\n$indent<div role=\"menu\" class=\" dropdown-menu\">\n";
    }
    /**
     * Ends the list of after the elements are added.
     *
     * @see Walker::end_lvl()
     *
     * @since 3.0.0
     *
     * @param string $output Passed by reference. Used to append additional content.
     * @param int    $depth  Depth of menu item. Used for padding.
     * @param array  $args   An array of arguments. @see wp_nav_menu()
     */
    public function end_lvl( &$output, $depth = 0, $args = array() ) {
        $indent = str_repeat("\t", $depth);
        $output .= "$indent</div>\n";
    }
    /**
     * Start the element output.
     *
     * @see Walker::start_el()
     *
     * @since 3.0.0
     *
     * @param string $output Passed by reference. Used to append additional content.
     * @param object $item   Menu item data object.
     * @param int    $depth  Depth of menu item. Used for padding.
     * @param array  $args   An array of arguments. @see wp_nav_menu()
     * @param int    $id     Current item ID.
     */
    public function end_el( &$output, $item, $depth = 0, $args = array() ) {
        if($depth === 1){
            if(strcasecmp( $item->attr_title, 'divider' ) == 0 || strcasecmp( $item->title, 'divider') == 0) {
                $output .= '</div>';
            }else if ($depth === 1 && (strcasecmp( $item->attr_title, 'header') == 0 && $depth === 1)) {
                $output .= '</h6>';
            }
        }else{
            $output .= '</li>';
        }
    }
    /**
     * @see Walker::start_el()
     * @since 3.0.0
     *
     * @param string $output Passed by reference. Used to append additional content.
     * @param object $item Menu item data object.
     * @param int $depth Depth of menu item. Used for padding.
     * @param int $current_page Menu item ID.
     * @param object $args
     */
    public function start_el( &$output, $item, $depth = 0, $args = array(), $id = 0 ) {
        $indent = ( $depth ) ? str_repeat( "\t", $depth ) : '';
        /**
         * Dividers, Headers or Disabled
         * =============================
         * Determine whether the item is a Divider, Header, Disabled or regular
         * menu item. To prevent errors we use the strcasecmp() function to so a
         * comparison that is not case sensitive. The strcasecmp() function returns
         * a 0 if the strings are equal.
         */
        //( strcasecmp($item->attr_title, 'disabled' ) == 0 )

        if($depth === 1 && (strcasecmp( $item->attr_title, 'divider' ) == 0 || strcasecmp( $item->title, 'divider') == 0)) {
            $output .= $indent . '<div class="dropdown-divider">';
        }else if ((strcasecmp( $item->attr_title, 'header') == 0 && $depth === 1) && $depth === 1){
            $output .= $indent . '<h6 class="dropdown-header">' . esc_attr( $item->title );
        }else{
            $class_names = $value = '';
            $classes = empty( $item->classes ) ? array() : (array) $item->classes;

            $atts = array();
            $atts['title']  = ! empty( $item->title )	? $item->title	: '';
            $atts['target'] = ! empty( $item->target )	? $item->target	: '';
            $atts['rel']    = ! empty( $item->xfn )		? $item->xfn	: '';
            $atts['href'] = ! empty( $item->url ) ? $item->url : '';
            $id = apply_filters( 'nav_menu_item_id', 'menu-item-'. $item->ID, $item, $args );

            if ( in_array( 'current-menu-item', $classes ) )
                $classes[] = ' active';
            if($depth === 0){
                $classes[] = 'nav-item';
                $classes[] = 'nav-item-' . $item->ID;
                $atts['class']			= 'nav-link';
                if ( $args->has_children ){
                    $classes[] = ' dropdown';
                    $atts['href']   		= '#';
                    $atts['data-toggle']	= 'dropdown';
                    $atts['class']			= 'dropdown-toggle nav-link';
                    $atts['role']	= 'button';
                    $atts['aria-haspopup']	= 'true';
                }
                $class_names = join( ' ', apply_filters( 'nav_menu_css_class', array_filter( $classes ), $item, $args ) );
                $class_names = $class_names ? ' class="' . esc_attr( $class_names ) . '"' : '';
                $id = $id ? ' id="' . esc_attr( $id ) . '"' : '';
                $output .= $indent . '<li' . $id . $value . $class_names .'>';
            }else{
                $classes[] = 'dropdown-item';
                $class_names = join( ' ', apply_filters( 'nav_menu_css_class', array_filter( $classes ), $item, $args ) );
                $atts['class'] = $class_names;
                $atts['id'] = $id;
            }

            $atts = apply_filters( 'nav_menu_link_attributes', $atts, $item, $args );
            $attributes = '';
            foreach ( $atts as $attr => $value ) {
                if ( ! empty( $value ) ) {
                    $value = ( 'href' === $attr ) ? esc_url( $value ) : esc_attr( $value );
                    $attributes .= ' ' . $attr . '="' . $value . '"';
                }
            }
            $item_output = $args->before;
            $item_output .= '<a'. $attributes .'>';

            /*
             * Icons
             * ===========
             * Since the the menu item is NOT a Divider or Header we check the see
             * if there is a value in the attr_title property. If the attr_title
             * property is NOT null we apply it as the class name for the icon
             */
            if ( ! empty( $item->attr_title ) ){
                $item_output .= '<span class="' . esc_attr( $item->attr_title ) . '"></span> ';
            }
            $item_output .= $args->link_before . apply_filters( 'the_title', $item->title, $item->ID ) . $args->link_after;
            $item_output .= '</a>';
            $item_output .= $args->after;
            $output .= apply_filters( 'walker_nav_menu_start_el', $item_output, $item, $depth, $args );
        }
    }
    /**
     * Traverse elements to create list from elements.
     *
     * Display one element if the element doesn't have any children otherwise,
     * display the element and its children. Will only traverse up to the max
     * depth and no ignore elements under that depth.
     *
     * This method shouldn't be called directly, use the walk() method instead.
     *
     * @see Walker::start_el()
     * @since 2.5.0
     *
     * @param object $element Data object
     * @param array $children_elements List of elements to continue traversing.
     * @param int $max_depth Max depth to traverse.
     * @param int $depth Depth of current element.
     * @param array $args
     * @param string $output Passed by reference. Used to append additional content.
     * @return null Null on failure with no changes to parameters.
     */
    public function display_element( $element, &$children_elements, $max_depth, $depth, $args, &$output ) {
        if ( ! $element )
            return;
        $id_field = $this->db_fields['id'];
        // Display this element.
        if ( is_object( $args[0] ) )
            $args[0]->has_children = ! empty( $children_elements[ $element->$id_field ] );
        parent::display_element( $element, $children_elements, $max_depth, $depth, $args, $output );
    }
    /**
     * Menu Fallback
     * =============
     * If this function is assigned to the wp_nav_menu's fallback_cb variable
     * and a menu has not been assigned to the theme location in the WordPress
     * menu manager the function with display nothing to a non-logged in user,
     * and will add a link to the WordPress menu manager if logged in as an admin.
     *
     * @param array $args passed from the wp_nav_menu function.
     *
     */
    public static function fallback( $args ) {
        if ( current_user_can( 'manage_options' ) ) {
            extract( $args );
            $fb_output = null;
            if ( $container ) {
                $fb_output = '<' . $container;
                if ( $container_id )
                    $fb_output .= ' id="' . $container_id . '"';
                if ( $container_class )
                    $fb_output .= ' class="' . $container_class . '"';
                $fb_output .= '>';
            }
            $fb_output .= '<ul';
            if ( $menu_id )
                $fb_output .= ' id="' . $menu_id . '"';
            if ( $menu_class )
                $fb_output .= ' class="' . $menu_class . '"';
            $fb_output .= '>';
            $fb_output .= '<li><a href="' . admin_url( 'nav-menus.php' ) . '">Add a menu</a></li>';
            $fb_output .= '</ul>';
            if ( $container )
                $fb_output .= '</' . $container . '>';
            echo $fb_output;
        }
    }
}

Include Custom Walker Class in fuctions.php

Last you need to only add the ‘wp_bootstrap_navwalker’ file in the array_map function (last line). This array already exists in your themes function.php

Add only ‘ wp_bootstrap_navwalker ‘

resources/functions.php

/**
 * Sage required files
 *
 * The mapped array determines the code library included in your theme.
 * Add or remove files to the array as needed. Supports child theme overrides.
 */
array_map(function ($file) use ($sage_error) {
    $file = "../app/{$file}.php";
    if (!locate_template($file, true, true)) {
        $sage_error(sprintf(__('Error locating <code>%s</code> for inclusion.', 'sage'), $file), 'File not found');
    }
}, ['helpers', 'setup', 'filters', 'admin', 'wp_bootstrap_navwalker']);

Now your bootstrap 4 responsive navigation is properly installed into the sage starter theme, now just add you classes and styles.

Get a free proposal for your business