Initially, I intended to run the site as dynamic content, using Node.js & Express. But I instead used Node.js as a static site generator, spitting out html pages that I can then push to Github Pages or surge.sh.


The Options

After a bit of research, I came across a bunch of blog-ready static site generators for Node.js (see StaticGen).

I decided on Metalsmith, due to the bare-bones structure and the flexibility of it's optional plugins.


How It Works

Installation is simple. I already had Node.js and npm installed, so I chose a directory and navigated to it in terminal.

npm install metalsmith --save

This installs Metalsmith and it's dependencies in the node_modules directory. It also creates a package.json file containing the required npm packages for this project.

Now I created an app file in the root directory that contains Metalsmith app. I called it build.js. To get started, this file contains the following:

var Metalsmith = require('metalsmith');
// require plugins here
Metalsmith(__dirname)
  .source('./src')
  .destination('./build')
  // build plugins here
  .build(function(err){
    if (err){
      console.log(err);
    } else {
      console.log('Site build complete!');
    }
  });

Now, when I run the terminal command node build from the project root directory, Metalsmith will grab the source directory, run it through any plugins and spit it out at the build directory.

This is what my source directory looks like:

|-/src
  |-/fonts
  |-/images
  |-/js
  |-/posts
    |-creating-this-site.md
  |-/scss
    |-_normalize.scss
    |-style.scss
  |-404.md
  |-blog.md
  |-index.md

Without plugins, Metalsmith will simply copy the contents of this source directory to the build directory.


The Plugins

What plugins did I want to use? I found this out as I went along, but for starters I wanted to:

  • use Jade for templating the html
  • use Sass for compiling css
  • concatenate JS files
  • use Markdown for writing posts & pages
  • create a Blog page containing a list (or 'collection') of posts

So after hitting the Metalsmith plugins directory, I found the plugins I needed to get started.

I installed the plugins by going to the project root in terminal and typing:

npm install metalsmith-sass --save
## Replace metalsmith-sass with plugin name.
## Repeat for each plugin

To use the plugins with Metalsmith, I required each at the top of build.js:

var Metalsmith = require('metalsmith'),
    // require plugins here
    collections = require('metalsmith-collections'),
    excerpts = require('metalsmith-excerpts'),
    sass = require('metalsmith-sass'),
    markdown = require('metalsmith-markdown'),
    permalinks = require('metalsmith-permalinks'),
    layouts = require('metalsmith-layouts'),
    concat = require('metalsmith-concat');

Now I integrated the plugin into the Metalsmith pipeline, passing options as an object:

Metalsmith(__dirname)
  .source('./src')
  .destination('./build')
  // build plugins here
  .use(collections({
    posts: {
      pattern: 'posts/*.md',
      sortBy: 'date',
      reverse: true
    }
  }))
  .use(markdown())
  .use(excerpts())
  .use(layouts({
    engine: "jade",
    moment: moment
  }))
  .use(concat({
    files: 'js/**/*.js',
    output: 'js/min/main-min.js'
  }))
  .use(sass({
    outputDir: 'css/'
  }))
  .build(function(err){
    if (err){
      console.log(err);
    } else {
      console.log('Site build complete!');
    }
  });

Each plugin manipulates files in the source directory. The order can be critical: converting from markdown to html prior to applying layouts with Jade.


Layouts

So how do layouts work? My layouts live in a directory /layouts containing:

|-/layouts
  |-/partials
    |-_head.jade
    |-_header.jade
  |-blog.jade
  |-layout.jade
  |-page.jade
  |-post.jade

I started with a master layout called layout.jade.

//- layout.jade
doctype
html
  head
    include partials/_head.jade
  body
    .container.row
    include partials/_header.jade
      .eight.columns.content
          block content

layout.jade includes the partials: _head.jade and _header.jade:

//- partials/_head.jade
title= title ? title + ' - ' + 'Eric Jinks' : 'Eric Jinks'

meta(charset="utf-8")

link(href='/css/style.css' rel='stylesheet' type='text/css')

script(src="/js/min/main-min.js")
//- partials/_header.jade
header
  .taright.four.columns
    h1.logo
      a(href="/")Eric Jinks
    nav.nav
      ul
        li
          a(href="/blog") blog
        li
          a(href="/") about
    ul.social
      - var twitter = "twitter.com/jinksi"
      - var github = "github.com/jinksi"
      - var instagram = "instagram.com/jinksi"
      - var email = "ericjinks@gmail.com"
      li
        a(href="http://#{twitter}" title="#{twitter}")
          i(class="icon ion-social-twitter")
        a(href="http://#{github}" title="#{github}")
          i(class="icon ion-social-github")
        a(href="http://#{instagram}" title="#{instagram}")
          i(class="icon ion-social-instagram")
        a(href="mailto#{email}" title="#{email}")
          i(class="icon ion-email")

Note the use of variables and logic in Jade === so much awesome.

Now I create a page template page.jade.

//- page.jade
extends layout

block content
  div!= contents

!= represents unescaped html.

This is what a markdown page from the source directory looks like (index.md):

---
title: About
layout: page.jade
---
Hello. Text goes here......

To add another page, I simply add another markdown file in the source directory:

---
title: 404 - Page Not Found
layout: page.jade
---
This page doesn't exist

And the Blog page blog.md:

---
title: Blog
layout: blog.jade
---

Which uses the blog layout - blog.jade:

//- blog.jade
extends layout
block content
  ul.post-list
    each post in collections.posts
      li.post-item
        h5.title
          a(href="/#{post.path}")
           =post.title
        div.details #{moment(post.publishDate).format("Do MMM YYYY")}
        div !{post.excerpt}
          a(href="/#{post.path}" class="readmore").u-pull-right read more
          .clear
      hr

The blog layout works in combination with the metalsmith-collections plugin to create the blog list.

Posts work in a similar way. Here is a basic test-post.md, inside the /src/posts directory:

---
title: Test Post
publishDate: 2015-10-05
author: Eric
layout: post.jade
---
Post content goes here.

post.jade:

//- post.jade
extends layout
block content
  .post-header
    h4.post-title= title
    div.details #{author}, #{publishDate}
  .post-content      
    div!= contents

What Went Wrong?

I was getting errors with the metalsmith-sass plugin. This had something to do with Xcode Command Line Tools being out of date on my machine. I solved this using the instructions found here.


The Result

Finally, after adding a few more Metalsmith plugins, this is what my project directory looks like:

/project_dir
|-/node_modules
|-/build
|-/layouts
  |-/partials
    |-_head.jade
    |-_header.jade
  |-blog.jade
  |-layout.jade
  |-page.jade
  |-post.jade
|-/src
  |-/fonts
  |-/images
  |-/js
  |-/posts
    |-creating-this-site.md
  |-/scss
    |-_normalize.scss
    |-style.scss
  |-404.md
  |-blog.md
  |-index.md
|-build.js
|-package.json

And this is what my build.js contains:

var Metalsmith = require('metalsmith'),
    branch = require('metalsmith-branch'),
    collections = require('metalsmith-collections'),
    excerpts = require('metalsmith-excerpts'),
    sass = require('metalsmith-sass'),
    markdown = require('metalsmith-markdown'),
    permalinks = require('metalsmith-permalinks'),
    serve = require('metalsmith-serve'),
    layouts = require('metalsmith-layouts'),
    watch = require('metalsmith-watch'),
    concat = require('metalsmith-concat'),
    moment = require('moment');

Metalsmith(__dirname)
  .metadata({
    site: {
      title: 'Eric Jinks',
      url: 'http://ericjinks.com'
    }
  })
  .source('./src')
  .destination('./build')
  .use(collections({
    posts: {
      pattern: 'posts/*.md',
      sortBy: 'date',
      reverse: true
    }
  }))
  .use(markdown())
  .use(excerpts())
  .use(branch('posts/**.html')
    .use(permalinks({
      pattern:':collection/:title'
    }))
  )
  .use(branch('**.html')
    .use(branch('!index.html')
      .use(branch('!404.html')
        .use(permalinks({
          pattern:':title'
        }))
      )
    )
  )
  .use(layouts({
    engine: "jade",
    moment: moment
  }))
  .use(concat({
    files: 'js/**/*.js',
    output: 'js/min/main-min.js'
  }))
  .use(sass({
    outputDir: 'css/'
  }))
  .use(serve({
    port: 8080,
    http_error_files: {
      404: "/404.html"
    }
  }))
  .use(watch({
    paths: {
        "${source}/**/*": "**/*",
        "layouts/**/*": "**/*"
      },
    livereload: true
  }))
  .build(function(err){
    if (err){
      console.log(err);
    } else {
      console.log('Site build complete!');
    }
  });

What Next?

(Read Part 2 here)

This blog could use a tag or category system à la Wordpress, an RSS feed, pagination and a draft post system. There are Metalsmith plugins for all of these and more. If not, plugins can easily be crafted with a bit of basic JS (source):

var plugin = function(files, metalsmith, done) {
  console.log(files);
  done();
};

Metalsmith(__dirname)
  //...
  .use(plugin)
  //...

More info

Node.js
npm
Jade
Metalsmith.io
Awesome Metalsmith
Getting to Know Metalsmith
Building a blog with Metalsmith