JavaScript
Article
By Craig Buckler

How to Create a Static Site with Metalsmith

By Craig Buckler

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:

  1. is not aimed at specific project types such as blogs
  2. supports a wide range of template and data format options
  3. is lightweight
  4. has few dependencies
  5. uses a modular structure
  6. offers a simple plug-in architecture, and
  7. 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:

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 order
  • src/html/article — assorted articles in reverse chronological order
  • src/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 to draft, 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:

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 to production, the build 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.

--ADVERTISEMENT--

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 and debug (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 the priority value set in the file’s front-matter.
  • an article collection for every file in the src/html/article directory. It orders them by date in reverse chronological order
  • a page collection for every default page named index.*. It orders them by the priority 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 directory
  • isPage: set true for default section pages named index.*
  • mainCollection: the primary collection name, either start or article
  • layout: if not set, the layout template can be determined from the main collection’s meta data
  • navmain: an array of top-level navigation objects
  • navsub: 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 sitemap
  • metalsmith-feed to generate an RSS feed containing pages in the article collection
  • metalsmith-assets to copy files and directories from src/assets directly to build 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.

  • Hi Craig, what are the advantages of learning how to use a static site generator, when this could be written in html and css? Might be a lower barrier of entry for some devs.. Let me know your thoughts!

    • Craig Buckler

      The main difference is ease of development because you’re automating many of the tedious tasks. Admittedly, setting up Metalsmith/other systems take time but you then have a system which will happily create a full website with permalinks, navigation, RSS etc. by just adding and removing markdown files.

      Manually doing that is fine for a small site with a handful of pages but will become complex as you grow. For example, you’d need to add a new page wherever it has to appear in the navigation – it’s too easy to make errors. In addition, tasks such as concatenation and minification are awkward without a build process.

      A static site isn’t suitable everywhere but I’d recommend it for mid-range sites where a CMS could be overkill.

  • Michael Wuergler

    metalsmith-roothpath is actually a super useful plugin. I use it all the time! Your mention of metalsmith-rootpath made it sound faulty because it’s “incompatible” with metalsmith-permalinks, however, the two plugins solve the exact same problem using two very different (both completely valid) methods, so there is simply no need to ever use them both in the same project. So they aren’t incompatible in the way that “one breaks the other”, they are incompatible in the way of “use one or the other, both work fine.”

    metalsmith-rootpath lets you create a relative navigation without having to rewrite, re-organize or copy/move any of your site assets, which is how I like it (no added bloat or needlessly-duplicated files), so naturally it won’t work if you have a Metalsmith build that wants to completely change the directory structure.

    Anyway, this was a nice tutorial, thanks for taking the time to share your experiences.

    Disclaimer: I am the author of the metalsmith-rootpath plugin. :)

    • Craig Buckler

      Thanks Michael. It wasn’t a criticism since metalsmith-rootpath and metalsmith-permalinks are two separate plugins. However, I don’t agree you wouldn’t want to use one without the other. In my case, I wanted to provide the option for relative or absolute paths depending on the dev/production hosting. For example, you might want relative paths to permalinks because the site is placed in a folder.

      That said, if you don’t intend your plugin to be used with another, I suggest updating the readme.

      I solved the issue with a simple root calculation which worked with or without permalinks (see line 26 of https://github.com/sitepoint-editors/metalsmith-demo/blob/master/lib/metalsmith-moremeta.js – ‘../’.repeat(file.path.split(‘/’).length)).

      • Michael Wuergler

        Ok, I see what you are saying. It’s not that I intended metalsmith-rootpath to not work with other plugins, what I meant was that right now metalsmith-rootpath won’t work on Metalsmith builds that change your “source to target” directory structure, (which some plugins do), but this is already highlighted in the Readme.

        If you would be willing to open an issue on the metalsmith-rootpath repo (https://github.com/radiovisual/metalsmith-rootpath) with a sample of your use case, and an example of your desired behavior, I would be happy to adapt the plugin to work in more build scenarios.

        metalsmith-rootpath has worked great for me, but I want it to be universal enough to fit the needs of other build types.

      • Michael Wuergler

        I have a note on the readme that warns users that metalsmith-rootpath should be run after any plugins or build routines that move or change files.

  • Michael Wuergler

    Just an FYI: metalsmith-rootpath is perfectly compatible with metalsmith-permalinks. You just need to run metalsmith-rootpath AFTER you run permalinks in your build chain, because metalsmith-rootpath needs to know the final directory output structure in order to assign the correct rootpath value. There is a unit test on the metalsmith-rootpath repo to prove that permalinks can work in harmony with the rootpath plugin.

  • Michael Wuergler

    So instead of saying metalsmith-permalinks is incompatible with metalsmith-rootpath, it would be more appropriate to mention the metalsmith-rootpath and metalsmith-permalinks relationship in your “Plugin Order is Critical” section, because metalsmith-rootpath and metalsmith-permalinks are perfectly compatible, it’s just that the plugin order of the two is critical.

  • Javier Rincón Borobia

    Hi Craig.

    Thanks for this big intro to metalsmith.

    One question: For a mid-range site, what is the adventage ssg vs angular/react without backend? It sound me as very similar by the end.

    • Craig Buckler

      A static site is generated to a series of HTML files for hosting. The resulting site is fast and works anywhere.

      An Angular/React site would have all content defined in a single page – JavaScript would switch out one “page” for another when required. Users will have a limited experience or even see nothing if JavaScript is not available or fails in any way.

      I would recommend an SSG unless you require some complex interactions to produce an app-like result and you don’t care about SEO.

Recommended
Sponsors
Get the latest in JavaScript, once a week, for free.