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 iswrong! 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)…
01.
; BASIC SETTINGS
02.
name = SonSpring
03.
core = 7.x
04.
05.
; FEATURES (intentionally blank)
06.
features[] =
07.
08.
; REGIONS
09.
regions[front_journal] = front_journal
10.
regions[content] = content
11.
regions[sidebar_first] = sidebar_first
12.
regions[sidebar_second] = sidebar_second
13.
regions[search] = search
14.
15.
; CSS
16.
stylesheets[all][] = assets/css/override/kill/ctools.css
17.
stylesheets[all][] = assets/css/override/kill/field.css
18.
stylesheets[all][] = assets/css/override/kill/node.css
19.
stylesheets[all][] = assets/css/override/kill/system.messages.css
20.
stylesheets[all][] = assets/css/override/kill/system.menus.css
21.
stylesheets[all][] = assets/css/override/kill/user.css
22.
stylesheets[all][] = assets/css/override/kill/views.css
23.
stylesheets[all][] = assets/css/reset.css
24.
stylesheets[all][] = assets/css/override/keep/system.base.css
25.
stylesheets[all][] = assets/css/override/keep/system.theme.css
26.
stylesheets[all][] = assets/css/override/keep/search.css
27.
stylesheets[all][] = assets/css/960_12_col.css
28.
stylesheets[all][] = assets/css/text.css
29.
stylesheets[all][] = assets/css/formalize.css
30.
stylesheets[all][] = assets/css/site.css
31.
32.
; JAVASCRIPT
33.
scripts[] = assets/js/master.min.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 thekeep 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 Performanceadmin 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…
http://example.com/foobar
http://example.com/foobar?page=1
http://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.
01.
<?php
02.
03.
function
sonspring_section() {
04.
$section_path
=
explode
(
'/'
, request_uri());
05.
$section_name
=
$section_path
[1];
06.
$section_q
=
strpos
(
$section_name
,
'?'
);
07.
08.
if
(
$section_q
) {
09.
$section_name
=
substr
(
$section_name
, 0,
$section_q
);
10.
}
11.
12.
switch
(
$section_name
) {
13.
case
''
:
14.
return
'section_home'
;
15.
break
;
16.
case
'journal'
:
17.
return
'section_journal'
;
18.
break
;
19.
case
'about'
:
20.
return
'section_about'
;
21.
break
;
22.
case
'work'
:
23.
return
'section_work'
;
24.
break
;
25.
case
'resources'
:
26.
return
'section_resources'
;
27.
break
;
28.
case
'contact'
:
29.
return
'section_contact'
;
30.
break
;
31.
case
'search'
:
32.
return
'section_search'
;
33.
break
;
34.
case
'user'
:
35.
return
'section_user'
;
36.
break
;
37.
case
'users'
:
38.
return
'section_user'
;
39.
break
;
40.
case
'filter'
:
41.
return
'section_filter'
;
42.
break
;
43.
case
'admin'
:
44.
return
'section_admin'
;
45.
break
;
46.
default
:
47.
return
'section_404'
;
48.
}
49.
}
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.
01.
// Purge needless XHTML stuff.
02.
function
sonspring_process_html_tag(&
$vars
) {
03.
$el
= &
$vars
[
'element'
];
04.
05.
// Remove type="..." and CDATA prefix/suffix.
06.
unset(
$el
[
'#attributes'
][
'type'
],
$el
[
'#value_prefix'
],
$el
[
'#value_suffix'
]);
07.
08.
// Remove media="all" but leave others unaffected.
09.
if
(isset(
$el
[
'#attributes'
][
'media'
]) &&
$el
[
'#attributes'
][
'media'
] ===
'all'
) {
10.
unset(
$el
[
'#attributes'
][
'media'
]);
11.
}
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
01.
<
link
rel
=
"stylesheet"
href
=
"..."
type
=
"text/css"
media
=
"all"
/>
02.
03.
<
style
type
=
"text/css"
media
=
"all"
>
04.
/* Code here. */
05.
</
style
>
06.
07.
<
script
type
=
"text/javascript"
>
08.
<!--//-->
<![CDATA[//><!--
09.
/* Code here. */
10.
//--><!]]>
11.
</
script
>
After – HTML5
01.
<
link
rel
=
"stylesheet"
href
=
"..."
/>
02.
03.
<
style
>
04.
/* Code here. */
05.
</
style
>
06.
07.
<
script
>
08.
/* Code here. */
09.
</
script
>
Template.php – Minification
Though this doesn’t pertain to HTML5 directly, I decided that while I was mucking about in thetemplate.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.
01.
// Minify HTML output.
02.
function
sonspring_process_html(&
$vars
) {
03.
$before
=
array
(
04.
"/>\s\s+/"
,
05.
"/\s\s+</"
,
06.
"/>\t+</"
,
07.
"/\s\s+(?=\w)/"
,
08.
"/(?<=\w)\s\s+/"
09.
);
10.
11.
$after
=
array
(
'> '
,
' <'
,
'> <'
,
' '
,
' '
);
12.
13.
// Page top.
14.
$page_top
=
$vars
[
'page_top'
];
15.
$page_top
= preg_replace(
$before
,
$after
,
$page_top
);
16.
$vars
[
'page_top'
] =
$page_top
;
17.
18.
// Page content.
19.
if
(!preg_match(
'/<pre|<textarea/'
,
$vars
[
'page'
])) {
20.
$page
=
$vars
[
'page'
];
21.
$page
= preg_replace(
$before
,
$after
,
$page
);
22.
$vars
[
'page'
] =
$page
;
23.
}
24.
25.
// Page bottom.
26.
$page_bottom
=
$vars
[
'page_bottom'
];
27.
$page_bottom
= preg_replace(
$before
,
$after
,
$page_bottom
);
28.
$vars
[
'page_bottom'
] =
$page_bottom
. drupal_get_js(
'footer'
);
29.
}
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 Drupaland 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
1.
<?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
01.
<?php
02.
03.
// Shorthand for if, then print.
04.
// I write PHP like a JS hacker.
05.
// Pager at the top, and bottom.
06.
07.
isset(
$admin_links
) && print
$admin_links
;
08.
$header
&& print
$header
;
09.
$exposed
&& print
$exposed
;
10.
$attachment_before
&& print
$attachment_before
;
11.
$pager
&& print
$pager
;
12.
$rows
? print
$rows
:
$empty
&& print
$empty
;
13.
$pager
&& print
$pager
;
14.
$attachment_after
&& print
$attachment_after
;
15.
$more
&& print
$more
;
16.
$footer
&& print
$footer
;
17.
$feed_icon
&& print
$feed_icon
;
views-view-unformatted.tpl.php
1.
<?php
2.
3.
$title
&& print
$title
;
4.
5.
foreach
(
$rows
as
$id
=>
$row
) {
6.
print
$row
;
7.
}
views-view-list.tpl.php
01.
<?php
02.
03.
$title
&& print
$title
;
04.
05.
print
'<ul class="list">'
;
06.
07.
foreach
(
$rows
as
$id
=>
$row
) {
08.
print
'<li>'
.
$row
.
'</li>'
;
09.
}
10.
11.
print
'</ul>'
;
views-view-fields.tpl.php
01.
<?php
02.
03.
foreach
(
$fields
as
$id
=>
$field
) {
04.
if
(isset(
$field
->separator)) {
05.
print
$field
->separator;
06.
}
07.
08.
print
$field
->content;
09.
}
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
01.
<!DOCTYPE html>
02.
<
html
lang
=
"en"
>
03.
<
head
>
04.
<
meta
charset
=
"utf-8"
/>
05.
<
meta
http-equiv
=
"x-ua-compatible"
content
=
"ie=edge, chrome=1"
/>
06.
<
title
><?
php
07.
if (sonspring_section() === 'section_home') {
08.
print 'SonSpring by Nathan Smith';
09.
}
10.
else {
11.
print $head_title;
12.
}
13.
?></
title
>
14.
<!--[if lt IE 8]>
15.
<
script
>
16.
window.top.location = 'http://desktop.sonspring.com/ie.html';
17.
</
script
>
18.
<![endif]-->
19.
<
meta
name
=
"author"
content
=
"Nathan Smith"
>
20.
<
meta
name
=
"description"
content
=
"Personal and professional home of Christian web designer Nathan Smith."
/>
21.
<
link
rel
=
"alternate"
type
=
"application/rss+xml"
title
=
"SonSpring RSS"
href
=
"http://feeds.feedburner.com/sonspring"
/>
22.
<
link
rel
=
"shortcut icon"
type
=
"image/x-icon"
href
=
"/sites/all/themes/sonspring/assets/images/favicon.ico"
/>
23.
<
link
rel
=
"stylesheet"
href
=
"http://fonts.googleapis.com/css?family=Oswald"
/>
24.
<?
php
print $styles; ?>
25.
</
head
>
26.
<
body
id="<?php print sonspring_section(); ?>">
27.
<?
php
28.
print $page_top;
29.
print $page;
30.
print $scripts;
31.
print $page_bottom;
32.
?>
33.
<
script
async
=
"async"
src
=
"http://mint.sonspring.com/?js"
></
script
>
34.
</
body
>
35.
</
html
>
page.tpl.php
01.
<
div
id
=
"wrapper"
>
02.
<?
php
include 'assets/includes/header.inc'; ?>
03.
<
div
id
=
"main"
class
=
"container_12"
>
04.
<
div
class
=
"grid_10"
>
05.
<
h1
><?
php
print $title; ?></
h1
>
06.
<?
php
$tabs && print render($tabs); ?>
07.
<
div
class
=
"grid_7 alpha"
>
08.
<?
php
print render($page['content']); ?>
09.
</
div
>
10.
<
div
class
=
"grid_3 omega aside"
>
11.
<?
php
$page['sidebar_first'] && print render($page['sidebar_first']); ?>
12.
</
div
>
13.
</
div
>
14.
<
div
class
=
"grid_2 aside"
>
15.
<?
php
16.
include 'assets/includes/fusion.inc';
17.
$page['sidebar_second'] && print render($page['sidebar_second']);
18.
?>
19.
</
div
>
20.
</
div
>
21.
</
div
>
22.
<?php include 'assets/includes/footer.inc';
page‐‐search.tpl.php
01.
<div id=
"wrapper"
>
02.
<?php
include
'assets/includes/header.inc'
; ?>
03.
<div id=
"main"
class
=
"container_12"
>
04.
<div
class
=
"grid_10"
>
05.
<h1><?php print
$title
; ?></h1>
06.
<?php
07.
$tabs
&& print render(
$tabs
);
08.
$page
[
'content'
] && print render(
$page
[
'content'
]);
09.
?>
10.
</div>
11.
<div
class
=
"grid_2 aside"
>
12.
<?php
13.
include
'assets/includes/fusion.inc'
;
14.
$page
[
'sidebar_second'
] && print render(
$page
[
'sidebar_second'
]);
15.
?>
16.
</div>
17.
</div>
18.
</div>
19.
<?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
01.
<?php
02.
!
empty
empty
(
$content
[
'upload'
]) && hide(
$content
[
'upload'
]);
03.
!
empty
empty
(
$content
[
'taxonomy_vocabulary_1'
]) && hide(
$content
[
'taxonomy_vocabulary_1'
]);
04.
!
empty
empty
(
$content
[
'links'
]) && hide(
$content
[
'links'
]);
05.
?>
06.
<div
class
=
"node clearfix"
>
07.
<?php
if
(!
$page
) { ?>
08.
<?php print render(
$title_prefix
); ?>
09.
<h2>
10.
<a href=
"<?php print $node_url; ?>"
><?php print
$title
; ?></a>
11.
</h2>
12.
<?php print render(
$title_suffix
); ?>
13.
<?php } ?>
14.
<?php
if
(
$submitted
) { ?>
15.
<?php
if
(
$page
) { ?>
16.
<div
class
=
"g_plus"
><div
class
=
"g-plusone"
data-size=
"small"
data-
count
=
"false"
></div></div>
17.
<?php } ?>
18.
<div
class
=
"meta mute"
>
19.
<span
class
=
"submitted"
>
20.
<?php print format_date(
$node
->created); ?>
21.
</span>
22.
—
23.
Topic:
24.
<?php print render(
$content
[
'taxonomy_vocabulary_1'
]); ?>
25.
</div>
26.
<?php } ?>
27.
<?php
28.
print render(
$content
);
29.
print render(
$content
[
'links'
]);
30.
?>
31.
</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
01.
<
div
id
=
"header"
>
02.
<
div
class
=
"container_12"
>
03.
<
div
class
=
"grid_3"
id
=
"ss_logo"
>
04.
<?
php
05.
if (!$is_front) {
06.
print '<a
href
=
"/"
>';
07.
}
08.
09.
print '<
span
>SonSpring</
span
>';
10.
11.
if (!$is_front) {
12.
print '</
a
>';
13.
}
14.
?>
15.
</
div
>
16.
<
div
class
=
"grid_7"
>
17.
<
ul
id
=
"nav"
>
18.
<
li
id
=
"nav_journal"
>
19.
<
a
href
=
"/journal"
>Journal</
a
>
20.
</
li
>
21.
<
li
id
=
"nav_about"
>
22.
<
a
href
=
"/about"
>About</
a
>
23.
</
li
>
24.
<
li
id
=
"nav_work"
>
25.
<
a
href
=
"/work"
>Work</
a
>
26.
</
li
>
27.
<
li
id
=
"nav_resources"
>
28.
<
a
href
=
"/resources"
>Resources</
a
>
29.
</
li
>
30.
<
li
id
=
"nav_contact"
>
31.
<
a
href
=
"/contact"
>Contact</
a
>
32.
</
li
>
33.
</
ul
>
34.
</
div
>
35.
<
div
class
=
"grid_2"
>
36.
<?
php
37.
if ($page['search'] && sonspring_section() !== 'section_search') {
38.
print render($page['search']);
39.
}
40.
?>
41.
</
div
>
42.
</
div
>
43.
</
div
>
fusion.inc
<a href="http://fusionads.net/" id="fusion_link">Powered by Fusion Ads</a>
<div id="fusion_ad"><span class="clear"> </span></div>
footer.inc
01.
<
div
id
=
"footer"
>
02.
<
div
class
=
"container_12"
>
03.
<
div
class
=
"grid_4"
>
04.
© <?
php
print date('Y'); ?> <
a
href
=
"https://profiles.google.com/sonspring/about"
>Nathan Smith</
a
>. All rights reserved.
05.
</
div
>
06.
<
div
class
=
"grid_4"
>
07.
<
a
href
=
"http://www.firehost.com/?ref=spon_nsmith_sonspring"
title
=
"Secure Hosting"
id
=
"hosted_by_firehost"
>Hosted by FireHost</
a
>
08.
</
div
>
09.
<
div
class
=
"grid_4 align_right"
>
10.
<
a
href
=
"http://feeds.feedburner.com/sonspring"
>Subscribe</
a
> via RSS. Follow me on <
a
href
=
"http://twitter.com/nathansmith"
>Twitter</
a
>.
11.
</
div
>
12.
</
div
>
13.
</
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.
No comments:
Post a Comment