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"!