There's a total of 395 static site generators out there on the web, and probably even more found on Github. It's pretty clear to see that developers want good tooling to rapidly build static sites. Github advocates for Jekyll and allows you to freely host a Jekyll site as a Organization or Project. A lot of people might be okay with this solution for a simple blog or open source project, however if you want a faster development environment and more flexibility to customize the functionality of your site, you might find this solution limiting. As I was rebuilding my site, I definitely wanted to shift off of Jekyll and go with something that allowed for extending functionality with Javasrcipt in ES6.
Metalsmith is an extremely pluggable static site generator built with Javascript and Node.js by the team over at Segment. It has a simple design in and of itself, however the power comes in from the number of plugins available in the Metalsmith ecosystem. In this post we'll take a look at some of these plugins and how they might fit into a simple workflow of building a static blog.
To get started let's create a new directory: mkdir metalsmith-blog and initialize it with npm init. You can build your site with either the Metalsmith CLI or the Javascript API. Let's take a look at both and you'll get to see which one might work better for your particular use case.
To use the Metalsmith CLI, let's install it globally with npm i -g metalsmith. In order for metalsmith to do it's work, we'll need to create a metalsmith.json file with a configuration for our site:
{
"source": "src",
"destination": "build",
"plugins": {}
}
With this configuration, Metalsmith will read all the files in the source directory (src) and copy them into the destination (build) directory. As you can see we'll also have the opportunity to include plugins that will process our files along the way. Let's add a few plugins that we can use to build a basic blog page with posts.
We'll need a way to render markdown files to html, so let's install metalsmith-markdown.
$ npm i metalsmith-markdown --save
Great, we'll also need a way to render our markdown in some sort of template. Metalsmith used to have a metalsmith-templates plugin, however this has been divided into two smaller plugins named metalsmith-layouts and metalsmith-in-place. At first, I wasn't sure the difference here, but there's a difference between the two. metalsmith-in-place renders files within your src directory, whereas metalsmith-layouts renders files from a root layouts directory. This allows you to separate layout specific code from source-specific template logic that may exist in a source file. One of the benefits of Metalsmith is how flexible it is in regards to templating. With consolidate.js behind the scenes, you can use any templating engine of your choice. In this case, we'll use liquid because of it's familiar for people migrating from Jekyll.
Let's install them both:
$ npm i metalsmith-layouts metalsmith-in-place --save
Let's also install tinyliquid for our template engine.
$ npm i tinyliquid --save
Great, lastly we'll need a way to output a collection of posts and create pretty permalinks such as /blog/metalsmith-post/ versus /blog/metalsmith-post/index.html. Let's do that by installing metalsmith-collections and metalsmith-permalinks.
$ npm i metalsmith-permalinks metalsmith-collections --save
Cool! Now let's set up our plugins in our metalsmith.json:
"plugins": {
"metalsmith-collections": {
"posts": {
"pattern": "blog/!(index).md"
}
},
"metalsmith-markdown": {
"gfm": true
},
"metalsmith-layouts": {
"engine": "liquid",
"directory": "layouts",
"includeDir": "layouts/includes"
},
"metalsmith-in-place": {
"engine": "liquid",
"pattern": "**/*.html",
"includeDir": "layouts/includes"
},
"metalsmith-permalinks": {
"pattern": "blog/:title"
}
}
You'll notice we're configuring each plugin with it's respective options. You can learn more about the options at each plugin's Github repo. Here we're collecting posts from our blog directory, rending layouts and source files while allowing us to utilize liquid's includes {% include partial-name %}, and creating permalinks for our posts with the title from our post front-matter.
Now let's set up some layouts we can use for our landing page, post, and blog page:
$ mkdir -p layouts/includes && touch layouts/base.liquid && touch layouts/includes/blog.liquid && touch layouts/post.liquid
<!DOCTYPE html>
<html>
<head>
<title>World Traveler</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css">
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<header class="header">
<div class="navbar">
<a class="navbar-brand" href="/">World Traveler</a>
<ul class="nav navbar-nav">
<li>
<a href="/">Home</a>
</li>
<li>
<a href="/blog">Blog</a>
</li>
</ul>
</div>
</header>
<div class="container">
<div class="row">
<div class="col-md-12">
<div class="content">
{{ contents }}
</div>
</div>
</div>
</div>
</body>
</html>
<div class="posts">
<ul class="posts__list">
{% for post in collections.posts %}
<li class="post">
<a href="/blog/{{ post.title | downcase | split: ' ' | join: '-'}}">{{ post.title }}</a>
</li>
{% endfor %}
</ul>
</div>
Here we're looping over our post collection with collections.posts and including our post partial passing in the post data.
<!DOCTYPE html>
<html>
<head>
<title>World Traveler</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css">
</head>
<body>
<header class="header">
<div class="navbar">
<a class="navbar-brand" href="/">World Traveler</a>
<ul class="nav navbar-nav">
<li>
<a href="/">Home</a>
</li>
<li>
<a href="/blog">Blog</a>
</li>
</ul>
</div>
</header>
<div class="container">
<div class="row">
<div class="col-md-12">
<div class="post">
<h3 class="post-title">{{ title }}</h3>
<p class="post-date">{{ date | date: "%b %d, %Y" }}</p>
<div class="post-content">
{{ contents }}
</div>
</div>
</div>
</div>
</div>
</body>
</html>
Finally, let's create a src directory and add in a markdown for a landing, blog page, and new blog post:
$ mkdir -p src/blog && touch src/index.md && touch src/blog/index.md && touch src/blog/2015-11-05-new-york-visit.md
---
layout: base.liquid
title: World Traveler
permalink: false
---
A site about my travels...
You'll notice here we're setting permalink to false. This tells our permalink plugin not to render this page under blog which we have set up as our permalink path.
---
layout: base.liquid
title: Blog
permalink: false
---
{% include blog %}
---
layout: post.liquid
title: Traveling to New York
date: 2015-11-06
---
This month we'll learn about the delights of New York City...
Now let's add a npm script to run the CLI with npm run build:
{
"scripts": {
"build": "metalsmith"
}
}
Finally we can run:
$ npm run build
Here we have a simple landing page and blog page with a blog post. Let's add some styling with sass.
$ npm i metalsmith-sass --save
"metalsmith-sass": {
"outputDir": "css"
}
$ mkdir src/sass && touch src/sass/style.scss
body {
margin: 0;
font-family: 'Helvetica';
font-weight: 100;
font-size: 18px;
line-height: 1.5;
}
a {
color: #053580;
font-weight: 400;
text-shadow: 0px 0px 0px #fff;
}
.content {
padding: 20px;
}
.navbar {
padding: 20px;
}
And we'll add our link tag: <link rel="stylesheet" href="/css/style.css">
Let's also add a nice header image courtesy of Unsplash
Add the jpeg in src/assets.
.header {
background-image: url("/assets/header.jpeg");
background-size: cover;
min-height: 300px;
background-position: center -95%;
}
Let's run the build again and start a local server.
$ npm run build && serve build
So now that we have a pretty good sense of what Metalsmith can do, let's take a look at how you might run the same build using the API with Babel and finish off our blog by minifying our html. This will make our site nice and fast!
If you aren't already up to speed with Babel 6, go ahead and install the CLI with: npm i -g babel-cli. Then we can set up our .babelrc.
$ touch .babelrc && npm install babel-preset-es2015 --save-dev
{
"presets": ["es2015"]
}
Let's firstly install metalsmith.
$ npm i metalsmith --save
Next, let's create a build.js and write our build script.
$ touch build.js
import Metalsmith from 'metalsmith';
import collections from 'metalsmith-collections';
import layouts from 'metalsmith-layouts';
import inPlace from 'metalsmith-in-place';
import markdown from 'metalsmith-markdown';
import permalinks from 'metalsmith-permalinks';
import sass from 'metalsmith-sass';
Metalsmith(__dirname)
.use(collections({
posts: {
pattern: 'blog/!(index).md',
sortBy: 'date',
reverse: true
}
}))
.use(sass({
outputDir: 'css'
}))
.use(markdown({
gfm: true
}))
.use(permalinks({
pattern: 'blog/:title'
}))
.use(layouts({
engine: 'liquid',
directory: 'layouts',
includeDir: 'layouts/includes'
}))
.use(inPlace({
engine: 'liquid',
pattern: '**/*.html',
includeDir: 'layouts/includes'
}))
.build(function () {
console.log('Cheers!');
});
Great, here we can see Metalsmith uses .use to invoke the plugins in order from top to bottom similar to express middleware, each function gets invoked with all the files from the previous function, processed and passed along. Let's run our build with babel-node:
$ babel-node build.js
Lastly, let's add html minification.
$ npm i metalsmith-html-minifier --save
...
import htmlMinifier from 'metalsmith-html-minifier';
...
.use(htmlMinifier())
.build(function () {
console.log('Cheers!');
})
And now we can adjust our build script.
{
"scripts": {
"build": "babel-node build.js"
}
}
Again, let's run our build.
$ npm run build
As you can see Metalsmith is extremely flexible and has a number of plugins you can choose from to extend your site to fit your needs. There are plugins ranging from tag generators, to s3 upload, to browserify and webpack bundling. You'll definitely have the flexibility and you can also run your builds nicely with ES6 using Babel 6.
From here, you may want to expand the structure of your layouts and src directories to better suite your project. Also, if you're looking for a way to build multiple permalinks, you can use something like metalsmith-branch to break out the permalinks for different paths.
I hope you were able to learn more about Metalsmith and you'll consider using it on your next project! Check out the Demo and Code.