How to set up a Bootstrap sub-theme in Drupal 8

develop coding web design coding web template

As first step you have to chose one of the Bootstrap starterkit between jsDelivr CDN, Less Starterkit and Sass Starterkit and then copy the entire starterkits folder into the themes directory and then rename this folder with the name of your theme.

As you can see in this folder there are lot of files starting with THEMENAME then I had to replace this string in the file names with my sub-theme's "machine name": "maria_consulting" so the structure of my themes folder became /themes/maria_consulting and it contains all the following files: config/install/maria_consulting.settings.yml config/schema/maria_consulting.schema.yml maria_consulting.info.yml maria_consulting.libraries.yml and maria_consulting.theme

NOTE: I had to rename THEMENAME.starterkit.yml to match maria_consulting.info.yml, this is the way that it looks for me:

core: 8.x
type: theme
base theme: bootstrap
name: 'Maria Consulting'
description: 'Uses the jsDelivr CDN for all CSS and JavaScript. No source files or compiling is necessary and is recommended for simple sites or beginners.'
package: 'Bootstrap'
regions:
  navigation: 'Navigation'
  navigation_collapsible: 'Navigation (Collapsible)'
  header: 'Top Bar'
  highlighted: 'Highlighted'
  help: 'Help'
  content: 'Content'
  sidebar_first: 'Primary'
  sidebar_second: 'Secondary'
  footer: 'Footer'
  page_top: 'Page top'
  page_bottom: 'Page bottom'
libraries:
  - 'maria_consulting/global-styling'

Note that I kept the same regions as in the Base Bootstrap Theme (Navigation, Top Bar, Content, Primary, Seconday, Footer, etc) as there are very generic and more than enough in terms of layout options. I also put in bold the settings which I changed. I also edited config/schema/maria_consulting.schema.yml so that for me it looks like:

# Schema for the theme setting configuration file of the Maria Consulting theme.
maria_consulting.settings:
  type: theme_settings
  label: 'Maria Consulting settings'

Enable Your New Sub-theme

Once the sub theme has been created, in admin/appearance I clicked the Enable and set default link next to my newly created sub-theme and Voila' the new Maria Consulting sub theme is now ACTIVE! Now I'm going to share my experience with you on how I did customize my Bootstrap Sub theme starterkit.

By default your Boostrap sub theme uses all the template twig files defined inside the /themes/bootstrap/templates/ folder. This folder contains all these sub folders: block bootstrap field file filter input menu node system and views.

In this article I will focus on how I did overwrite some of these system twigs templates. I created this folder inside my theme: /themes/maria_consulting/templates/system, this folder contains these 2 files:
page.html.twig page--service.html.twig. This is how it looks like my page.html.twig file:

{% set container = theme.settings.fluid_container ? 'container-fluid' : 'container' %}
{% include directory ~ '/templates/parts/header.html.twig' %}

{# Main #}
{% block main %}
  <div role="main" class="main-container {{ container }} js-quickedit-main-content">
    <div class="row">

      {# Header #}
      {% if page.header and not is_front %}
        {% block header %}
          <div class="col-sm-12" role="heading">
            {{ page.header }}
          </div>
        {% endblock %}
      {% endif %}

      {# Sidebar First #}
      {% if page.sidebar_first %}
        {% block sidebar_first %}
          <aside class="col-sm-4" role="complementary">
            {{ page.sidebar_first }}
          </aside>
        {% endblock %}
      {% endif %}

      {# Content #}
      {%
        set content_classes = [
          page.sidebar_first and page.sidebar_second ? 'col-sm-4',
          page.sidebar_first and page.sidebar_second is empty ? 'col-sm-8',
          page.sidebar_second and page.sidebar_first is empty ? 'col-sm-8',
          page.sidebar_first is empty and page.sidebar_second is empty ? 'col-sm-12'
        ]
      %}
      <section{{ content_attributes.addClass(content_classes) }}>

        {# Highlighted #}
        {% if page.highlighted %}
          {% block highlighted %}
            <div class="highlighted">{{ page.highlighted }}</div>
          {% endblock %}
        {% endif %}

        {# Action Links #}
        {% if action_links %}
          {% block action_links %}
            <ul class="action-links">{{ action_links }}</ul>
          {% endblock %}
        {% endif %}

        {# Help #}
        {% if page.help %}
          {% block help %}
            {{ page.help }}
          {% endblock %}
        {% endif %}

        {# Content #}
        {% block content %}
          <a id="main-content"></a>
          {{ page.content }}
        {% endblock %}
      </section>

      {# Sidebar Second #}
      {% if page.sidebar_second %}
        {% block sidebar_second %}
          <aside class="col-sm-4" role="complementary">
            {{ page.sidebar_second }}
          </aside>
        {% endblock %}
      {% endif %}
    </div>
  </div>
{% endblock %}

{% if is_front %}
{% include directory ~ '/templates/parts/home_page.html.twig' %}
{% endif %}

{% include directory ~ '/templates/parts/footer.html.twig' %}

As you can see there is nothing particular special in this file as it is very similar to the original one, the only extra thing I added was a bit of modularity by creating twig parts, for example: {% include directory ~ '/templates/parts/footer.html.twig' %} this is the equivalent in Drupal 7 to use include inside your tpl.php file, but I found it as solution more elegant and clean.

NOTE: I decided to place my twig parts inside this folder: /themes/maria_consulting/templates/parts as they do not need to be necessary placed inside templates/system/parts as the inclusion {% include directory ~ '/templates/parts/footer.html.twig' %} specify a absolute path inside your theme!

In Drupal 7 we were used to implement all our theme preprocess hooks inside the template.php files, but in Drupal 8 things works slightly differntly. The equivalent of maria_consulting/template.php is maria_consulting/maria_consulting.theme

maria_consulting.theme may contains all the PHP code for all your custom HOOKS such as maria_consulting_preprocess_page(), maria_consulting_preprocess_field(), maria_consulting_preprocess_views_view_field(), etc..

But placing all the PHP code inside your MYTHEME.theme file it is not a very good approach in Drupal 8 and it seems almost identical to the way we were used to do it in Drupal 7! But as Drupal 8 is built in Symfony, you should never put your logic inside this file, but instead you should start defining your custom classes (in Drupal 8 they are called Plugins) that implements methods to serve the code for all these hooks!

Actually in a Bootstrap Sub theme you do not even need to set to set up all these classes as you can use the default behaviour which is invoked inside this static class: /themes/bootstrap/src/Bootstrap.php:

<?php
/**
 * @file
 * Contains \Drupal\bootstrap\Bootstrap.
 */

namespace Drupal\bootstrap;

use Drupal\bootstrap\Plugin\AlterManager;
use Drupal\bootstrap\Plugin\FormManager;
use Drupal\bootstrap\Plugin\PreprocessManager;
use Drupal\bootstrap\Utility\Element;
use Drupal\bootstrap\Utility\Unicode;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Drupal\Core\Form\FormStateInterface;

/**
 * The primary class for the Drupal Bootstrap base theme.
 *
 * Provides many helper methods.
 *
 * @ingroup utility
 */
class Bootstrap {

// ......More code....

  /**
   * Preprocess theme hook variables.
   *
   * @param array $variables
   *   The variables array, passed by reference.
   * @param string $hook
   *   The name of the theme hook.
   * @param array $info
   *   The theme hook info.
   */
  public static function preprocess(array &$variables, $hook, array $info) {
    static $theme;
    if (!isset($theme)) {
      $theme = self::getTheme();
    }
    static $preprocess_manager;
    if (!isset($preprocess_manager)) {
      $preprocess_manager = new PreprocessManager($theme);
    }

    // Adds a global "is_front" variable back to all templates.
    // @see https://www.drupal.org/node/2829585
    if (!isset($variables['is_front'])) {
      $variables['is_front'] = static::isFront();
      if (static::hasIsFrontCacheContext()) {
        $variables['#cache']['contexts'][] = 'url.path.is_front';
      }
    }

    // Ensure that any default theme hook variables exist. Due to how theme
    // hook suggestion alters work, the variables provided are from the
    // original theme hook, not the suggestion.
    if (isset($info['variables'])) {
      $variables = NestedArray::mergeDeepArray([$info['variables'], $variables], TRUE);
    }

    // Add active theme context.
    // @see https://www.drupal.org/node/2630870
    if (!isset($variables['theme'])) {
      $variables['theme'] = $theme->getInfo();
      $variables['theme']['dev'] = $theme->isDev();
      $variables['theme']['livereload'] = $theme->livereloadUrl();
      $variables['theme']['name'] = $theme->getName();
      $variables['theme']['path'] = $theme->getPath();
      $variables['theme']['title'] = $theme->getTitle();
      $variables['theme']['settings'] = $theme->settings()->get();
      $variables['theme']['has_glyphicons'] = $theme->hasGlyphicons();
      $variables['theme']['query_string'] = \Drupal::getContainer()->get('state')->get('system.css_js_query_string') ?: '0';
    }

    // Invoke necessary preprocess plugin.
    if (isset($info['bootstrap preprocess'])) {
      if ($preprocess_manager->hasDefinition($info['bootstrap preprocess'])) {
        $class = $preprocess_manager->createInstance($info['bootstrap preprocess'], ['theme' => $theme]);
        /** @var \Drupal\bootstrap\Plugin\Preprocess\PreprocessInterface $class */
        $class->preprocess($variables, $hook, $info);
      }
    }
  }

}

Here I want to highlights this logic, as you can see this public static preprocess method is used inside bootstrap.theme line 117:

/**
 * {@inheritdoc}
 *
 * @see \Drupal\bootstrap\Bootstrap::preprocess()
 */
function bootstrap_preprocess(&$variables, $hook, $info) {
  Bootstrap::preprocess($variables, $hook, $info);
}

Basically with this meccanism all the preprocess hooks go thought Bootstrap::preprocess(). If you look at the implementation of this method inside /themes/bootstrap/src/Bootstrap.php a line 1141 I saw something that I really liked, please look very carefully at this IF statement:

// Invoke necessary preprocess plugin.
if (isset($info['bootstrap preprocess'])) {
  if ($preprocess_manager->hasDefinition($info['bootstrap preprocess'])) {
    $class = $preprocess_manager->createInstance($info['bootstrap preprocess'], ['theme' => $theme]);
    /** @var \Drupal\bootstrap\Plugin\Preprocess\PreprocessInterface $class */
    $class->preprocess($variables, $hook, $info);
  }
}

Basically this function use $preprocess_manager = new PreprocessManager($theme); which is an instance of PreprocessManager($theme);! The PreprocessManager checks if it needs to Invoke a necessary preprocess plugin that implements that particular HOOK Preprocess!

Auto loading a class dynamically is a very powerful PHP functionality which is very well described in the PSR-4 (PHP Standard Recommendation - 4 for Autoloading Standard). The principle is very simple: PreprocessManager is expecting to load a class that implement this Interface defined in /themes/bootstrap/src/Plugin/Preprocess:

<?php
/**
 * @file
 * Contains \Drupal\bootstrap\Plugin\Preprocess\PreprocessInterface.
 */

namespace Drupal\bootstrap\Plugin\Preprocess;

/**
 * Defines the interface for an object oriented preprocess plugin.
 *
 * @ingroup plugins_preprocess
 */
interface PreprocessInterface {

  /**
   * Preprocess theme hook variables.
   *
   * @param array $variables
   *   The variables array, passed by reference (modify in place).
   * @param string $hook
   *   The name of the theme hook.
   * @param array $info
   *   The theme hook info array.
   */
  public function preprocess(array &$variables, $hook, array $info);

}

As long as there is a Plugin (a PHP Class) that implement that Interface, the PreprocessManager will be able to load that class and dynamically invoke that method!

If you look carefully at the structure of the Bootstrap Theme there are already lot of available Plugins inside the /themes/bootstrap/src/Plugin/Preprocess folder that already implement that Interface:

BootstrapCarousel.php Breadcrumb.php FilterTips.php InputButton.php Page.php Region.php BootstrapDropdown.php FieldMultipleValueForm.php FormElementLabel.php Input.php PreprocessBase.php Select.php BootstrapModal.php FileLink.php FormElement.php Links.php PreprocessInterface.php Table.php BootstrapPanel.php FileUploadHelp.php ImageWidget.php MenuLocalAction.php ProgressBar.php ViewsViewTable.php

It is very Obvious to Guess what each class does! But if you want to overwrite these Behaviours please do not change directly the code inside these Classes!

For Maria Consulting I had to implement some custom hook for Page Preprocess, this is the way I did it: I set up this Plugin in my theme folder /themes/maria_consulting/src/Plugin/Preprocess:

<?php
/**
 * @file
 * Contains \Drupal\maria_consulting\Plugin\Preprocess\Page.
 */

namespace Drupal\maria_consulting\Plugin\Preprocess;

use Drupal\bootstrap\Annotation\BootstrapPreprocess;
use Drupal\bootstrap\Utility\Element;
use Drupal\bootstrap\Utility\Variables;
use Drupal\Core\Render\Markup;
use Drupal\maria_consulting\MariaConsulting;

/**
 * Pre-processes variables for the "page" theme hook.
 * Please see https://drupal-bootstrap.org/api/bootstrap/docs%21plugins%21Preprocess.md/group/plugins_preprocess/8
 *
 * @ingroup plugins_preprocess
 *
 * @BootstrapPreprocess("page")
 */
class Page extends \Drupal\bootstrap\Plugin\Preprocess\Page {

  /**
   * {@inheritdoc}
   */
  public function preprocess(array &$variables, $hook, array $info)
  {
    $is_front = \Drupal::service('path.matcher')->isFrontPage();
    if($is_front){
      $my_tids = MariaConsulting::getMyServiceIDs();
      $tags_array = MariaConsulting::getServicesDetails();
      $variables['more_services'] = MariaConsulting::getMoreServices($tags_array, array(), $my_tids);

    }elseif ($node = \Drupal::routeMatch()->getParameter('node')) {
      $content_type = $node->bundle();
      if ($content_type == "service" && isset($node->field_tags)) {
        // Set the node ID if we're on a node page.
        $nid = isset($variables['node']) ? $variables['node']->id() : '';

        $field_tags = $node->get('field_tags');
        $my_tags_list = $field_tags->getValue();

        $my_tids = array();
        foreach($my_tags_list as $term){
          $my_tids[] = $term['target_id'];
        }

        $my_special_services = MariaConsulting::getSpecialServiceIDs();       
        if(!in_array($nid, $my_special_services)){
          $tags_array = MariaConsulting::getServicesDetails();
          $special_services = MariaConsulting::getSpecialServices();
          $variables['more_services'] = MariaConsulting::getMoreServices($tags_array, $special_services, $my_tids);
        }else{
          $variables['more_services'] = false;
        }

      }

      // For webform we need to print the body in a different place:
      if ($content_type == "webform" && isset($node->body)) {
        $webform = $node->get('webform');
        $iterator = $webform->getIterator();
        $element = $iterator->offsetGet(0);
        $raw_html = render($element->view());
        $variables['node_webform'] = Markup::create($raw_html);
      }
    }
    parent::preprocess($variables, $hook, $info);
  }

}

As you can see my Page Plugin extends \Drupal\bootstrap\Plugin\Preprocess\Page and overwrite the preprocess method. As I'm extending and overriding this preprocess method from the Bootstrap base theme, it is imperative that I also had to call the parent (base theme) method after my custom preprocessing!

All the Alter Plugin follow the same logic that I already described for the preprocess Plugins. If you look at public static function alter defined inside the /themes/bootstrap/src/Bootstrap.php Class at line 173:

public static function alter($function, &$data, &$context1 = NULL, &$context2 = NULL) {
  static $theme;
  if (!isset($theme)) {
    $theme = self::getTheme();
  }

  // Immediately return if the active theme is not Bootstrap based.
  if (!$theme->isBootstrap()) {
    return;
  }

  // Extract the alter hook name.
  $hook = Unicode::extractHook($function, 'alter');

  // Handle form alters as a separate plugin.
  if (strpos($hook, 'form') === 0 && $context1 instanceof FormStateInterface) {
    // some code to handle forms...
  }
  // Process hook alter normally.
  else {
    // Retrieve a list of alter definitions.
    $alter_manager = new AlterManager($theme);

    /** @var \Drupal\bootstrap\Plugin\Alter\AlterInterface $class */
    if ($alter_manager->hasDefinition($hook) && ($class = $alter_manager->createInstance($hook, ['theme' => $theme]))) {
      $class->alter($data, $context1, $context2);
    }
  }
}

I wanted to just highlight the logic behind the $alter_manager, basically this Class checks if there is a Plugin that Implement this interface:

<?php
/**
 * @file
 * Contains \Drupal\bootstrap\Plugin\Alter\AlterInterface.
 */

namespace Drupal\bootstrap\Plugin\Alter;

/**
 * Defines the interface for an object oriented alter.
 *
 * @ingroup plugins_alter
 */
interface AlterInterface {
  public function alter(&$data, &$context1 = NULL, &$context2 = NULL);
}

As usual I set up all my custom Alter Plugins inside my /themes/maria_consulting/src/Plugin/Alter folder, for example my ThemeSuggestions Plugin looks like:

<?php
/**
 * @file
 * Contains \Drupal\maria_consulting\Plugin\Alter\ThemeSuggestions.
 */

namespace Drupal\maria_consulting\Plugin\Alter;

use Drupal\bootstrap\Annotation\BootstrapAlter;
use Drupal\bootstrap\Bootstrap;
use Drupal\bootstrap\Plugin\PluginBase;
use Drupal\bootstrap\Utility\Unicode;
use Drupal\bootstrap\Utility\Variables;
use Drupal\Core\Entity\EntityInterface;

/**
 * Implements hook_theme_suggestions_alter().
 *
 * @ingroup plugins_alter
 *
 * @BootstrapAlter("theme_suggestions")
 */
class ThemeSuggestions extends \Drupal\bootstrap\Plugin\Alter\ThemeSuggestions {

  /**
   * {@inheritdoc}
   */
  public function alter(&$suggestions, &$variables = [], &$hook = NULL) {
    // Add some custom suggestions:
    if ($hook == 'page' && ($node = \Drupal::routeMatch()->getParameter('node'))) {
      $content_type = $node->bundle();
      $suggestions[] = 'page__'.$content_type;
    }
    parent::alter($suggestions, $variables, $hook);
  }

}

As you can see ThemeSuggestions extends \Drupal\bootstrap\Plugin\Alter\ThemeSuggestions and at the end it calls parent::alter($suggestions, $variables, $hook);. This Plugin was necessary in my Sub Theme because how I did mention earlier I had to set up this new twig template file: page--service.html.twig and my ThemeSuggestions Plugin let Drupal know that I want to use this twig file when the bundle is "service"!

1. Multimedia

video marketing audio video market interactive channels business-media

Drupal evolved in the recent years, the latest release follows the most recent and advanced web development techniques to embed Audio and Video.

2. Management

content concept on laptop screen

Manage your content with Drupal: create as many content types as you like, hundreds of Taxonomies, Nodes and plenty of views to disply them!

3. Consulting

tower bridge on bright sunny day

Contact the Drupal Experts for a free consultation, get a fast and effective support. Get your custom solution, we can create the module that does the job.

4. eCommerce

parcel with shopping cart logo in trolley on laptop

Drupal 8 is very secure when it comes to login, payments, basket, products. Recently I used it in Enterprise Financial Solutions: OpenBanking and P2P.