My previous posts discussed reasons why you should or should not consider a static site generator. In summary, a static site generator builds HTML-only page files from templates and raw data typically contained in Markdown files. It offers some of the benefits of a CMS without the hosting, performance and security overheads.
A static site may be appropriate for a range of projects, including:
- A small website or personal blog. Sites with a few dozen pages, infrequent posts and one or two authors could be ideal.
- Technical documentation such as a REST API.
- Application prototypes requiring a series of web page views.
- An eBook — Markdown files can be converted to PDF or other formats as well as HTML.
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.
Frequently Asked Questions about Creating a Static Site with Metalsmith
What is Metalsmith and why should I use it for creating a static site?
Metalsmith is a simple, pluggable static site generator. It’s built on Node.js and uses a modular structure that allows you to add functionality as needed through plugins. This makes it incredibly flexible and customizable. You should use Metalsmith for creating a static site because it allows you to build the site exactly as you want, without the constraints of a traditional CMS. Plus, static sites are faster, more secure, and easier to maintain than dynamic sites.
How do I install Metalsmith?
To install Metalsmith, you need to have Node.js and npm installed on your computer. Once you have these, you can install Metalsmith by running the command npm install metalsmith
in your terminal. This will install Metalsmith and all its dependencies.
How do I create a new Metalsmith project?
To create a new Metalsmith project, first navigate to the directory where you want to create the project in your terminal. Then, run the command metalsmith
to create a new project. This will create a new directory with the name of your project, and inside this directory, it will create a basic structure for your static site.
How do I add plugins to my Metalsmith project?
To add plugins to your Metalsmith project, you need to install them via npm and then require them in your Metalsmith configuration file. For example, to add the markdown plugin, you would first run npm install metalsmith-markdown
, and then in your configuration file, you would add var markdown = require('metalsmith-markdown');
and .use(markdown())
to your Metalsmith build chain.
How do I build my Metalsmith site?
To build your Metalsmith site, you need to run the command metalsmith build
in your terminal. This will compile all your files and output them to a build directory, which you can then deploy to your server.
How do I customize the layout of my Metalsmith site?
To customize the layout of your Metalsmith site, you can use a templating engine like Handlebars or Jade. These allow you to create reusable templates for different parts of your site, like the header, footer, and individual pages.
How do I add content to my Metalsmith site?
To add content to your Metalsmith site, you can create markdown files in your source directory. These files will be converted to HTML when you build your site. You can also use a CMS like Netlify CMS to manage your content.
How do I deploy my Metalsmith site?
To deploy your Metalsmith site, you can use a service like Netlify or GitHub Pages. These services will host your static site and automatically deploy changes when you push to your repository.
How do I update my Metalsmith site?
To update your Metalsmith site, you simply make changes to your source files and then rebuild your site. The changes will be reflected in the build directory, which you can then deploy to your server.
Can I use Metalsmith for large, complex sites?
Yes, Metalsmith is highly scalable and can be used for large, complex sites. Its modular structure allows you to add functionality as needed, and its use of static files means it can handle a large amount of content without slowing down.
Craig is a freelance UK web consultant who built his first page for IE2.0 in 1995. Since that time he's been advocating standards, accessibility, and best-practice HTML5 techniques. He's created enterprise specifications, websites and online applications for companies and organisations including the UK Parliament, the European Parliament, the Department of Energy & Climate Change, Microsoft, and more. He's written more than 1,000 articles for SitePoint and you can find him @craigbuckler.