Back

Tweaking the Twig Tweak Module: A Simple Drupal Site Recipe

front page twig template

Many years ago I worked for a printing company. While I was there, we implemented a workflow that allowed us to produce business cards within 15 minutes of the client submitting their request. Through that experience I learned the competitive advantage of providing a simple service in a short timeframe.

At Speed & Function, we typically work on complex projects where completing the design, development, and QA phases can take months. However, a little known fact is that we can also tackle quick and simple tasks.

There is a common assumption that Drupal is only for large multi-user projects with complex business logic and high page counts. It is also believed that Drupal has significant restrictions on the design and templates that can be used within Drupal’s themes. Both of these assumptions are false.

In this post, I’ll demonstrate a quick and easy way to launch a simple, custom designed site on Drupal 8.
Note: the approach that I’m going to use isn’t canonical and some veteran Drupal theme developers may criticize me for this “quick and dirty” approach, but with it we can prop up a Drupal site in just a few hours! You’ll just need to find some free HTML templates (or buy more a advanced solution from a stock site) and follow the instructions below.

Basic Drupal Theming Background Information

Honestly, Drupal’s theme structure and template suggestions for the nesting aren’t my favorite. I understand the main idea of it, and that all these wrappers make sense and serve for some out-of-the-box functionality (such as contextual links, etc.), but sometimes setting up a Drupal theme from scratch for a simple project simple markup can take a lot of time. To start, we have to overwrite a long set of templates:

  • html.html.twig
  • page.html.twig
  • region.html.twig
  • node.html.twig

This list can be long as some templates can have cases, for example, for different node templates for different content types.

Let’s start with theme building. Only one file is required to set Drupal 8 theme [theme_name].info.yml. It can look like:

name: Theme Name
type: theme
description: Theme description.
core: 8.x
base theme: [Base Theme Name or false]
[libraries:
…]

If we set a Base Theme, the templates are inherited form that theme, if we set ‘false’ then the templates go from /core/modules/system/templates.

Let’s create a page that displays a body field with some minimal markup. FYI — if we don’t set a region in our theme info file, we’ll have a default set of regions:

  • Left sidebar
  • Right sidebar
  • Content
  • Header
  • Primary menu
  • Secondary menu
  • Footer
  • Highlighted
  • Help
  • Breadcrumb

I have created a ‘/templates’ folder in my theme folder and dropped in html.html.twig. This template looks like:

<!DOCTYPE html>
<html{{ html_attributes }}>
  <head>
    <head-placeholder token="{{ placeholder_token }}">
    <title>{{ head_title|safe_join(' | ') }}</title>
    <css-placeholder token="{{ placeholder_token }}">
    <js-placeholder token="{{ placeholder_token }}">
  </head>
  <body{{ attributes }}>
    {#
      Keyboard navigation/accessibility link to main content section in
      page.html.twig.
    #}
    <a href="#main-content" class="visually-hidden focusable">
      {{ 'Skip to main content'|t }}
    </a>
    {{ page_top }}
    {{ page }}
    {{ page_bottom }}
    <js-bottom-placeholder token="{{ placeholder_token }}">
  </body>
</html>

{{ page }} is the next template placeholder in the Drupal template chain. It looks like:

<div class="layout-container">

  <header role="banner">
    {{ page.header }}
  </header>

  {{ page.primary_menu }}
  {{ page.secondary_menu }}

  {{ page.breadcrumb }}

  {{ page.highlighted }}

  {{ page.help }}

  <main role="main">
    <a id="main-content" tabindex="-1"></a>{# link is in html.html.twig #}

    <div class="layout-content">
      {{ page.content }}
    </div>{# /.layout-content #}

    {% if page.sidebar_first %}
      <aside class="layout-sidebar-first" role="complementary">
        {{ page.sidebar_first }}
      </aside>
    {% endif %}

    {% if page.sidebar_second %}
      <aside class="layout-sidebar-second" role="complementary">
        {{ page.sidebar_second }}
      </aside>
    {% endif %}

  </main>

  {% if page.footer %}
    <footer role="contentinfo">
      {{ page.footer }}
    </footer>
  {% endif %}

</div>{# /.layout-container #}

One key snippet that we should take from page.html.twig is the line

<a id="main-content" tabindex="-1"></a>{# link is in html.html.twig #}

This is the anchor that allows screen readers to access the main content and skip the navigation and header. The link to that anchor is on the html.html.twig.

Twig Tweak

Allow me to introduce a beautiful module that allows us to avoid a number of nested templates and get quicker results. It’s the Twig Tweak module. Rather than detail a long intro, I’ll share a quick example and hope you catch the trick.

Let’s rebuild html.html.twig to output the node title and body just inside that template with the power of Twig Tweak. We are going to replace {{ page }} with {{ drupal_field(‘body’, ‘node’) }} (see Cheat sheet (8.x-2.x).

Let’s also add site name, page title and main content anchor mentioned above in the template:

<!DOCTYPE html>
<html{{ html_attributes }}>
  <head>
    <head-placeholder token="{{ placeholder_token }}">
    <title>{{ head_title|safe_join(' | ') }}</title>
    <css-placeholder token="{{ placeholder_token }}">
    <js-placeholder token="{{ placeholder_token }}">
  </head>
  <body{{ attributes }}>
    {#
      Keyboard navigation/accessibility link to main content section in
      page.html.twig.
    #}
    <a href="#main-content" class="visually-hidden focusable">
      {{ 'Skip to main content'|t }}
    </a>
    {{ page_top }}
    <h1>{{ drupal_config('system.site', 'name') }}</h1>
    <a id="main-content" tabindex="-1"></a>{# link is in html.html.twig #}
    <h2>{{ drupal_title() }}</h2>
    {{ drupal_field('body', 'node') }}
    {{ page_bottom }}
    <js-bottom-placeholder token="{{ placeholder_token }}">
  </body>
</html>

Here’s an example of what it should look like:

Page view in anonymous case

Note that there’s also the admin toolbar menu if we logged as admin. Using this method eliminates the View/Edit tabs, main menu, contextual links and many other out-of-the-box features. Don’t worry, we can still access them using this approach (see Cheatsheet, but I’m choosing not to for the sake of simplicity. If you can’t find something that you need in the Cheatsheet, you can get it in the hook template_preprocess_html.

Turning an HTML responsive template into a Drupal site

Let’s move on to the task at hand. Here is a simple responsive template.

We can use this as a starting point and just simplify it. Now, it’s a simple one-page site about Pizza Cafe:

Let’s skip the footer for now and set a plan for where we’ll store the information you can see on that page:

You can see that we need to set additional WYSIWYG field for the pizza list on the right side, and we need one more image field that allows multiple values for the pizza gallery under the footer.

Let’s create a new content type for the front page with two additional fields:

Next, we have to create a node with content and set it as a front page in site the settings. Let’s put static HTML directly into theme folder and use the CSS and JS files in our theme.

We’ll need to add styles:

<link rel="stylesheet" href="css/bootstrap.min.css">
<link rel="stylesheet" href="css/jquery.fancybox.css">
<link rel="stylesheet" href="css/main.css">
<link rel="stylesheet" href="css/responsive.css">
<link rel="stylesheet" href="css/animate.min.css">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css">

And scripts:

<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script> 
<script src="js/bootstrap.min.js"></script> 
<script src="js/jquery.fancybox.pack.js"></script> 
<script src="js/modernizr.js"></script> 
<script src="js/main.js"></script>

We can use jquery from the core, and add everything else to one library. Our .libraries.yml file will be:

global-styling:
  version: VERSION
  js:
    js/bootstrap.min.js: {}
    js/jquery.fancybox.pack.js: {}
    js/modernizr.js: {}
    js/main.js: {}
  css:
    theme:
      css/bootstrap.min.css: {}
      css/jquery.fancybox.css: {}
      css/main.css: {}
      css/responsive.css: {}
      css/animate.min.css: {}
      //maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css: { type: external, minified: true }
  dependencies:
    - drupal/jquery

And we have to add in .info.yml:

libraries:
 - abzats_pizza/global-styling

One more thing: we need to wrap main.js:

(function ($) {
   // main.js content here
}(jQuery));

For more information see jquery’s official documentation

Next, copy static markup inside page.html.twig. Let’s start from header section and stop before footer section, and replace the sections we need to edit with tokens from Cheat sheet (8.x-2.x). Now page.html.twig looks like:

<!-- Header Section -->
<section class="banner" role="banner"> 
  <!-- Navigation Section -->
  <header id="header">
    <div class="header-content clearfix"> <a class="logo" href="#">{{ drupal_config('system.site', 'name') }}</a>
      <nav class="navigation" role="navigation">
        {{ drupal_menu('front') }}
      </nav>
      <a href="#" class="nav-toggle">Menu<span></span></a> </div>
  </header>
  <!-- Navigation Section --> 
</section>
<!-- Header Section --> 
<a id="main-content" tabindex="-1"></a>{# link is in html.html.twig #}
<!-- Intro Section -->
<section id="introduction" class="section introduction">
  <div class="container">
    <div class="row">
      <div class="col-md-4 col-sm-6">
        <div class="intro-content">
          <h1>{{ drupal_title() }}</h1>
        </div>
      </div>
      <div class="col-md-5 col-sm-6">
        <div class="intro-content">
          {{ drupal_field('body', 'node') }}
        </div>
      </div>
      <div class="col-md-3 col-sm-6">
        <div class="intro-content">
          {{ drupal_field('field_right_side', 'node') }}
        </div>
      </div>
    </div>
  </div>
</section>
<!-- Intro Section --> 

<!-- work section -->
<section id="works" class="works section no-padding">
  <div class="container-fluid">
    <div class="row no-gutter">
      {{ drupal_field('field_gallery', 'node') }}
    </div>
  </div>
</section>
<!-- work section --> 

Now everything except the menu and gallery looks good. Moving forward, we can enable debugging. To do this, we need to customize menu–front.html.twig and field–field-gallery.html.twig. Copy menu.html.twig from /core/modules/system/templates to our template folder and rename it to menu–front.html.twig. Skipping the multilevel menu feature for the sake of simplicity, our menu–front.html.twig is:

<ul class="primary-nav">
{% for item in items %}
  <li>
    {{ link(item.title, item.url) }}
  </li>
{% endfor %}
</ul>

Don’t forget to set a custom front menu in admin:

The links should be #introduction, #works, #contact. There are anchors specified in the markup.

We’ll do the same for field–field-gallery.html.twig; copy field.html.twig from /core/modules/system/templates to our template folder and rename it to field–field-gallery.html.twig, then fix the markup. Here we’ll have to set a link to the image in display settings:

It also looks like we need to customize image-formatter.html.twig, which we can take from /core/modules/image/templates. Thus field–field-gallery.html.twig will look like:

{% for item in items %}
  <div class="col-lg-2 col-md-4 col-sm-4 work">{{ item.content }}</div>
{% endfor %}

And image-formatter.html.twig:

{% if url %}
  <a href="{{ url }}" class="work-box">
    {{ image }}
    <div class="overlay">
      <div class="overlay-caption">
        <p><i class="fa fa-search-plus fa-2x"></i></p>
      </div>
    </div>
    <!-- overlay --> 
  </a>
{% else %}
  {{ image }}
{% endif %}

Now, let’s go back to the footer. We can the use blocks for that stuff, but in this case, I think it’s overkill. It’s better to use theme settings. Let’s add file .theme to our theme and drop in the code:

<?php

function abzats_pizza_form_system_theme_settings_alter(&$form, \Drupal\Core\Form\FormStateInterface &$form_state, $form_id = NULL) {
  // Work-around for a core bug affecting admin themes. See issue #943212.
  if (isset($form_id)) {
    return;
  }

  $form['abzats_pizza'] = array(
    '#type'          => 'fieldset',
    '#title'         => t('Footer Information'),
  );

  $form['abzats_pizza']['abzats_pizza_contact'] = array(
    '#type'          => 'textarea',
    '#title'         => t('Contact'),
    '#default_value' => theme_get_setting('abzats_pizza_contact'),
    '#description'   => t('Left footer info block: Contact block.'),
  );

  $form['abzats_pizza']['abzats_pizza_copyright'] = array(
    '#type'          => 'textarea',
    '#title'         => t('Contact'),
    '#default_value' => theme_get_setting('abzats_pizza_copyright'),
    '#description'   => t('Right footer info block: Copyright block.'),
  );

}

function abzats_pizza_preprocess_page(&$variables) {
  $variables['abzats_pizza_contact'] = theme_get_setting('abzats_pizza_contact');
  $variables['abzats_pizza_copyright'] = theme_get_setting('abzats_pizza_copyright');
}

We need the second function to pass our custom settings in the template.

The settings page will look like:

Let’s add to page.twig.html the footer section:

<!-- footer section -->
<footer id="contact" class="footer">
  <div class="container">
    <div class="col-md-4">
      {{ abzats_pizza_contact }}
    </div>
    <div class="col-md-3">
    </div>
    <div class="col-md-5">
      {{ abzats_pizza_copyright }}
    </div>
  </div>
</footer>
<!-- footer section -->

The result is:

This looks bad. Why? We need to prevent filtering HTML in twig template, which we can do that with |raw instruction:

<!-- footer section -->
<footer id="contact" class="footer">
  <div class="container">
    <div class="col-md-4">
      {{ abzats_pizza_contact|raw }}
    </div>
    <div class="col-md-3">
    </div>
    <div class="col-md-5">
      {{ abzats_pizza_copyright|raw }}
    </div>
  </div>
</footer>
<!-- footer section --> 

Don’t forget that we can use additional abilities using the Twig Tweak module. For this example, we can delete the preprocess function because we can access any configuration setting in the template:

<!-- footer section -->
<footer id="contact" class="footer">
  <div class="container">
    <div class="col-md-4">
      {{ drupal_config('abzats_pizza.settings', 'abzats_pizza_contact')|raw }}
    </div>
    <div class="col-md-3">
    </div>
    <div class="col-md-5">
      {{ drupal_config('abzats_pizza.settings', 'abzats_pizza_copyright')|raw }}
    </div>
  </div>
</footer>
<!-- footer section --> 

Finally, let’s add system message just after the header section:

…
</section>
<!-- Header Section --> 
{{ drupal_messages() }}
<a id="main-content" tabindex="-1"></a>{# link is in html.html.twig #}
<!-- Intro Section -->
…

Here it makes sense to set a base theme and borrow styles from basic Drupal elements via the base theme. Let’s replace line
‘base theme: false’
with
‘base theme: classy’
in our .info.yml file. After that system messages will have better styling.

Lastly, I propose renaming page.html.twig to page–front.html.twig. This way we can use that template solely for the front page. For any additional pages we can use the simpler template page.html.twig, which omits the menu, sidebar text, and gallery:

<!-- Header Section -->
<section class="banner" role="banner"> 
  <!-- Navigation Section -->
  <header id="header">
    <div class="header-content clearfix"> <a class="logo" href="#">{{ drupal_config('system.site', 'name') }}</a>
  </header>
  <!-- Navigation Section --> 
</section>
<!-- Header Section --> 
{{ drupal_messages() }}
<a id="main-content" tabindex="-1"></a>{# link is in html.html.twig #}
<!-- Intro Section -->
<section class="section introduction">
  <div class="container">
    <div class="row">
      <div class="col-md-4 col-sm-6">
        <div class="intro-content">
          <h1>{{ drupal_title() }}</h1>
        </div>
      </div>
      <div class="col-md-5 col-sm-6">
        <div class="intro-content">
          {{ drupal_field('body', 'node') }}
        </div>
      </div>
      <div class="col-md-3 col-sm-6">
        <div class="intro-content">
        </div>
      </div>
    </div>
  </div>
</section>
<!-- Intro Section --> 

<!-- footer section -->
<footer id="contact" class="footer">
  <div class="container">
    <div class="col-md-4">
      {{ drupal_config('abzats_pizza.settings', 'abzats_pizza_contact')|raw }}
    </div>
    <div class="col-md-3">
    </div>
    <div class="col-md-5">
      {{ drupal_config('abzats_pizza.settings', 'abzats_pizza_copyright')|raw }}
    </div>
  </div>
</footer>
<!-- footer section -->

Here’s the privacy policy page as an example:

Privacy Policy page

And that’s all it takes! You can download the Pizza Cafe theme on GitHub, which includes a demo module of Abzats Pizza. Just turn on the module and you can use this example in your Drupal 8 installation.

Final Thoughts

That was a lot to take in but once you have a grasp of the concept, you’ll find Drupal to be an even more dynamic tool in your web development tool kit. Before you go, consider one last practical example of how to use the approach you’ve learned today.

Imagine you’re tasked with a large project expected expected to take several month to complete. Let’s assume that it’s Drupal 8 site from scratch and you already have some statics and/or the design. With this simplified approach, you can spend about 1 hour and build a simplified version of the site with basic placeholder information that uses the client’s design and is already on Drupal 8. This site can go into production as a stand in as you work on the final deliverable.

The use cases here are endless. How would you use Twig sites in your development process? Share your examples with us on Twitter @SpeedFunction.

Read more