How we used our hack of the metalsmith templates plugin to allow master templates

Using master templates with Metalsmith

By: David

Master template

Recently I hacked the metalsmith-templates plugin to support master templates. The idea is that you pass in a master option, naming a master template file in the templates directory. This template will be applied to all files after an eventual file-specific template and/or a default template.

Here's what our call to the templates plugin looks like:

.use(templates({
  engine: 'handlebars',
  directory: './templates',
  master:'master.hbt',
  pattern: ["*/*/*html","*html"]
}))

The contents variable in the master template file will contain the full result of the previous template, or the raw file contents if no previous template has been run. This enabled us to really clean up our page-specific templates (index, post, author, tag, etc), as they no longer needed to deal with headers and footers and the like.

The power of partials

We have a subdirectory in the templates directory named partials. Any file put here will automatically be added as a Handlebars partial through the following loop:

_.each(fs.readdirSync('templates/partials'),function(file){
  var name = file.split(".")[0],
      contents = fs.readFileSync(__dirname+"/templates/partials/"+file).toString();
  Handlebars.registerPartial(name,contents);
});

Combined with using a master template, this makes for really skinny page templates! Here's the full code for the index page template:

{{#posts}}

{{> listedpost root=true}}

{{/posts}}

It loops through the posts array, and prints each post using the listedpost partial. Note that we're also passing a hash with extra variables which will extend the context, a syntax available since Handlebars 2.

Here's what listedpost looks like:

<article>
  {{> posthead root=root}}
  <section class="post-excerpt">
    <p>{{excerpt}}</p>
  </section>
</article>

Each markdown post file on our blog contains an excerpt in the YAML front matter, which is what we use when we show a post in a list.

Here's the posthead partial, where we finally have use for the root variable.:

<header class="post-header">
  <h2 class="post-title">
    <a href="{{#unless root}}../../{{/unless}}{{path}}">{{{title}}}</a>
    <span class="post-meta">
      <time datetime="{{date}}">{{moment date 'MMM Do YYYY'}}</time> 
    </span>
  </h2>
    <div class='tags'>
      By: <span><a href='{{#unless root}}../../{{/unless}}about/{{toLowerCase author}}'>{{author}}</a></span>
    </div>
    <div class="tags">
      Tags:
      {{#tags}}
        <span><a href='{{#unless ../root}}../../{{/unless}}tags/{{this}}/'>{{this}}</a></span>
      {{/tags}}
    </div>
</header>

This partial is also used in the post.hbt template, hence the need for the root flag as a post-specific file is two levels deeper. Here's the post.hbt code, with some boring Disqus stuff removed:

<article>

  {{> posthead}}

  <section class='post-content'>
  {{{contents}}}
  </section>

  <!-- Disqus code redacted --->

</article>

Page types

To centralize control over template usage, and allow for some further shortcuts in the templates, we invented the notion of page types. In all markdown files to be processed, instead of naming templates, we have a type variable in the YAML front matter. So far, type can be post, author, index, tag or taglist. For example, here's the YAML for this very post:

title: Using master templates with Metalsmith
author: David
tags: [metalsmith,handlebars]
date: 2014-08-21
excerpt: How we used our hack of the metalsmith templates plugin to allow master templates
type: post

We use the type variable through a mini Metalsmith plugin running all files through the following map:

.use(function(files,metalsmith,done){
  _.map(files,function(file){
    return file.type ? _.extend(file,{template:file.type+".hbt"},_.object(["is"+file.type],[true])) : file;
  });
  done();
})

Now a file with type set to post will have a template and ispost variable added like thus:

type: post
template: post.hbt
ispost: true

This means that should we change templating tactics, we don't need to update all individual files, instead we can simply add some logic to our mini plugin loop.

The last thing added in the example above, ispost: true, allows us to simplify doing post-specific stuff in the Handlebars templates. Testing for equality in Handlebars is complicated, but testing truthiness is easy, which is why the ispost type variables are useful. As an example, here's a snippet from the master template:

{{#if isindex}}
  <link rel="stylesheet" href="css/theme.css"/>
{{else}}
  <link rel="stylesheet" href="../../css/highlight.css">
  <link rel="stylesheet" href="../../css/theme.css"/>
{{/if}}
comments powered by Disqus