Building a Static Site with Template Toolkit

Template Toolkit is great for dynamic sites, but it can also make the task of keeping a static site up-to-date. Perl Guru Randal Schwartz sings TT's virtues while building a site for budding karaoke stars.

As described in previous articles, I’m a big fan of the Template Toolkit (or TT for short). I frequently use TT to generate dynamic web pages based on input parameters, but TT can help static web sites as well. Let’s take a look at a typical small, static website and how TT can help things.

Now Appearing: Randal

I’m a karaoke singer. I admit it. Karaoke helped get me through my initial stage fright when I started teaching: moving from behind the keyboard to the front of a room wasn’t what I had in mind when I studied software design in my youth. Karaoke also got me through the initial potential homesickness when I started travelling to teach: no matter what city I ended up in, I could find at least one night at a Karaoke Bar during the week, and things would seem “familiar.” And yes, there are two reasons you clap at the end of every singer’s performance: you’re either happy they sang or you’re happy they’re done!

Many of the local Karaoke Bars in my home area are serviced by a company — call it The Music Guy — that has the beginnings of a good web site, but fails to keep the site up to date or even consistent. I can understand that: it’s hard to synchronize the list of places a person working for the company will appear with the list of people that appear at each place. And then there’s the “look” of each page: maintaining the same navigation and fonts and colors can be troublesome. Cascaded Style Sheets (CSS) fix some of that, but you still have to have consistent HTML on which to hang the CSS.

This is a perfect application for a static web site built by a templating system. And Template Toolkit Version 2 (TT2) even includes a nice tool (ttree) to take a file hierarchy and run some of the files as templates, while simply copying or ignoring others.

So, I sketched up a sample web site for The Music Guy using ttree and some dummy text. Let’s explore the result.

Swinging from the Tree

To control ttree for a project of any size, you need a ttreerc file. The simplest way to use ttree for a project is to set the TTREERC environment variable to point at a file for the project. Yes, this is inconvenient: apparently Andy Wardley thinks you’re only ever working on one thing at a time. My sample ttreerc file is given in Listing One.

LISTING ONE: An example ttreerc file

03src = src
04dest = dest
05lib = lib
06copy = \.(gif|jpg|png|css)$
07ignore = ~$
08post_chomp 1
10preprocess = data
11preprocess = templates
12process = process

Line 1 enables recursive processing, which makes sense because the tool is processing a hierarchy. Line 2 enables a verbose trace of the processing. Lines 3 and 4 define source and destination directories, relative to the current directory. Files in src are copied to the corresponding location in dest. (I’ll publish the files in dest to the web site.)

Line 5 defines a lib, which gets added to the INCLUDE_PATH for TT. This directory contains the files for INCLUDE and WRAPPER and PROCESS directives.

Line 6 defines filenames that will be copied as-is from the src to the dest hierarchy. Here, you don’t want TT to expand your images or CSS files! Similarly, Line 7 defines files that are ignored: in this case, files that end in tilde, such as the editing backup files made by Emacs. Any file that doesn’t match either the copy or ignore patterns is processed through TT.

Line 8 enables post-chomp mode, which I find convenient as it seems to eat about the right number of newlines by default.

Lines 10-12 control how each file processed through TT gets interpreted. Both data and templates are pre-processed, meaning that their contents are evaluated before each file. And process is processed in place of the original file, passing the template variable into the template so that the template can completely control additional headers and footers to be added to the file. All three of these files are located in the lib directory. (This configuration uses the PROCESS configuration setting, so see the TT documentation for further explanation and examples of that.)

Let’s look at lib/data, as shown in Listing Two. This data file defines the TT variables that hold the venues, the DJs, and the shows. The venues TT variable is a hash of hashes. The keys of venues are the “short id” for each location. Rather than referring to The Hot Seat Bar and Grill everywhere else, we simply refer to hot. However, that generally won’t make sense to the people reading the site, so the value at the key of name in the referenced hash gives the human-readable equivalent. Similarly, the djs variable contains the relevant information for the DJs.

LISTING TWO: A data file that lists venues, DJs, and appearances

venues = {
cheerful = { name = "The Cheerful Sports Page" }
harmony = { name = "The Harmony Inn" }
hot = { name = "The Hot Seat Bar and Grill" }
jimmy = { name = "Jimmy’s Sports Bar" }
jo = { name = "Jo’s Saloon" }
johns = { name = "Dr. Johns Pub" }
kenzie = { name = "McKenzie Pub" }
lair = { name = "Pete’s Lair" }
sneakers = { name = "Sneakers Pub" }
wong = { name = "Wong’s Chinese Restaurant" }

djs = {
adam = { name = "Adam" }
brian = { name = "Brian" }
chad = { name = "Chad" }
jake = { name = "Jake" }
jd = { name = "J.D." }
melissa = { name = "Melissa" }
rc = { name = "RC" }
sean = { name = "Sean" }
shannon = { name = "Shannon" }
starr = { name = "Starr" }

shows = [
{ venue = "sneakers" notes = "Starts right after the game!" }
{ venue = "hot" dj = "adam" }
{ venue = "lair" dj = "brian" }
{ venue = "cheerful" dj = "starr" }
{ venue = "johns" dj = "rc" }
{ venue = "hot" dj = "jd" }
{ venue = "jimmy" dj = "brian" }
{ venue = "cheerful" dj = "shannon" }
{ venue = "jo" dj = "rc" }
{ venue = "johns" dj = "sean" }
{ venue = "hot" dj = "shannon" }
{ venue = "cheerful" }
{ venue = "kenzie" }
{ venue = "jo" dj = "rc" }
{ venue = "hot" dj = "adam" }
{ venue = "lair" dj = "melissa" }
{ venue = "johns" dj = "rc" }
{ venue = "cheerful" dj = "jake" }
{ venue = "harmony" dj = "starr" }
{ venue = "jo" dj = "chad" }
{ venue = "wong" dj = "jd" notes = "Big cash prizes!" }
{ venue = "hot" dj = "jd" }
{ venue = "harmony" dj = "shannon" }
{ venue = "cheerful" }
{ venue = "jimmy" }
{ venue = "wong" }

The big data element is the shows mapping, which is an array of arrays, one array for each day of the week. Within that day-of-week array, the first element is a string with the day name, and the remaining elements are hashes giving the venue and the DJ (if known), using each DJ’s short identifier. You can also define notes if a particular show has unusual characteristics (starts late, has a contest, and so on). For routine maintenance, editing data should suffice.

Next, lib/templates, shown in Listing Three, defines blocks that are used for INCLUDE and WRAPPER directives in the site’s pages. The djlink creates an anchor referencing a particular DJ, used like…

[% INCLUDE djlink dj = "shannon"
prefix = "../dj/" %]

… which would refer to Shannon, showing her human-readable name (not just the internal tag), and adding ../dj/ in front of shannon.html. Similarly, venuelink let’s you say…

[% INCLUDE venuelink venue = "hot"
prefix = "" %]

… to refer to the Hot Seat in the same web directory.

Both djwrapper and venuewrapper are used in the individual DJ and venue pages to add common content. For example, as shown in Listing Seven,, src/dj/jd.html invokes djwrapper, passing the DJ id of jd, and including some text content specifically about “J.D.”. This content is passed as content into djwrapper. Line 17 of Listing Three fetches the human readable name (here, J.D.), which is used in the <h1> for the page in Line 19. Line 20 inserts the wrapped content.

LISTING THREE: A collection of templates that each page can use

01[% BLOCK djlink;
02 # dj = "dj short id"
03 # prefix = "../dj/" or "dj/" or ""
05<a href=”[% prefix; dj | uri | html %].html”>[% djs.$dj.name %]</a>
06[%- END %]
07[%##################################### %]
08[% BLOCK venuelink;
09 # venue = "venue short id"
10 # prefix = "../venue/" or "venue/" or ""
12<a href=”[% prefix; venue | uri | html %].html”>[% venues.$venue.name %]</a>
13[%- END %]
14[%##################################### %]
15[% BLOCK djwrapper;
16 # dj = "dj short id"
17 djname = djs.$dj.name;
19<h1>[% djname | html %]</h1>
20[% content %]
21<h2>Where is [% djname %]?</h2>
22[% djname | html %] can be seen at:
25 FOR day IN shows;
26 dayname = day.0;
27 FOR show IN day.slice(1);
28 venue = show.venue;
29 NEXT UNLESS show.dj == dj; # not here
30 "<li> ";
31 INCLUDE venuelink venue = venue prefix = "../venue/";
32 " on "; dayname;
33 IF show.notes;
34 " ("; show.notes | html; ")";
35 END;
36 "\n";
37 END;
38 END;
41[% END %]
42[%##################################### %]
43[% BLOCK venuewrapper;
44 # venue = "venue short id"
45 venuename = venues.$venue.name;
47<h1>[% venuename | html %]</h1>
48[% content %]
49<h2>What’s happening at [% venuename | html %]?</h2>
50[% venuename | html %] is <i>the</i> place to be on:
53 FOR day IN shows;
54 dayname = day.0;
55 FOR show IN day.slice(1);
56 NEXT UNLESS show.venue == venue; # not here
57 "<li> "; dayname;
58 IF show.dj;
59 ", hosted by ";
60 INCLUDE djlink dj = show.dj prefix = "../dj/";
61 END;
62 IF show.notes;
63 " ("; show.notes | html; ")";
64 END;
65 "\n";
66 END;
67 END;
70[% END %]

Since each DJ’s page will have the same bottom content (his or her schedule), that’s generated next in Lines 21-40. For every day in the list of shows, grab the dayname (line 26), and look at the hashes defining the shows on that day (Line 27). The DJ for the show is compared to the current DJ, and if it’s not the same, we skip the entry (Line 29). Otherwise, link to the venue (Line 31), and add show notes if any (Lines 33-35).

The output of the template for J.D. creates file dest/dj/jd.html, shown in Figure One.

FIGURE ONE: A schedule for one of the disc jockeys, created by Template Toolkit

<html><head><title>The Music Guy Karaoke</title>
<h2>Where is J.D.?</h2>
J.D. can be seen at:
<li> <a href=”../venue/hot.html”>The Hot Seat Bar and Grill</a>
on Wednesday
<li> <a href=”../venue/wong.html”>Wong’s Chinese Restaurant</a>
on Friday (Big cash prizes!)
<li> <a href=”../venue/hot.html”>The Hot Seat Bar and Grill</a>
on Saturday

Some of the data comes from the master wrapper, which you’ll see in a minute.

All of the menu items are consistent. In fact, if you didn’t like the way they looked, you could edit the templates file and ttree would re-generate all the files. Any file listed as a preprocess or process (as well as wrapper and postprocess) are automatically listed as dependencies, so any change to those files cause all files to be rebuilt. You can also explictly mark dependencies in the ttree file as well, even creating a Makefile- like syntax to describe the relationships.

You might also add other common items to this wrapper, such as an image. For example, ahead of content, you could add…

<img src=”[% dj | uri | html %]_headshot.jpg”>

… and then add headshots like jd_headshot.jpg, which would be automatically included.

The venuewrapper in Lines 43-70 works in a similar way, providing a common look for every venue. The only tricky part is that a show might or might not have a DJ assigned, so Lines 58-61 add that value conditionally. For the Hot Seat, the resulting file looks like:

<html><head><title>The Music Guy Karaoke</title>
<h1>The Hot Seat Bar and Grill</h1>
<h2>What’s happening at The Hot Seat Bar and Grill?</h2>
The Hot Seat Bar and Grill is <i>the</i> place to be on:
<li> Tuesday, hosted by <a href=”../dj/adam.html”>Adam</a>
<li> Wednesday, hosted by <a href=”../dj/jd.html”>J.D.</a>
<li> Thursday, hosted by <a href=”../dj/shannon.html”>Shannon</a>
<li> Friday, hosted by <a href=”../dj/adam.html”>Adam</a>
<li> Saturday, hosted by <a href=”../dj/jd.html”>J.D.</a>

Now, on to Listing Four, the lib/process file. This template gets control after all of the preprocess files have been executed, and instead of the actual template being processed. The value of template within the template refers to the now-deferred template. The META variables within the deferred template are available to this process template.

LISTING FOUR: The process control template

01[% content = PROCESS $template %]
02<html><head><title>[% template.title or "The Music Guy Karaoke" %]</title>
04[% content %]

For example, if you had…

[% META type = "venue" %]

… in the template, template.type would be venue, allowing the process template to execute alternative code. For example, this set of files defines a default HTML title of The Music Guy Karaoke, but any specific page can alter that with a META directive.

To execute the actual template, call…

[% PROCESS $template %]

… within this process template. In this case, we’re capturing the output into content, and reinserting that in the appropriate place in Line 4. The reason for this may not be obvious in this trivial case, but typically the PROCESS goes inside a TRY block, making sure that errors trigger alternate behavior. We can also set global variables in global or template to alter how the wrapper for the page is rendered as well, such as controlling the sidebar navigation (which isn’t shown here for simplicity).

Speaking of navigation, let’s look next at the top-level index.html, shown in Listing Five. We’re not using an individual page wrapper, but this still appears within the overall content from lib/process. The main data on the front page needs to be a dump of all shows, organized by day and venue, with links for more information. To do this, we loop over shows (yet again!) in Lines 5-25. Each interation puts one array in day, whose dayname is extracted at the end of Line 5 and shown in ine 6.

LISTING FIVE: The top-level index for the site

<h1>Welcome to Karaoke!</h1>

<h2>Our show line-up</h2>

[% FOR day IN shows; dayname = day.0; %]
<h3>[% dayname %]</h3>
FOR show IN day.slice(1);
venue = show.venue;
"<ul>\n" IF loop.first;
"<li> ";
INCLUDE venuelink venue = venue prefix = "venue/";
IF show.dj;
dj = show.dj;
", hosted by ";
INCLUDE djlink dj = dj prefix = "dj/";
IF show.notes;
" ("; show.notes | html; ")";
"</ul>\n" IF loop.last;
[% END %]

Lines 8-23 put one show hash at a time into show. This time, we’re creating a bullet list, so we place the bullet-list start on the first loop iteration in Line 10. Line 12 adds the link to the venue. Lines 13-17 add a link to the DJ, if known. And Lines 18-20 add the show notes if any. Line 22 closes off the bullet list on the last item, and it’s all done. For the section on Thursday, the result looks like Figure Two.

FIGURE TWO: This schedule for a day of the week is also generated from templates

<li> <a href=”venue/hot.html”>The Hot Seat Bar and Grill</a>,
hosted by <a href=”dj/shannon.html”>Shannon</a>
<li> <a href=”venue/cheerful.html”>The Cheerful Sports Page</a>
<li> <a href=”venue/kenzie.html”>McKenzie Pub</a>
<li> <a href=”venue/jo.html”>Jo’s Saloon</a>,
hosted by <a href=”dj/rc.html”>RC</a>

You can see that the venues are linked by their real name, and the DJs (if known) have been added and linked as well.

Just two more indexes to go. The list of DJs comes from Listing Six, or src/dj/index.html, and walks through the hash of DJs adding a link for each one. Because the DJ hash is unsorted, we first sort by the name subelement, causing them to be sorted by their first names. If we wanted a special sort order, we could have sorted by a field used only for sorting, adding a sort_by hash element, for example.

LISTING SIX: The list of disc jockeys

<h2>Check out our DJs!</h2>

[% FOR dj IN djs.keys.sort("name") %]
[% "<ul>\n" IF loop.first %]
<li> [% INCLUDE djlink dj = dj prefix = "" %]
[% "</ul>\n" IF loop.last %]
[% END %]

Similarly, Listing Eight shows src/venue/index.html, the index for the venues. Similar strategy here, so I won’t bore you with the details.

LISTING SEVEN: Data specific to one of the DJs

[% WRAPPER djwrapper dj = "jd" %]
Yes, the man that is “dj” spelled backwards!
[% END %]

LISTING EIGHT: The index page for all of the venues

<h2>Check out our venues!</h2>

[% FOR venue IN venues.keys.sort("name") %]
[% "<ul>\n" IF loop.first %]
<li> [% INCLUDE venuelink venue = venue prefix = "" %]
[% "</ul>\n" IF loop.last %]
[% END %]

And there you have it. A handful of files, creating a fully templated static web site. Any time a show gets cancelled or added, I edit data. When a new DJ gets hired, I add an entry in djs, and create a simple web page with any unique information (and a headshot, if we’ve added those graphics). The boilerplate comes for free. For a new venue, it’s an entry in venues, and a new page under the src/venue/ directory.

LISTING NINE: Data specific to an individual venue

[% WRAPPER venuewrapper venue = "hot" %]
Check out our daily drink special!
[% END %]

If I decide that I want to show how drive to a venue, I could create a template in src/templates/ that I call from each venue, or I could put the addresses in the venues hash and generate it directly from the venuewrapper template. With templates, the commonality is captured, and the data defines the differences.

I hope this little exposure to ttree inspires you to template-ize some of your repetitive tasks. Until next time, enjoy!

Tantalizing tenor Randal Schwartz appears regularly in Las Vegas, NH, when not casting Perl spells for Stonehenge Consulting. You can reach Randal at class="emailaddress">merlyn@stonehenge.com. You can download the sample files shown in this article from http://www.linux-mag.com/downloads/2006-03/perl.tar.gz.

Comments are closed.