Metalsmith is a plugin-based static site generator originally from Segment . It's a current favorite of mine because of how sites are built as a pipeline of plugins where the output of each plugin is the input of the next. This allows for strong control over what happens and when it happens.
A simple pipeline could look like:
- Set the input directory.
- Convert all Markdown files to HTML.
- Process all HTML files through a Handlebars template.
- Set the output directory.
- Clean the output directory.
- Output all files.
While a more complex pipeline could look like:
- Set the input directory.
- Validate the frontmatter in all Markdown files.
- Compile all Sass files to CSS.
- Reduce the size of large images.
- Convert all Markdown files to HTML.
- Process all HTML files through a Handlebars template.
- Add a favicon.
- Minify HTML, CSS, and JavaScript.
- Build a sitemap.
- Set the output directory.
- Clean the output directory.
- Output all files.
As of writing, this website is built using Metalsmith and 44 plugins.
In this tutorial we'll first set up a blank project and then we'll continue to iterate on it by adding additional plugins and source files.
Setting up Metalsmith
Setting up a Metalsmith project is a lot like setting up any other Node.js project, in the end you'll have a package.json
and package-lock.json
, index.js
, and other source files throughout.
This guide assumes you already have Node.js installed.
1. Directory structure
The official Metalsmith examples recommend a certain directory structure, so we'll stick to that. Throughout this guide I will continue to list the directory tree to keep things clear.
Go ahead and make a new directory for your project and a src/
directory under that:
.
└── src
nvm
2. Set up nvm
or "Node Version Manager" is a way to install and run different versions of Node.js at the same time on one machine. But more importantly, it lets the project indicate what version should be used.
The full instructions for installation are on GitHub , but in general it is as simple as:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.34.0/install.sh | bash
Metalsmith requires a Node.js version above 0.12.x so we'll instruct nvm
to use the latest LTS version (currently 10.16.3) by creating a .nvmrc
file at the root of our project with the contents:
lts/*
And then running the command:
nvm install
Now our directory tree will look like:
.
├── .nvmrc
└── src
npm
3. Set up npm
or "Node Package Manager" is a tool to install Node.js packages and is the recommended way to install Metalsmith. To set up our project run the command:
npm init
And follow the prompts. Hitting the enter key for all prompts is fine for now if you don't want to fill it all out. This will create an initialized package.json
for us:
.
├── .nvmrc
├── package.json
└── src
4. Set up Metalsmith
From the root of our project run the command:
npm install --save metalsmith
Which will install Metalsmith and all required dependencies. This will create the node_modules/
directory (with many subdirectories in it) and the package-lock.json
file. To save some space I'll skip writing out all the extra node_modules/
subdirectories:
.
├── .nvmrc
├── node_modules
│ └── metalsmith
├── package-lock.json
├── package.json
└── src
Now we're going to set up a dummy pipeline to make sure Metalsmith is installed and working right. Create an index.js
file with the contents:
const Metalsmith = require('metalsmith');
Metalsmith(__dirname)
.source('./src') // source directory for the pipeline
.destination('./build') // destination directory of the pipeline
.clean(true) // clean the destination directory before build
.build(function (err) { // execute the build
if (err) {
throw err;
}
});
And then run the command:
node index
If there is no output from the command we know Metalsmith is working right.
5. Adding an index page
You might have noticed after the node index
command above it didn't actually create the build/
directory like we configured in index.js
, that's because there's nothing for it to output yet. Let's fix that by creating a file src/index.html
with the contents:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
</body>
</html>
And then try the command again:
node index
We'll know this worked if it copied src/index.html
to build/index.html
:
.
├── .nvmrc
├── build
│ └── index.html
├── index.js
├── node_modules
│ └── metalsmith
├── package-lock.json
├── package.json
└── src
└── index.html
6. Creating a layout
We're not living in 1999 anymore and we have the power of HTML templating systems these days, thankfully. Handlebars (.hbs
) is a popular choice for Metalsmith and is what we're going to use, but there are plugins for other systems such as Nunjucks (.njk
) as well.
Let's install both the layouts plugin as well as Handlebars like we did with Metalsmith:
npm install --save metalsmith-layouts jstransformer-handlebars
Now we need to add metalsmith-layouts
as a step in our build process in index.js
:
const Metalsmith = require('metalsmith');
const layouts = require('metalsmith-layouts');
Metalsmith(__dirname)
.source('./src') // source directory for the pipeline
.use(layouts({ // <-- process all HTML files with Handlebars
default: 'page.hbs',
engine: 'handlebars'
}))
.destination('./build') // destination directory of the pipeline
.clean(true) // clean the destination directory before build
.build(function (err) { // execute the build
if (err) {
throw err;
}
});
By default metalsmith-layouts
will look for templates in the layouts/
directory, so let's create that and a file layouts/page.hbs
that looks very similar to src/index.html
but with a special Handlebars expression:
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
{{{ contents }}}
</body>
</html>
{{{ contents }}}
here will be replaced with the body of any source file that uses this template. We use {{{ }}}
instead of {{ }}
to preserve any HTML in the source files.
Now that we've moved the structural HTML from src/index.html
to layouts/page.hbs
we can write a page body in src/index.html
:
<h1>This is a title!</h1>
<p>This is a paragraph.</p>
Then build:
node index
We'll end up with build/index.html
looking like:
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<h1>This is a title!</h1>
<p>This is a paragraph.</p>
</body>
</html>
And a directory tree looking like:
.
├── .nvmrc
├── build
│ └── index.html
├── index.js
├── layouts
│ └── page.hbs
├── node_modules
│ ├── jstransformer-handlebars
│ ├── metalsmith
│ └── metalsmith-layouts
├── package-lock.json
├── package.json
└── src
└── index.html
7. Using Markdown
It's more common to use Markdown instead of HTML as an input source in Metalsmith because of its ease-of-use and familiarity among developers. Plus it's more portable so you'll be able to use it with a different static site generator in the future.
Markdown conversion requires another plugin:
npm install --save metalsmith-markdown
And an addition to our index.js
:
const Metalsmith = require('metalsmith');
const markdown = require('metalsmith-markdown');
const layouts = require('metalsmith-layouts');
Metalsmith(__dirname)
.source('./src') // source directory for the pipeline
.use(markdown()) // <-- convert Markdown to HTML
.use(layouts({ // process all HTML files with Handlebars
default: 'page.hbs',
engine: 'handlebars'
}))
.destination('./build') // destination directory of the pipeline
.clean(true) // clean the destination directory before build
.build(function (err) { // execute the build
if (err) {
throw err;
}
});
Then rename src/index.html
to src/index.md
and change it to Markdown syntax:
# This is a title!
This is a paragraph.
And build:
node index
Which will give us a build/index.html
very similar to before (heading IDs can be turned off with a metalsmith-markdown
option):
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<h1 id="this-is-a-header">This is a title!</h1>
<p>This is a paragraph.</p>
</body>
</html>
And a directory tree:
.
├── .nvmrc
├── build
│ └── index.html
├── index.js
├── layouts
│ └── page.hbs
├── node_modules
│ ├── jstransformer-handlebars
│ ├── metalsmith
│ ├── metalsmith-layouts
│ └── metalsmith-markdown
├── package-lock.json
├── package.json
└── src
└── index.html
8. Frontmatter in Markdown
Wouldn't it be neat if we had a real page title instead of <title></title>
? That's where frontmatter and page metadata comes in.
According to Middleman :
Frontmatter allows page-specific variables to be included at the top of a template using the YAML or JSON format.
Adding frontmatter to our src/index.md
looks like this:
---
title: This is the Index
---
# This is a title!
This is a paragraph.
Where anything between the ---
's is parsed as YAML and is made available to the templating system. Now we can use that information in layouts/page.hbs
like so:
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{ title }}</title>
</head>
<body>
{{{ contents }}}
</body>
</html>
And after building:
node index
We get build/index.html
:
<html lang="en">
<head>
<meta charset="UTF-8">
<title>This is the Index</title>
</head>
<body>
<h1 id="this-is-a-header">This is a title!</h1>
<p>This is a paragraph.</p>
</body>
</html>
9. Adding another page
All of the work so far has set us up nicely to be able to add new, consistent pages very quickly. Let's add an about page at src/about.md
:
---
title: About
---
This is a sample website built with Metalsmith.
Which after build:
node index
Produces build/help.html
:
<html lang="en">
<head>
<meta charset="UTF-8">
<title>About</title>
</head>
<body>
<p>This is a sample website built with Metalsmith.</p>
</body>
</html>
And a directory tree looking like:
.
├── .nvmrc
├── build
│ ├── help.html
│ └── index.html
├── index.js
├── layouts
│ └── page.hbs
├── node_modules
│ ├── jstransformer-handlebars
│ ├── metalsmith
│ ├── metalsmith-layouts
│ └── metalsmith-markdown
├── package-lock.json
├── package.json
└── src
├── help.md
└── index.html
10. Using template partials
Good development practices encourage splitting up code into smaller pieces that can be composed together. Template partials allow us to define HTML snippets, such as headers or footers, that can be used and re-used in our templates.
To make our lives easier let's install another plugin:
npm install --save metalsmith-discover-partials
And add it to our index.js
:
const Metalsmith = require('metalsmith');
const discoverPartials = require('metalsmith-discover-partials');
const markdown = require('metalsmith-markdown');
const layouts = require('metalsmith-layouts');
Metalsmith(__dirname)
.source('./src') // source directory for the pipeline
.use(discoverPartials()) // <-- find and register template partials
.use(markdown()) // convert Markdown to HTML
.use(layouts({ // process all HTML files with Handlebars
default: 'page.hbs',
engine: 'handlebars'
}))
.destination('./build') // destination directory of the pipeline
.clean(true) // clean the destination directory before build
.build(function (err) { // execute the build
if (err) {
throw err;
}
});
metalsmith-discover-partials
looks for Handlebars files in the partials/
directory by default, so go ahead and create that and the file partials/header.hbs
:
<b>This is the header.</b>
<hr>
And we can reference it by its filename base in layouts/page.hbs
like this:
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{ title }}</title>
</head>
<body>
{{>header}}
{{{ contents }}}
</body>
</html>
And after one last build:
node index
We end up with a build/index.html
of:
<html lang="en">
<head>
<meta charset="UTF-8">
<title>This is the Index</title>
</head>
<body>
<b>This is the header</b>
<hr>
<h1 id="this-is-a-title">This is a title!</h1>
<p>This is a paragraph.</p>
</body>
</html>
And a directory tree of:
.
├── .nvmrc
├── build
│ ├── help.html
│ └── index.html
├── index.js
├── layouts
│ └── page.hbs
├── node_modules
│ ├── jstransformer-handlebars
│ ├── metalsmith
│ ├── metalsmith-discover-partials
│ ├── metalsmith-layouts
│ └── metalsmith-markdown
├── package-lock.json
├── package.json
├── partials
│ └── header.hbs
└── src
├── help.md
└── index.html
Conclusion
Metalsmith is a great option for someone looking for a lot of control in a static site generator. There are many different plugins that exist for processing different kinds of inputs and there is no strict order they need to be used in. With a minimal amount of setup that should be familiar to existing Node.js developers it's very quick to get started with Metalsmith.