HTML5 in Drupal 7
I have been meaning to write this post for awhile. A few friends have asked me to explain how to do HTML5 in Drupal 7, via Twitter. Another concerned individual, having found a passing reference to it in a book review, even emailed:
"I urge you to start producing this post just as deeply as you explained basic 960 principles. How I enjoyed reading it. It was like a chat with a soulmate! It is a rare opportunity to read background on why things are the way they are. In my mental system, why is the key ingredient to know what are you doing."
So, without further ado, here it is…
- Intro
- Structure
- Aggregation
- Template.php - Section
- Template.php - Pruning
- Template.php - Minification
- Block, Field, Region, Views
- HTML, Page, Node
- Includes
- Conclusion
Intro
In addition to HTML5, I will cover how to remove some of the generated markup Drupal spits out by default, and how to override default system CSS. I will also provide a few tips on how to get some quick-win page speed boosts.
Before we dive in, I just wanted to mention that this won't be a comprehensive how-to on building a Drupal 7 theme. For the sake of brevity, I won't be covering the HTML5 Tools module, which endeavors to add features like new HTML5 form elements to Drupal. Instead, I will focus on trimming the XHTML glut that Drupal still instinctively wants to include, stemming from its earlier days.
Remember — back before the W3C declared XHTML 2.0 is dead, heralding HTML5 as the future — when we all used to put type="text/css"
on tags, as if we'd ever used a <style>
tag that wasn't CSS? Oh how silly we were! Yet we thought we were so cool, appeasing the validator. Then HTML5 came along, paving cowpaths, exposing the tedium of so many practices we'd once thought necessary.
Structure
When you first install Drupal, you will see a top-level directory named themes. Common sense would tell you that this is where you should store your own theme related files. In this case, common sense is wrong! In actuality, the directory you want to play in is sites. This is all about you and your particular site(s), and therein is another themes directory that will house your theme(s).
Note: Drupal can handle multiple domains via a single codebase, hence the "sites" directory instead of "site," but that's beyond the scope of this article. If you're interested, there's a Multisite Drupal group dedicated to the topic.
The first thing you should do is create a *.info
file with the name of your theme. Since my site is sonspring.com, and my theme is aptly named "SonSpring," my info file is sonspring.info
. Here are the contents (;
comments out a line)…
; BASIC SETTINGS
name = SonSpring
core = 7.x
; FEATURES (intentionally blank)
features[] =
; REGIONS
regions[front_journal] = front_journal
regions[content] = content
regions[sidebar_first] = sidebar_first
regions[sidebar_second] = sidebar_second
regions[search] = search
; CSS
stylesheets[all][] = assets/css/override/kill/ctools.css
stylesheets[all][] = assets/css/override/kill/field.css
stylesheets[all][] = assets/css/override/kill/node.css
stylesheets[all][] = assets/css/override/kill/system.messages.css
stylesheets[all][] = assets/css/override/kill/system.menus.css
stylesheets[all][] = assets/css/override/kill/user.css
stylesheets[all][] = assets/css/override/kill/views.css
stylesheets[all][] = assets/css/reset.css
stylesheets[all][] = assets/css/override/keep/system.base.css
stylesheets[all][] = assets/css/override/keep/system.theme.css
stylesheets[all][] = assets/css/override/keep/search.css
stylesheets[all][] = assets/css/960_12_col.css
stylesheets[all][] = assets/css/text.css
stylesheets[all][] = assets/css/formalize.css
stylesheets[all][] = assets/css/site.css
; JAVASCRIPT
scripts[] = assets/js/master.js
Aggregation
If you've looked at any Drupal themes, the preceding code is pretty straightforward stuff, nothing too out of the ordinary. One thing you might have noticed is the override directory. Inside it are two other directories, kill and keep.
I learned this trick from Morten Birch Heide-jørgensen during one of his talks on Drupal theming. He calls his folder foad and the technique FOAD, short for "F Off And Die" - a clever way to get rid of system CSS files you don't want.
If you name one of your own CSS files the same as one of the defaults, Drupal will include yours instead of providing its own. Thusly, everything in the kill directory is just an empty file, whereas everything in the keep directory is a CSS file that's been only slightly (if at all) modified from its original defaults.
Now, you might be wondering: Why bother having a keep directory at all, if I'm reusing the very same code as the default files it contains? Here's where it gets interesting. When you go to the Performance admin page, and turn on aggregation and compression of CSS files, Drupal will gather up all its default system stylesheets into a single file, and all your stylesheets into another.
That's silly. But with my keep directory, I force those default stylesheets - along with my theme stylesheets - to be aggregated and compressed into one file.
Template.php - Section
Call it an old habit from my beginner CMS days using Textpattern, but for theming purposes I like to know what "section" of the site I'm on. Basically, that just means I am curious what foobar is equal to, in the following scenarios…
https://example.com/foobar
https://example.com/foobar?page=1
https://example.com/foobar/node/search%20term
So, at the beginning of my template.php file, I have a simple function that runs through and grabs me the first path fragment in the URL, does a simple switch/case, and returns a string that becomes the <body>
ID.
<?php
function sonspring_section() {
$section_path = explode('/', request_uri());
$section_name = $section_path[1];
$section_q = strpos($section_name, '?');
if ($section_q) {
$section_name = substr($section_name, 0, $section_q);
}
switch ($section_name) {
case '':
return 'section_home';
break;
case 'journal':
return 'section_journal';
break;
case 'about':
return 'section_about';
break;
case 'work':
return 'section_work';
break;
case 'resources':
return 'section_resources';
break;
case 'contact':
return 'section_contact';
break;
case 'search':
return 'section_search';
break;
case 'user':
return 'section_user';
break;
case 'users':
return 'section_user';
break;
case 'filter':
return 'section_filter';
break;
case 'admin':
return 'section_admin';
break;
default:
return 'section_404';
}
}
Template.php - Pruning
In my template.php file, I have a function called sonspring_process_html_tag. Obviously, if your theme is named "MyTheme" then your function would be named mytheme_process_html_tag. It simply unsets the variables for the type
and media
attributes on style
, link
, and script
tags.
It also clears out CDATA
comments, that only ever existed to satisfy the XHTML validator, because hardly anyone ever served XHTML as application/xhtml+xml
. In the past, even though we all served XHTML as text/html
, we still jumped through hoops by adding CDATA
comments. Not so with HTML5.
// Purge needless XHTML stuff.
function sonspring_process_html_tag(&$vars) {
$el = &$vars['element'];
// Remove type="…" and CDATA prefix/suffix.
unset($el['#attributes']['type'], $el['#value_prefix'], $el['#value_suffix']);
// Remove media="all" but leave others unaffected.
if (isset($el['#attributes']['media']) && $el['#attributes']['media'] === 'all') {
unset($el['#attributes']['media']);
}
}
In reality, XHTML always worked fine without the needless code. HTML5 just makes it official. By way of comparison, here's the before and after of what Drupal would normally output by default, followed by the slimmed down result.
Before - XHTML
<link rel="stylesheet" href="…" type="text/css" media="all" />
<style type="text/css" media="all">
/* Code here. */
</style>
<script type="text/javascript">
<!--//--><![CDATA[//><!--
/* Code here. */
//--><!]]>
</script>
After - HTML5
<link rel="stylesheet" href="…" />
<style>
/* Code here. */
</style>
<script>
/* Code here. */
</script>
Template.php - Minification
Though this doesn't pertain to HTML5 directly, I decided that while I was mucking about in the template.php file, I might as well add some HTML minification. Basically, the following code just strips out unnecessary whitespace and line breaks, saving a bit of file size where possible. If a page contains a <pre>
or <textarea>
tag, it is left alone, as to not affect code blocks or text typed in a form.
// Minify HTML output.
function sonspring_process_html(&$vars) {
$before = array(
"/>\s\s+/",
"/\s\s+</",
"/>\t+</",
"/\s\s+(?=\w)/",
"/(?<=\w)\s\s+/"
);
$after = array('> ', ' <', '> <', ' ', ' ');
// Page top.
$page_top = $vars['page_top'];
$page_top = preg_replace($before, $after, $page_top);
$vars['page_top'] = $page_top;
// Page content.
if (!preg_match('/<pre|<textarea/', $vars['page'])) {
$page = $vars['page'];
$page = preg_replace($before, $after, $page);
$vars['page'] = $page;
}
// Page bottom.
$page_bottom = $vars['page_bottom'];
$page_bottom = preg_replace($before, $after, $page_bottom);
$vars['page_bottom'] = $page_bottom . drupal_get_js('footer');
}
Block, Field, Region, Views
While it is technically possible to make use of HTML5 and RDF together, I have opted to simply leave this aspect out of my templates, as you will see shortly. As such, I have simply disabled the RDF module (enabled by default).
For the most part, Drupal only renders what's in your templates. But there are a few areas where it tends to wrap things in <div>
tags that you might not want. Thankfully, Drupal's theme system makes it easy to remedy. The following is a list of files that I have created, which contain only the code necessary to generate your content, thereby overriding Drupal's default output.
I have purposefully omitted the closing ?>
tag, because it is considered a best practice (by both Drupal and Zend) to not close the <?php
tag. Instead, the parser realizes the PHP code is finished when it has reached the end of the file. This prevents the unintentional injection of whitespace into a page. (Needless to say, ?>
is still necessary when mixing PHP and HTML within the same file.)
Note: The templates that start with "views-" pertain to Views, a third party Drupal module that makes it easy to output lists of content based on various criteria.
block.tpl.php
<?php print $content;
Note: I have removed $item_attributes
from my field.tpl.php file. This is used by the RDF module, and can potentially be utilized by third party modules, to add things like onclick="…"
to various elements. I removed it because I am not using the RDF module, but your mileage may vary — read more.
field.tpl.php
<?php print render($items);
region.tpl.php
<?php print $content;
views-view.tpl.php
<?php
// Shorthand for if, then print.
// I write PHP like a JS hacker.
// Pager at the top, and bottom.
isset($admin_links) && print $admin_links;
$header && print $header;
$exposed && print $exposed;
$attachment_before && print $attachment_before;
$pager && print $pager;
$rows ? print $rows : $empty && print $empty;
$pager && print $pager;
$attachment_after && print $attachment_after;
$more && print $more;
$footer && print $footer;
$feed_icon && print $feed_icon;
views-view-unformatted.tpl.php
<?php
$title && print $title;
foreach ($rows as $id => $row) {
print $row;
}
views-view-list.tpl.php
<?php
$title && print $title;
print '<ul class="list">';
foreach ($rows as $id => $row) {
print '<li>' . $row . '</li>';
}
print '</ul>';
views-view-fields.tpl.php
<?php
foreach ($fields as $id => $field) {
if (isset($field->separator)) {
print $field->separator;
}
print $field->content;
}
HTML, Page, Node
Though the aforementioned files pertaining to block, field, region, and views are all technically templates, in that they end in *.tpl.php
, the crux of the templating system revolves around HTML, page, and node templates.
Below, I have listed out the majority of my template code. You may notice I did not include my page‐‐front.tpl.php
file. It is mostly hard coded because it does not change that often, with the only dynamic part being recent journal entries.
html.tpl.php
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge, chrome=1" />
<title><?php
if (sonspring_section() === 'section_home') {
print 'SonSpring by Nathan Smith';
}
else {
print $head_title;
}
?></title>
<!--[if lt IE 8]>
<script>
window.top.location = 'https://desktop.sonspring.com/ie.html';
</script>
<![endif]-->
<meta name="author" content="Nathan Smith">
<meta name="description" content="Personal and professional home of Christian web designer Nathan Smith." />
<link rel="alternate" type="application/rss+xml" title="SonSpring RSS" href="https://feeds.feedburner.com/sonspring" />
<link rel="shortcut icon" type="image/x-icon" href="/sites/all/themes/sonspring/assets/assets/images/favicon.ico" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Oswald" />
<?php print $styles; ?>
</head>
<body id="<?php print sonspring_section(); ?>">
<?php
print $page_top;
print $page;
print $scripts;
print $page_bottom;
?>
<script async="async" src="https://mint.sonspring.com/?js"></script>
</body>
</html>
page.tpl.php
<div id="wrapper">
<?php include 'assets/includes/header.inc'; ?>
<div id="main" class="container_12">
<div class="grid_10">
<h1><?php print $title; ?></h1>
<?php $tabs && print render($tabs); ?>
<div class="grid_7 alpha">
<?php print render($page['content']); ?>
</div>
<div class="grid_3 omega aside">
<?php $page['sidebar_first'] && print render($page['sidebar_first']); ?>
</div>
</div>
<div class="grid_2 aside">
<?php
include 'assets/includes/fusion.inc';
$page['sidebar_second'] && print render($page['sidebar_second']);
?>
</div>
</div>
</div>
<?php include 'assets/includes/footer.inc';
page‐‐search.tpl.php
<div id="wrapper">
<?php include 'assets/includes/header.inc'; ?>
<div id="main" class="container_12">
<div class="grid_10">
<h1><?php print $title; ?></h1>
<?php
$tabs && print render($tabs);
$page['content'] && print render($page['content']);
?>
</div>
<div class="grid_2 aside">
<?php
include 'assets/includes/fusion.inc';
$page['sidebar_second'] && print render($page['sidebar_second']);
?>
</div>
</div>
</div>
<?php include 'assets/includes/footer.inc';
Note: In node.tpl.php, I removed $attributes
, $title_attributes
, and $content_attributes
, used for RDF. For a pristine node.tpl.php — source.
node.tpl.php
<?php
!empty($content['upload']) && hide($content['upload']);
!empty($content['taxonomy_vocabulary_1']) && hide($content['taxonomy_vocabulary_1']);
!empty($content['links']) && hide($content['links']);
?>
<div class="node clearfix">
<?php if (!$page) { ?>
<?php print render($title_prefix); ?>
<h2>
<a href="<?php print $node_url; ?>"><?php print $title; ?></a>
</h2>
<?php print render($title_suffix); ?>
<?php } ?>
<?php if ($submitted) { ?>
<?php if ($page) { ?>
<div class="g_plus"><div class="g-plusone" data-size="small" data-count="false"></div></div>
<?php } ?>
<div class="meta mute">
<span class="submitted">
<?php print format_date($node->created); ?>
</span>
—
Topic:
<?php print render($content['taxonomy_vocabulary_1']); ?>
</div>
<?php } ?>
<?php
print render($content);
print render($content['links']);
?>
</div>
Includes
For the sake of completeness, I figured I ought to finish by listing out the contents of my *.inc
files, contained within an includes directory under assets. While these files could have just as easily been left in the higher level *.tpl.php
files, to ease in maintainability and in an effort to adhere to DRY principles, I separated them out into their own files. By having the file extension *.inc
, Drupal knows to disallow direct browsing, so they can only be used via inclusion in a PHP page.
header.inc
<div id="header">
<div class="container_12">
<div class="grid_3" id="ss_logo">
<?php
if (!$is_front) {
print '<a href="/">';
}
print '<span>SonSpring</span>';
if (!$is_front) {
print '</a>';
}
?>
</div>
<div class="grid_7">
<ul id="nav">
<li id="nav_journal">
<a href="/journal">Journal</a>
</li>
<li id="nav_about">
<a href="/about">About</a>
</li>
<li id="nav_work">
<a href="/work">Work</a>
</li>
<li id="nav_resources">
<a href="/resources">Resources</a>
</li>
<li id="nav_contact">
<a href="/contact">Contact</a>
</li>
</ul>
</div>
<div class="grid_2">
<?php
if ($page['search'] && sonspring_section() !== 'section_search') {
print render($page['search']);
}
?>
</div>
</div>
</div>
fusion.inc
<a href="https://fusionads.net/" id="fusion_link">Powered by Fusion Ads</a>
<div id="fusion_ad"><span class="clear"> </span></div>
footer.inc
<div id="footer">
<div class="container_12">
<div class="grid_4">
© <?php print date('Y'); ?> <a href="https://profiles.google.com/sonspring/about">Nathan Smith</a>. All rights reserved.
</div>
<div class="grid_4 align_center">
Hosted by <a href="https://dreamhost.com/r.cgi?106853">DreamHost</a>
</div>
<div class="grid_4 align_right">
<a href="https://feeds.feedburner.com/sonspring">Subscribe</a> via RSS. Follow me on <a href="https://twitter.com/nathansmith">Twitter</a>.
</div>
</div>
</div>
Conclusion
While all that might seem like a lot of code at first glance, keep in mind that's pretty much the entirety of my site's theme (aside from CSS and light JavaScript). Once you get the hang of how it works, the Drupal theming system really not that difficult to understand. Plus, you're learning real PHP, not pseudo code.
I wouldn't say Drupal is perfect, but it offers extensibility via simple overrides. By far, it is the most intuitive approach I've found in my meandering quest for the ideal CMS. Hopefully this walkthrough has helped dispel the myth that Drupal's learning curve is "steep," or at least made the process seem less daunting.