How to Create a Static Site with Metalsmith
A static site may be appropriate for a range of projects, including:
In essence, a static site generator is a build tool. You could use one for running tasks or project scaffolding like you could with Grunt or Gulp.
Why Metalsmith?
The undisputed static site champion is Jekyll—a Ruby project launched in 2008. You don’t necessarily require Ruby expertise to use Jekyll but it will help. Fortunately, there is a wide range of open source static site generators for most popular languages. JavaScript options include Hexo, Harp and Assemble. You could also use a build tool such as Gulp for simpler projects.
I choose Metalsmith for this tutorial because it:
- is not aimed at specific project types such as blogs
- supports a wide range of template and data format options
- is lightweight
- has few dependencies
- uses a modular structure
- offers a simple plug-in architecture, and
- is easy to get started.
A demonstration website has been built for this tutorial. It won’t win any design awards but it illustrates the basic concepts. The Metalsmith build code can be examined and installed from the GitHub repository. Alternatively, you can follow the instructions here and create your own basic site.
I have used Metalsmith a couple of times—please don’t presume this is the definitive way to build every static site!
Install Metalsmith
Ensure you have Node.js installed (for example using nvm) then create a new project directory, e.g. project
and initialize your package.json
file:
cd project && cd project
npm init -y
Now install Metalsmith and the assorted plugins we’ll use to build our site. These are:
- metalsmith-assets — includes static assets in your Metalsmith build
- metalsmith-browser-sync — incorporates BrowserSync into your workflow
- metalsmith-collections — adds collections of files to the global metadata
- metalsmith-feed — generates an RSS feed for a collection
- metalsmith-html-minifier — minifies HTML files using kangax/html-minifier
- metalsmith-in-place — renders templating syntax in source files
- metalsmith-layouts — applies layouts to your source files
- metalsmith-mapsite — generates a sitemap.xml file
- metalsmith-markdown — converts markdown files
- metalsmith-permalinks — applies a custom permalink pattern to files
- metalsmith-publish — adds support for draft, private, and future-dated posts
- metalsmith-word-count — computes word count / average reading time of all paragraphs in a HTML file
npm install --save-dev metalsmith metalsmith-assets metalsmith-browser-sync metalsmith-collections metalsmith-feed metalsmith-html-minifier metalsmith-in-place metalsmith-layouts metalsmith-mapsite metalsmith-markdown metalsmith-permalinks metalsmith-publish metalsmith-word-count handlebars
Project Structure
We’ll use the following structure for source (src
) and build (build
) directories within the project.
You can create your example files as described below or copy them directly from the demonstration src directory.
Pages
Page Markdown files are contained in src/html
. This can contain one level of sub-directories for each website section, i.e.
src/html/start
— pages describing the project in a specific ordersrc/html/article
— assorted articles in reverse chronological ordersrc/html/contact
— a single contact page
Each directory contains a single index.md
file which is the default page for that section. Other pages can use any unique name.
The build process will transform these files into directory-based permalinks, e.g.
src/html/start/index.md
becomes/start/index.html
src/html/start/installation.md
becomes/start/installation/index.html
Each Markdown file provides the content and meta information known as “front-matter” at the top between ---
markers, e.g.
---
title: My page title
description: A description of this page.
layout: page.html
priority: 0.9
date: 2016-04-19
publish: draft
---
This is a demonstration page.
## Example title
Body text.
Most front-matter is optional but you can set:
priority
: a number between 0 (low) and 1 (high) which we’ll use to order menus and define XML sitemaps.publish
: can be set todraft
,private
or a future date to ensure it is not published until required.date
: the date of the article. If none is set, we’ll use any future publish date or the file creation date.layout
: the HTML template to use.
Templates
HTML page templates are contained in src/template
. Two templates have been defined:
src/html/template/page.html
the default layoutsrc/html/template/article.md
an article layout showing dates, next/back links, etc.
The Handlebars templating system is used although alternative options are supported. A typical template requires a {{{ contents }}}
tag to include the page content as well as any front-matter values such as {{ title }}
:
<!DOCTYPE html>
<html lang="en">
<head>
{{> meta }}
</head>
<body>
{{> header }}
<main>
<article>
{{#if title}}
<h1>{{ title }}</h1>
{{/if}}
{{{ contents }}}
</article>
</main>
{{> footer }}
</body>
</html>
References to {{> meta }}
, {{> header }}
and {{> footer }}
are partials…
Partials
Partials—or HTML snippet files—are contained within src/partials
. These are mostly used within templates but can also be included within content pages using the code:
{{> partialname }}
where partialname
is the name of the file in the src/partials
directory.
Static Assets
Static assets such as images, CSS and JavaScript files are contained in src/assets
. All files and sub-directories will copied to the root of the website as-is.
Custom Plugins
Custom plugins required to build the site are contained in the lib
directory.
Build Directory
The website will be built in the build
directory. We will build the site in two ways:
- Development mode: HTML will not be minified and a test web server will be started.
- Production mode: if
NODE_ENV
is set toproduction
, thebuild
directory is wiped and final minified files are generated.
Defining Your First Build File
A basic example named build.js
can be created in the root of your project directory:
// basic build
'use strict';
var
metalsmith = require('metalsmith'),
markdown = require('metalsmith-markdown'),
ms = metalsmith(__dirname) // the working directory
.clean(true) // clean the build directory
.source('src/html/') // the page source directory
.destination('build/') // the destination directory
.use(markdown()) // convert markdown to HTML
.build(function(err) { // build the site
if (err) throw err; // and throw errors
});
Run this using node ./build.js
and a static site will be created in the build
directory. The Markdown will be parsed into HTML but it won’t be usable because we haven’t included templates in our build process.
Metalsmith Plugins
Superficially, Metalsmith build files look similar to those used in Gulp (although it doesn’t use streams). A plugin is invoked by passing it to the Metalsmith use
method with any appropriate arguments. The plugin itself must return another function which accepts three parameters:
- a
files
array containing information about every page - a
metalsmith
object containing global information such as meta data, and - a
done
function which must be called when the plugin has finished working
This simple example logs all meta and page information to the console (it can be defined in build.js
):
function debug(logToConsole) {
return function(files, metalsmith, done) {
if (logToConsole) {
console.log('\nMETADATA:');
console.log(metalsmith.metadata());
for (var f in files) {
console.log('\nFILE:');
console.log(files[f]);
}
}
done();
};
};
The Metalsmith build code can be updated to use this plugin:
ms = metalsmith(__dirname) // the working directory
.clean(true) // clean the build directory
.source('src/html/') // the page source directory
.destination('build/') // the destination directory
.use(markdown()) // convert Markdown to HTML
.use(debug(true)) // *** NEW *** output debug information
.build(function(err) { // build the site
if (err) throw err; // and throw errors
});
This debugging function may help you create your own custom plugins but most of the functionality you could ever require has already been written—there’s a long list of plugins on the Metalsmith website.
Making a Better Build
Key parts of the demonstration site build file are explained below.
A variable named devBuild
is set true
if the NODE_ENV
environment variable has been set to production
(export NODE_ENV=production
on Mac/Linux or set NODE_ENV=production
on Windows):
devBuild = ((process.env.NODE_ENV || '').trim().toLowerCase() !== 'production')
The main directories are defined in a dir
object so we can reuse them:
dir = {
base: __dirname + '/',
lib: __dirname + '/lib/',
source: './src/',
dest: './build/'
}
The Metalsmith and plugin modules are loaded. Note:
- the excellent Browsersync test server is only required when creating a development build
- the HTML minifier module referenced by
htmlmin
is only required when creating a production build - three custom plugins have been defined:
setdate
,moremeta
anddebug
(explained in more detail below)
metalsmith = require('metalsmith'),
markdown = require('metalsmith-markdown'),
publish = require('metalsmith-publish'),
wordcount = require("metalsmith-word-count"),
collections = require('metalsmith-collections'),
permalinks = require('metalsmith-permalinks'),
inplace = require('metalsmith-in-place'),
layouts = require('metalsmith-layouts'),
sitemap = require('metalsmith-mapsite'),
rssfeed = require('metalsmith-feed'),
assets = require('metalsmith-assets'),
htmlmin = devBuild ? null : require('metalsmith-html-minifier'),
browsersync = devBuild ? require('metalsmith-browser-sync') : null,
// custom plugins
setdate = require(dir.lib + 'metalsmith-setdate'),
moremeta = require(dir.lib + 'metalsmith-moremeta'),
debug = consoleLog ? require(dir.lib + 'metalsmith-debug') : null,
A siteMeta
object is defined with information which applies to every page. The important values are domain
and rootpath
which are set according to the development or production build:
siteMeta = {
devBuild: devBuild,
version: pkg.version,
name: 'Static site',
desc: 'A demonstration static site built using Metalsmith',
author: 'Craig Buckler',
contact: 'https://twitter.com/craigbuckler',
domain: devBuild ? 'http://127.0.0.1' : 'https://rawgit.com', // set domain
rootpath: devBuild ? null : '/sitepoint-editors/metalsmith-demo/master/build/' // set absolute path (null for relative)
}
A templateConfig
object has also been defined to set template defaults. This will be used by both the metalsmith-in-place
and metalsmith-layouts
plugins which enable in-page and template rendering using Handlebars:
templateConfig = {
engine: 'handlebars',
directory: dir.source + 'template/',
partials: dir.source + 'partials/',
default: 'page.html'
}
The Metalsmith object is now initiated as before but we also pass our siteMeta
object to the metadata
method to ensure that information is available on every page. Therefore, we can reference items such as {{ name }}
in any page to get the site name.
var ms = metalsmith(dir.base)
.clean(!devBuild) // clean build before a production build
.source(dir.source + 'html/') // source directory (src/html/)
.destination(dir.dest) // build directory (build/)
.metadata(siteMeta) // add meta data to every page
Our first plugin invocation calls metalsmith-publish
which removes any file which has its front-matter publish
value set to draft
, private
or a future date:
.use(publish()) // draft, private, future-dated
setdate
is a custom plugin contained in lib/metalsmith-setdate.js. It ensures every file has a ‘date’ value set even if none has been defined in front-matter by falling back to the publish
date or the file creation time where possible:
.use(setdate()) // set date on every page if not set in front-matter
metalsmith-collections
is one of the most important plugins since it allocates each page to a category or taxonomy based on its location in the source directory or other factors. It can re-order files using front-matter such as date
or priority
and allows you to set custom meta data for that collection. The code defines:
- a start collection for every file in the
src/html/start
directory. It orders them by thepriority
value set in the file’s front-matter. - an article collection for every file in the
src/html/article
directory. It orders them bydate
in reverse chronological order - a page collection for every default page named
index.*
. It orders them by thepriority
value set in the file’s front-matter.
.use(collections({ // determine page collection/taxonomy
page: {
pattern: '**/index.*',
sortBy: 'priority',
reverse: true,
refer: false
},
start: {
pattern: 'start/**/*',
sortBy: 'priority',
reverse: true,
refer: true,
metadata: {
layout: 'article.html'
}
},
article: {
pattern: 'article/**/*',
sortBy: 'date',
reverse: true,
refer: true,
limit: 50,
metadata: {
layout: 'article.html'
}
}
}))
Next comes Markdown to HTML conversion followed by the metalsmith-permalinks
plugin which defines a directory structure for the build. Note that :mainCollection
is set for each file by moremeta
below:
.use(markdown()) // convert Markdown
.use(permalinks({ // generate permalinks
pattern: ':mainCollection/:title'
}))
metalsmith-word-count
counts the number of words in an article and calculates approximately how long it takes to read. The argument { raw: true }
outputs the numbers only:
.use(wordcount({ raw: true })) // word count
moremeta
is another custom plugin contained in lib/metalsmith-moremeta.js. It appends additional metadata to each file:
root
: an absolute or calculated relative file path to the root directoryisPage
: set true for default section pages namedindex.*
mainCollection
: the primary collection name, eitherstart
orarticle
layout
: if not set, the layout template can be determined from the main collection’s meta datanavmain
: an array of top-level navigation objectsnavsub
: an array of secondary-level navigation objects
The plugin code is relatively complex because it handles the navigation. There are easier options should you require a simpler hierarchy.
.use(moremeta()) // determine root paths and navigation
The metalsmith-in-place
and metalsmith-layouts
plugins control in-page and template layouts respectively. The same templateConfig
object defined above is passed:
.use(inplace(templateConfig)) // in-page templating
.use(layouts(templateConfig)); // layout templating
If htmlmin
is set (in a production build), we can minify the HTML:
if (htmlmin) ms.use(htmlmin()); // minify production HTML
debug
is our final custom plugin contained in lib/metalsmith-debug.js. It is similar to the debug
function described above:
if (debug) ms.use(debug()); // output page debugging information
The Browsersync test server is started so we can test development builds. If you’ve not used it before it’ll seem like magic: your site will magically refresh every time you make a change and views in two or more browsers are synchronised as you scroll or navigate around the site:
if (browsersync) ms.use(browsersync({ // start test server
server: dir.dest,
files: [dir.source + '**/*']
}));
Finally, we can use:
metalsmith-mapsite
to generate an XML sitemapmetalsmith-feed
to generate an RSS feed containing pages in the article collectionmetalsmith-assets
to copy files and directories fromsrc/assets
directly tobuild
without modification.
ms
.use(sitemap({ // generate sitemap.xml
hostname: siteMeta.domain + (siteMeta.rootpath || ''),
omitIndex: true
}))
.use(rssfeed({ // generate RSS feed for articles
collection: 'article',
site_url: siteMeta.domain + (siteMeta.rootpath || ''),
title: siteMeta.name,
description: siteMeta.desc
}))
.use(assets({ // copy assets: CSS, images etc.
source: dir.source + 'assets/',
destination: './'
}))
All that remains is the final .build()
step to create the site:
.build(function(err) { // build
if (err) throw err;
});
Once complete, you can run node ./build.js
to build your static site again.
The Gotchas
I learned a lot building a simple Metalsmith website but be aware of the following issues:
Incompatible Plugins
Plugins can clash with others. For example, metalsmith-rootpath which calculates relative root paths does not play nicely with metalsmith-permalinks which creates custom build directory structures. I solved this issue by writing custom root
path calculation code in the lib/metalsmith-moremeta.js plugin.
Plugin Order is Critical
Plugins can depend on each other or conflict if placed in the wrong order. For example, the RSS-generating metalsmith-feed plugin must be called after metalsmith-layouts to ensure RSS XML is not generated within a page template.
Browsersync Re-Build Issues
When Browsersync is running and files are edited, collections are re-parsed but the old data appears to remain. It’s possibly an issue with the custom lib/metalsmith-moremeta.js plugin but menus and next/back links to be thrown out of synchronization. To fix it, stop the build with Ctrl/Cmd + C and restart the build.
Do You Still Need Gulp?
Those using a task manager such as Gulp will notice Metalsmith offers a familiar build process. There are plugins for CSS pre-processing with Sass, image minification, file concatenation, uglification and more. It may be enough for simpler workflows.
However, Gulp has a more extensive range of plugins and permits complex build activities such as linting, deployment and PostCSS processing with auto-prefixer. There are a couple of Gulp/Metalsmith integration plugins although I experienced several issues and they should not be necessary because a Gulp task can run Metalsmith directly, e.g.
var
gulp = require('gulp'),
metalsmith = require('metalsmith'),
publish = require('metalsmith-publish'),
markdown = require('metalsmith-markdown');
// build HTML files using Metalsmith
gulp.task('html', function() {
var ms = metalsmith(dir.base)
.clean(false)
.source('src/html/')
.destination('build')
.use(publish())
.use(markdown())
.build(function(err) {
if (err) throw err;
});
});
This process prevents the Browsersync re-build issues mentioned above. Remember to use .clean(false)
to ensure Metalsmith never wipes the build folder when other tasks are active.
Is Metalsmith for You?
Metalsmith is ideal if you have simple or highly-customized website requirements. Perhaps try it with a documentation project and add features one at a time. Metalsmith is not as feature-complete as alternatives such as Jekyll but it’s not intended to be. You may well have to write your own plugins but the ease of doing that is a huge benefit to JavaScript developers.
Creating a Metalsmith build system takes time and we haven’t considered the effort involved in HTML templating and deployment. However, once you have a working process, it becomes remarkably simple to add, edit and remove Markdown files. It can be easier than using a CMS and you have all the benefits of a static site.