Building a blog with Metalsmith
By Rob Ribeiro | Published | Updated
When you want to be able to deploy interesting web projects alongside a blog with a unified and personal theme, using a CMS or blog engine stops becoming an option. Maybe you just don't want to deal with all of the concerns of hosting, securing, and updating WordPress on your own. Or maybe you want to be able to serve lots of visitors quickly on slim hardware. If any of these sounds like you, it is time to look at static site generators, such as Metalsmith. This post covers how to get started building a basic blog with Metalsmith.
Video
Why spend a few minutes reading when you could spend longer listening to me talk?
Intro
First, let's cover some background. Metalsmith is a static site generator written for Node.js. It is very much in the spirit of Gulp, allowing you to define a build pipeline using plugins. This pipeline takes your directory tree of documents, assets, and templates and passes them through a series of plugins that process them into a full website. All you need to do is pick and configure the plugins that you need. Let's get started!
There are many different plugins in the Metalsmith ecosystem to assist with processing various file types. For the purpose of this article, we will be working with the following: Less for CSS pre-processing, markdown for document formatting, and jade for templating. We will combine plugins to process these with plugins that can help us gather posts into collections, generate excerpts, create permalinks, and more.
Before I show any other code, I want to give you the require
calls and
corresponding variables that will be used throughout the article:
var metalsmith = require('metalsmith'),
branch = require('metalsmith-branch'),
collections = require('metalsmith-collections'),
excerpts = require('metalsmith-excerpts'),
markdown = require('metalsmith-markdown'),
permalinks = require('metalsmith-permalinks'),
serve = require('metalsmith-serve'),
templates = require('metalsmith-templates'),
watch = require('metalsmith-watch'),
moment = require('moment');
Directory structure
Before jumping into things, let's lay out the basic directory structure of our demo Metalsmith project:
/project_dir
|-/node_modules
|-/build
|-/src
|-/posts
|-test-post.md
|-/templates
|-layout.jade
|-post.jade
|-posts.jade
|-posts.md
|-build.js
|-package.json
As with other Node.js projects, we will have a package.json
and /node_modules
.
In addition, we will define your Metalsmith pipeline in build.js
file. You are
welcome to call it something else, but I find node build
to be a fairly descriptive
command for what we are trying to do here. Beyond this, there are three other
directories: /src
, /build
, and /templates
. Our jade templates go in /templates
.
The general site structure is composed in /src
, and the output of processing that
through the build pipeline is output into /build
. You will see more of the details of
how this works below.
Basic concept
Let's start without any sort of plugin processing:
var siteBuild = metalsmith(__dirname)
.metadata({
site: {
title: 'azurelogic.com',
url: 'https://azurelogic.com'
}
})
.source('./src')
.destination('./build')
// build plugins go here
.build(function (err) {
if (err) {
console.log(err);
}
else {
console.log('Site build complete!');
}
});
This chunk of code forms the foundation of our static site pipeline. Let's talk
about what everything does. metalsmith(__dirname)
starts up the Metalsmith
generator with __dirname
as the base directory. .source('./src')
then sets
the source directory for where all of our markdown files, templates, javascript,
Less, CSS, etc should come from. Conversely, .destination('./build')
sets
the destination directory for all of these files. .build(callback)
triggers the
build pipeline to be processed. Since we have not invoked any plugins, all this does
is copy the files and directory structure from /src
to /build
. If you add
files anywhere in the tree below /src
and that file is not processed by a plugin,
it will get copied over to /build
with its path intact. There is one
other important function to mention here: .metadata(obj)
allow you to inject any
additional metadata into the process for use in templates or other plugins. I have
found it useful for injecting my site title and url, but there are other uses with
other plugins.
Document structure and metadata
We just discussed metadata in the build pipeline, but there is much more to the metadata than just that. Every file should begin with a metadata (or front matter) section formatted like this:
---
key1: value1
key2: value2
---
You don't have to have any data in the metadata section. In fact, my Less and jade files omit it entirely. You will likely find this section most useful for populating data used by templates directly or by various plugins to process markdown documents. We will see more of this in the next section.
Adding markdown and template processing
If you take note of the comment in the generic pipeline block above,
you see that the build pipeline is built up between the Metalsmith setup and the call
to build(callback)
. This build pipeline is composed of use(plugin)
calls. Let's
see what this looks like with markdown and jade template processing added to the
pipeline.
var siteBuild = metalsmith(__dirname)
.metadata({
site: {
title: 'azurelogic.com',
url: 'https://azurelogic.com'
}
})
.source('./src')
.destination('./build')
.use(markdown())
.use(templates({
engine: 'jade',
moment: moment
}))
.build(function (err) {
if (err) {
console.log(err);
}
else {
console.log('Site build complete!');
}
});
It is important to note that order matters here. We have to process the markdown docs into HTML before we can insert that markup into the templates. It is also worth noting that we can even pass other libraries into the template engine, as I have done with moment. This allows for formatting date strings inside of the templates. This is a very powerful feature. So let's take a look at a sample markdown doc and jade templates.
test-post.md
---
title: test post
publishDate: 2015-01-01
modifyDate: 2015-01-01
author: Rob Ribeiro
template: post.jade
---
This is just a test
post.jade
extends layout
block main
article#article
h1= title
div By #{author} | Published #{moment(publishDate).format("M.D.YYYY")}
if modifyDate && moment(publishDate).isBefore(modifyDate)
span Updated #{moment(modifyDate).format("M.D.YYYY")}
div!= contents
layout.jade
doctype html
html
head
block head
block title
title= title ? title + " | " + site.title : site.title
block styles
body
block body
#layout-main
block main
You should immediately notice the front matter on test-post.md. The template
field is what tells the template plugin which template file to use. The rest of
this metadata is useful throughout the build pipeline, and it is even available
inside of the templates. post.jade consumes some of this metadata even. It
interpolates author
as well as the publishDate
and modifyDate
after being
formatted through moment, which as mentioned before was being passed into the
template engine earlier. You can also see that we are able to do conditional logic,
even involving the site.title
, which was set way back in the call to metadata(obj)
in the build pipeline. This allows for very rich configuration and templating.
In my full build, I even have conditions based on whether I am in dev or production.
Testing
Now that we have established a basic build pipeline, we can start to build pages. However, it is difficult to write blindly and know that it will all work in production. We can use other plugins to serve the plugins and even refresh the files as we work on them. This is where serve and watch come into play:
.use(serve({
port: 8080,
verbose: true
}))
.use(watch({
pattern: '**/*',
livereload: true
}))
we need to keep them towards the bottom of your pipeline, as the build pipeline
should be essentially complete by the time you reach them. The serve plugin acts as a
webserver, and the watch plugin acts as to reprocess individual files through the
pipeline as we edit and save. Note that this only applies to files in our source
directory, which does not include /templates
. In order to have live reload
functionality, you will need to add this to script tag to your layout template:
script(src="http://localhost:35729/livereload.js")
Processing Collections, Excerpts, and Permalinks
So, let's expand the functionality of our pipeline now. We now want a blog roll page that
can show all of our posts with the first paragraph of each of them. In addition, we want
our urls to look like https://azurelogic.com/projects/
instead of
https://azurelogic.com/projects.html
. The first goal is accomplished by a combination
of the excerpts and collections plugins. The second goal is done with the permalinks
plugin, but we're going to include the branch plugin to show how it works and to do some
route restructuring. Let's look at the code first:
.use(excerpts())
.use(collections({
posts: {
pattern: 'posts/**.html',
sortBy: 'publishDate',
reverse: true
}
}))
.use(branch('posts/**.html')
.use(permalinks({
pattern: 'posts/:title',
relative: false
}))
)
.use(branch('!posts/**.html')
.use(branch('!index.md').use(permalinks({
relative: false
})))
)
The first thing to think about here is how this fits into the pipeline. In order to process the template for the posts page correctly, we will need to have the excerpts and collections, which means that this chunk goes in between the markdown and template plugins in the pipeline. Excerpts extracts the content of the first
tag into metadata, while collections creates a collection of files in the metadata for selective iteration. This is very useful in designing a blog roll:
posts.md
---
title: blog of posts
template: posts.jade
paginate: posts
---
posts.jade
extends layout
block main
#posts
h1 posts
if collections.posts && collections.posts.length > 0
each post in collections.posts
h2
a(href='/'+post.path)!= post.title
div!= post.excerpt
else
h2= collections.posts.length
h2 there ain't no posts here, man
Notice that the posts.md file only contains front matter. That is because it only
serves to trigger the building of the designated template and to provide the required
metadata. In addition, we now have metadata for the posts
collection
and for excerpt
on each post. This makes for a very concise "repeater" concept that
accomplishes our first goal.
Moving on to permalinks and branch, we have two branches: posts and "not posts"
(also, now that markdown has been processed, these are .html
files, not .md
).
This allows us to rewrite items in the post directory based on their title
metadata
instead of the filename. You could also use this process to include the year or full date.
This is also why we need the branch. We don't want to rewrite the url of any of our
other pages like this.
Wrap-up and Complete Pipeline
With all of this put together, we have the basis for building a static site with Metalsmith that includes a blog and is highly extensible. In future articles on Metalsmith, I plan to cover other plugins to enable things like sitemaps, rss, tags, and pagination, as well as techniques for multiple pipelines and optional sections. Until then, here is the completed pipeline discussed in this article:
var metalsmith = require('metalsmith'),
branch = require('metalsmith-branch'),
collections = require('metalsmith-collections'),
excerpts = require('metalsmith-excerpts'),
markdown = require('metalsmith-markdown'),
permalinks = require('metalsmith-permalinks'),
serve = require('metalsmith-serve'),
templates = require('metalsmith-templates'),
watch = require('metalsmith-watch'),
moment = require('moment');
var siteBuild = metalsmith(__dirname)
.metadata({
site: {
title: 'azurelogic.com',
url: 'https://azurelogic.com'
}
})
.source('./src')
.destination('./build')
.use(markdown())
.use(excerpts())
.use(collections({
posts: {
pattern: 'posts/**.html',
sortBy: 'publishDate',
reverse: true
}
}))
.use(branch('posts/**.html')
.use(permalinks({
pattern: 'posts/:title',
relative: false
}))
)
.use(branch('!posts/**.html')
.use(branch('!index.md').use(permalinks({
relative: false
})))
)
.use(templates({
engine: 'jade',
moment: moment
}))
.use(serve({
port: 8080,
verbose: true
}))
.use(watch({
pattern: '**/*',
livereload: true
}))
.build(function (err) {
if (err) {
console.log(err);
}
else {
console.log('Site build complete!');
}
});