How to Create a Static Site with Metalsmith

Course- Javascript >

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:

  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.

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.