Creating a Drupal 8 theme
from scratch
Devsigner 2015
So you wanna build
a website…
And you want
Step 1:
Step 2:
You’re using a CMS?
Drupal, you say?
What is a theme?
•
Themes control appearance
•
Themes don’t provide functionality
•
Usually specific to a single site
•
Contains HTML (templates), CSS, Javascript, and a
Changes for Drupal 8
•
Drupal 8 loves abstractions
•
Drupal 8 loves themers and front end developers
•
Kinda
•
Documentation is still somewhat sparse
•
Drupal 8 is still changing
Setup for Drupal 8
•
Make sure you use newer versions of Drush, 8.x
minimum
•
Clear the cache, a lot
•
Disable page cache, and CSS/JS aggregation, these are
Themes now go in
themes directory
No more sites/all/
Add a
YAML…
•
YAML Ain’t Markup Language
•
Readable by machines
•
But also readable by humans, which is cool since
key:
value
strings:
"Quoted for special characters or spaces"
collection:
-
"Indented with spaces"
-
"Start with dash (-)"
collections:
can:
"specify key"
nested:
items:
"Work as well"
another:
"See?"
boolean:
true
or:
false
•
Make a directory for your theme with it’s name
•
Add a themename.info.yml file
demo.info.yml
name:
Demo
type:
theme
description:
'A demo theme.'
Wow, that looks like
the web in like 1996
Want to remove some of
Drupal’s default
demo.info.yml
name:
Demo
type:
theme
description:
'A demo theme.'
core:
8.x
libraries:
-
demo/global-styling
stylesheets-remove:
No more drupal_add_css()
or drupal_add_js()
Drupal 8 Libraries
•
Drupal 8 is all about abstraction and reuse, even with
front end code
•
Libraries represent external components
•
Libraries can have CSS, Javascript, and dependencies
•
Examples: normalize, html5shiv, jQuery, modernizer,
Libraries are defined in
themename.libraries.yml
demo.libraries.yml
global-styling:
version:
8.x-1.x
css:
theme:
css/styles.css:
{}
demo.info.yml
name:
Demo
type:
theme
description:
'A demo theme.'
core:
8.x
libraries:
Specify additional
demo.libraries.yml
global-styling:
version:
8.x-1.x
css:
theme:
css/styles.css:
{}
Let’s add some
Javascript
demo.libraries.yml
global-styling:
version:
8.x-1.x
css:
theme:
css/styles.css:
{}
css/print.css:
{
media:
}
js:
js/script.js:
{}
dependencies:
-
core/jquery
Note the dependencies,
jQuery is no longer included
on every page by default
What else can you
do with libraries?
demo.libraries.yml
global-styling:
version:
8.x-1.x
css:
theme:
css/styles.css:
{}
landing-pages:
version:
8.x-1.x
css:
theme:
css/landing-pages.css:
{}
How to include additional
libraries?
•
Specify globally in theme info.yml file
•
Add in preprocess function with #attached
•
Add via Twig function
demo.info.yml
name:
Demo
type:
theme
description:
'A demo theme.'
core:
8.x
libraries:
- demo/global-styling
Where do I put
Wait, Drupal 8 still has
preprocess functions????
Adding a
themes
└── demo
├── css
│ ├── print.css
│ └── styles.css
├── demo.info.yml
├── demo.libraries.yml
└── demo.theme
Think of .theme as a
replacement for
OH MY GOD ITS
FULL OF PHP
demo.theme
<?php
/**
* @file
* Contains preprocess functions for Demo theme.
*/
/**
* Preprocess for node pages.
*/
function
demo_preprocess_node(&
$variables
) {
$variables
[
'#attached'
][
'library'
][] =
'demo/landing-pages'
;
}
Don’t forget to rebuild
caches when changing
Templates, what are
they good for?
What’s so great about Twig
•
Flexible
•
Easy to use
•
Similar to other template systems (jinja2, swig, etc)
•
SECURE
{# All twig tags use braces #}
{# Comment use the hash/pound #}
{{ this_tag_prints_output }}
{# Tags with percent signs, are control blocks #}
{%
if
variable %}
{{ variable }}
{%
endif
%}
{%
set
variable =
'Special value.'
%}
{{ variable|clean_class }}
Example filters
•
Part of Twig — capitalize, date, escape, first, length,
lower, number_format, trim, etc
•
Drupal provided — t, trans, passthrough,
placeholder, drupal_escape, safe_join, without,
clean_class, clean_id, render
{{
'This text will be translated'
|t }}
<title>{{ head_title|safe_join(
' | '
) }}</title>
region.html.twig
{%
set
classes = [
'region'
,
'region-'
~ region|clean_class,
]
%}
{%
if
content %}
<
div
{{ attributes.addClass(classes) }}>
{{ content }}
</
div
>
{%
endif
%}
Working with
Drupal regions
page.html.twig
{%
if
page.sidebar_first %}
<
aside
class=
"layout-sidebar-first"
role=
"complementary"
>
{{ page.sidebar_first }}
</
aside
>
Templates are
cached!
Fast websites are good,
but caching sucks when
Disable render caching in
sites/default/settings.php
sites/default/settings.php
/**
* Disable CSS and JS aggregation.
*/
$config
[
'system.performance'
][
'css'
][
'preprocess'
] =
FALSE
;
$config
[
'system.performance'
][
'js'
][
'preprocess'
] =
FALSE
;
/**
* Disable the render cache (this includes the page cache).
*
* This setting disables the render cache by using the Null cache back-end
* defined by the development.services.yml file above.
*
* Do not use this setting until after the site is installed.
*/
Disable caching in
sites/default/services.yml
parameters:
twig.config:
# Twig auto-reload:
#
# Automatically recompile Twig templates whenever the source code changes.
# If you don't provide a value for auto_reload, it will be determined
# based on the value of debug.
#
# Not recommended in production environments
# @default null
auto_reload:
null
# Twig cache:
#
# By default, Twig templates will be compiled and stored in the filesystem
# to increase performance. Disabling the Twig cache will recompile the
# templates from source each time they are used. In most cases the
# auto_reload setting above should be enabled rather than disabling the
# Twig cache.
#
# Not recommended in production environments
# @default true
sites/default/services.yml
parameters:
twig.config:
# Twig debugging:
#
# When debugging is enabled:
# - The markup of each Twig template is surrounded by HTML comments that
# contain theming information, such as template file name suggestions.
# - Note that this debugging markup will cause automated tests that directly
# check rendered HTML to fail. When running automated tests, 'debug'
# should be set to FALSE.
# - The dump() function can be used in Twig templates to output information
# about template variables.
# - Twig templates are automatically recompiled whenever the source code
# changes (see auto_reload below).
#
# For more information about debugging Twig templates, see
# http://drupal.org/node/1906392.
#
# Not recommended in production environments
# @default false
(after clearing the
cache, of course)
<!-- THEME DEBUG -->
<!-- THEME HOOK: 'menu__main' -->
<!-- FILE NAME SUGGESTIONS:
* menu--main.html.twig
x menu.html.twig
-->
<!-- BEGIN OUTPUT from 'core/modules/system/templates/menu.html.twig' -->
<
ul
class=
"menu"
>
<
li
class=
"menu-item"
>
<
a
href=
"/"
data-drupal-link-system-path=
"
<
front
>
"
>Home</
a
>
</
li
>
</
ul
>
Debug features
•
We can see template suggestions
•
We can see what the default or current used
template is
•
We can also use the dump() function to inspect
themes
└── demo
├── css
│ └── styles.css
├── demo.info.yml
├── demo.libraries.yml
├── demo.theme
└── templates
└── region.html.twig
templates/region.html.twig
{%
set
classes = [
'region'
,
'region-'
~ region|clean_class,
]
%}
{{ dump(attributes) }}
{%
if
content %}
<
div
{{ attributes.addClass(classes) }}>
{{ content }}
</
div
>
{%
endif
%}
The existence of a
template file is still cached,
even with debug enabled :(
templates/node.html.twig
<
article
{{ attributes }}>
{{ title_prefix }}
{%
if not
page %}
<
h2
{{ title_attributes }}>
<
a
href=
"
{{ url }}
"
rel=
"bookmark"
>{{ label }}</
a
>
</
h2
>
{%
endif
%}
{{ title_suffix }}
{%
if
display_submitted %}
<
footer
>
{{ author_picture }}
<
div
{{ author_attributes }}>
{%
trans
%}Submitted by {{ author_name }} on {{ date }}{%
endtrans
%}
{{ metadata }}
</
div
>
</
footer
>
{%
endif
%}
<
div
{{ content_attributes }}>
{{ dump(content) }}
{{ content }}
</
div
>
</
article
>
array (size=5)
'field_image'
=>
array (size=2)
'#cache'
=>
array (size=3)
'contexts'
=>
array (size=0)
...
'tags'
=>
array (size=0)
...
'max-age'
=>
int
-1
'#weight'
=>
int
-1
'body'
=>
array (size=16)
'#theme'
=>
string
'field'
(length=5)
'#title'
=>
string
'Body'
(length=4)
'#label_display'
=>
string
'hidden'
(length=6)
'#view_mode'
=>
string
'full'
(length=4)
'#language'
=>
string
'en'
(length=2)
'#field_name'
=>
string
'body'
(length=4)
'#field_type'
=>
string
'text_with_summary'
(length=17)
'#field_translatable'
=>
boolean
true
'#entity_type'
=>
string
'node'
(length=4)
'#bundle'
=>
string
'article'
(length=7)
<
div
{{ content_attributes }}>
<
div
class="for-the-tags"
>
{{ content.field_tags }}
</
div
>
{{ content|without(
'field_tags'
) }}
</
div
>
But what variables
are defined?
Other fun stuff to
do with templates
{%
import _self as
menus %}
{#
We call a macro which calls itself to render the full tree.
@see http://twig.sensiolabs.org/doc/tags/macro.html
#}
{{ menus.menu_links(items, attributes,
0
) }}
{%
macro
menu_links(items, attributes, menu_level) %}
{%
import _self as
menus %}
{%
if
items %}
{%
if
menu_level ==
0
%}
<
ul
{{ attributes.addClass(
'menu'
) }}>
{%
else
%}
<
ul
class=
"menu"
>
{%
endif
%}
{%
for
item
in
items %}
<
li
{{ item.attributes }}>
{{ link(item.title, item.url) }}
{%
if
item.below %}
{{ menus.menu_links(item.below, attributes, menu_level +
1
) }}
{%
endif
%}
</
li
>
{%
endfor
%}
</
ul
>
{%
endif
%}
{%
endmacro
%}
So you’ve got a
You know how to find
template names,
Not included by
But what if you
liked those?
Enter the Classy
theme
Classy theme
•
Provides a base theme that just adds classes for
other themes to build on
•
Literally only has about 40 lines of CSS
•
No PHP at all
•
But over a hundred templates!
What’s with the
@import url("/core/modules/tour/css/tour.module.css?nqlozl"); </style>
<style media="all">
@import url("/core/modules/contextual/css/contextual.toolbar.css?nqlozl");
@import url("/core/modules/editor/css/editor.css?nqlozl");
@import url("/core/modules/filter/css/filter.caption.css?nqlozl");
@import url("/core/modules/ckeditor/css/plugins/drupalimagecaption/ckeditor.drupalimagecaption.css?nqlozl");
@import url("/core/modules/toolbar/css/toolbar.menu.css?nqlozl");
@import url("/core/modules/ckeditor/css/ckeditor.css?nqlozl");
@import url("/core/modules/contextual/css/contextual.theme.css?nqlozl");
@import url("/core/modules/contextual/css/contextual.icons.theme.css?nqlozl");
@import url("/core/assets/vendor/jquery.ui/themes/base/theme.css?nqlozl");
@import url("/core/misc/dialog.theme.css?nqlozl");
@import url("/core/modules/quickedit/css/quickedit.theme.css?nqlozl");
@import url("/core/modules/quickedit/css/quickedit.icons.theme.css?nqlozl");
@import url("/core/themes/seven/css/components/quickedit.css?nqlozl");
@import url("/core/modules/toolbar/css/toolbar.theme.css?nqlozl");
@import url("/core/modules/toolbar/css/toolbar.icons.theme.css?nqlozl");
@import url("/core/modules/user/css/user.icons.theme.css?nqlozl");
@import url("/core/modules/shortcut/css/shortcut.theme.css?nqlozl");
@import url("/core/modules/shortcut/css/shortcut.icons.theme.css?nqlozl");
@import url("/core/modules/filter/css/filter.admin.css?nqlozl"); </style>
<style media="all">
@import url("/core/themes/bartik/css/base/elements.css?nqlozl");
@import url("/core/themes/bartik/css/layout.css?nqlozl");
@import url("/core/themes/bartik/css/components/admin.css?nqlozl");
@import url("/core/themes/bartik/css/components/block.css?nqlozl");
@import url("/core/themes/bartik/css/components/book.css?nqlozl");
@import url("/core/themes/bartik/css/components/breadcrumb.css?nqlozl");
@import url("/core/themes/bartik/css/components/captions.css?nqlozl");
@import url("/core/themes/bartik/css/components/comments.css?nqlozl");
@import url("/core/themes/bartik/css/components/content.css?nqlozl");
@import url("/core/themes/bartik/css/components/contextual.css?nqlozl");
@import url("/core/themes/bartik/css/components/dropbutton.component.css?nqlozl");
@import url("/core/themes/bartik/css/components/featured-top.css?nqlozl");
@import url("/core/themes/bartik/css/components/feed-icon.css?nqlozl");
@import url("/core/themes/bartik/css/components/form.css?nqlozl");
@import url("/core/themes/bartik/css/components/forum.css?nqlozl");
@import url("/core/themes/bartik/css/components/header.css?nqlozl");
@import url("/core/themes/bartik/css/components/region-help.css?nqlozl");
@import url("/core/themes/bartik/css/components/item-list.css?nqlozl");
@import url("/core/themes/bartik/css/components/list-group.css?nqlozl");
@import url("/core/themes/bartik/css/components/node-preview.css?nqlozl");
@import url("/core/themes/bartik/css/components/pager.css?nqlozl");
@import url("/core/themes/bartik/css/components/panel.css?nqlozl");
@import url("/core/themes/bartik/css/components/primary-menu.css?nqlozl");
@import url("/core/themes/bartik/css/components/search.css?nqlozl");
@import url("/core/themes/bartik/css/components/search-results.css?nqlozl");
@import url("/core/themes/bartik/css/components/secondary-menu.css?nqlozl");
@import url("/core/themes/bartik/css/components/shortcut.css?nqlozl");
@import url("/core/themes/bartik/css/components/skip-link.css?nqlozl");
@import url("/core/themes/bartik/css/components/sidebar.css?nqlozl");
@import url("/core/themes/bartik/css/components/site-footer.css?nqlozl");
@import url("/core/themes/bartik/css/components/table.css?nqlozl"); </style>