Metalsmith

A pluggable JavaScript static site generator

What is Metalsmith

Jekyll + Gulp.js = Metalsmith
    Metalsmith(__dirname)
        .use(markdown())
        .use(sass())
        // ...
        .destination('./build')
        .build();

Why is this great?

  • super easy to use (API or JSON)
  • easy to write plugins for
  • verbose API
  • use whatever template engine you like (e. g. Handlebars, Jade)
  • written in your favourite programming language ;)

Great, what do I have to do?

$ npm install -D metalsmith
.
|– src/
    |– content/
    |– styles/
    |_ scripts/
|– templates/
|_ build.js

Show me code!

var Metalsmith = require('metalsmith');

Metalsmith(__dirname)
    .destination('./build')
    .build();

This will move everything from __dirname/src to __dirname/build

Simple enough.
(but kind of useless)

Make it useful!

src/index.md

---
title: Home
---
##My Page

This is some content!
$ npm install -D metalsmith-markdown
var Metalsmith = require('metalsmith'),
    markdown   = require('metalsmith-markdown');

Metalsmith(__dirname)
    .use(markdown())
    .destination('./build')
    .build();
$ node build.js
build/index.html

Make It Beautiful!

templates/index.hbt

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <title>{{ title }} | Metalsmith Page</title>
    <link rel="stylesheet" 
        href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css">
</head>
<body>
    <div class="main-wrapper">
        {{{ contents }}}
    </div>

</body>
</html>
---
title: Home
template: index.hbt
---
$ npm install -D metalsmith-templates handlebars

(uses consolidate.js)

build.js

// ...
    templates = require('metalsmith-templates');

Metalsmith(__dirname)
    .use(markdown())
    .use(templates('handlebars'))
// ...

Content types

.
|_ src/
    |_ contents/
        |– posts/
            |_ my-frist-post.md
        |_ pages/
            |_ about.md

src/contents/posts/my-frist-post.md

---
title: My First Post
template: post.hbt
date: 2014-05-19
---
#My First Post

Hey, I'm blogging!

src/contents/pages/about.md

---
title: About
template: page.hbt
---
##About

Awesome, witty content here!
build/contents/pages/about.html build/contents/posts/my-first-post.html

Collections

$ npm install -D metalsmith-collections
build.js

    // ...
    .use(collections({
        blog: {
            pattern: '*/posts/*',
            oderBy: 'date',
            reverse: true
        },
        pages: {
            pattern: '*/pages/*'
        }
    }))
    // ...

templates/home.hbt

<!-- ... -->
<div class="main-wrapper">
    {{#each collections.blog}}
        <article class="post">
            <h3>{{title}}</h3>
            <p>{{{contents}}}</p>
        </article>
    {{/each}}
</div>
<!-- ... -->
$ npm install -D metalsmith-permalinks
build.js

    // ...
    .use(permalinks({
        pattern: ':collection/:title'
    }))
    // ...
build/pages/about/index.html
build/blog/my-first-post/index.html

Writing Plugins

Internal Structure

{
    'path/to/file': {
        'title': 'FROM_THE_TITLE_KEY',
        'template': 'TEMPLATE_NAME',
        'contents': <Buffer()>,
        'mode': 'HEX_FILE_PERM_CODE'
    }
}

Error Handling

.build(function(err) {
    if (err) { throw err; }
});
var plugin = function(opts) {
    return function(files, metalsmith, done) {
        done();      
    };
};
var autoTemplate = function(opts) {
    var pattern = new RegExp(opts.pattern);

    return function(files, metalsmith, done) {
        for (var file in files) {
            if (pattern.test(file)) {
                var _f = files[file];
                if (!_f.template) {
                    _f.template = opts.templateName;
                }
            }
        }
        done();
    };
};
    //...
    .use(autoTemplate({
        pattern: 'posts',
        templateName: 'post.hbt'
    }))
    //...
var metadata    = metalsmith.metadata(),
    collections = metadata.collections;

Preprocessing

$ npm install -D metalsmith-sass metalsmith-coffee
// ...
    .use(sass({
        outputStyle: 'compressed'
    }))
    .use(coffee())
// ...
All other static files (images, fonts, etc.) are just copied over (including folder structure)
Metalsmith(__dirname)
    .use(collections({
        blog: {
            pattern: '*/posts/*',
            oderBy: 'date',
            revers: true
        },
        pages: {
            pattern: '*/pages/*'
        }
    }))
    .use(markdown())
    .use(autoTemplate({
        pattern: 'posts/',
        templateName: 'post.hbt'
    }))
    .use(permalinks({
        pattern: ':collection/:title'
    }))
    .use(templates('handlebars'))
    .destination('./build')
    .build(function(err) {
        if (err) { throw err; }
    });
{
    "source": "src",
    "destination": "build",
    "plugins": {
        "metalsmith-collections": {
            "blog": {
                "pattern": "*/posts/*",
                "oderBy": "date",
                "revers": true
            },
            "pages": {
                "pattern": "*/pages/*"
            }
        },
        "metalsmith-markdown": true,
        "metalsmith-autoTemplate": {
            "pattern": "posts/",
            "templateName": "post.hbt"
        },
        "metalsmith-permalinks": ":collection/:title",
        "metalsmith-templates": "handlebars"
    }
}
var clone = require('clone');
var defaults = require('defaults');
var each = require('async').each;
var front = require('front-matter');
var fs = require('fs-extra');
var Mode = require('stat-mode');
var noop = function(){};
var path = require('path');
var readdir = require('recursive-readdir');
var rm = require('rimraf').sync;
var utf8 = require('is-utf8');
var Ware = require('ware');

/**
 * Expose `Metalsmith`.
 */

module.exports = Metalsmith;

/**
 * Initialize a new `Metalsmith` builder with a working `dir`.
 *
 * @param {String} dir
 */

function Metalsmith(dir){
  if (!(this instanceof Metalsmith)) return new Metalsmith(dir);
  this.dir = path.resolve(dir);
  this.ware = new Ware();
  this.data = {};
  this.source('src');
  this.destination('build');
  this.clean(true);
}

/**
 * Add a `plugin` to the middleware stack.
 *
 * @param {Function} plugin
 * @return {Metalsmith}
 */

Metalsmith.prototype.use = function(plugin){
  this.ware.use(plugin);
  return this;
};

/**
 * Get or set the global `metadata` to pass to templates.
 *
 * @param {Object} metadata
 * @return {Object or Metalsmith}
 */

Metalsmith.prototype.metadata = function(metadata){
  if (!arguments.length) return this.data;
  this.data = clone(metadata);
  return this;
};

/**
 * Get or set the source directory.
 *
 * @param {String} path
 * @return {String or Metalsmith}
 */

Metalsmith.prototype.source = function(path){
  if (!arguments.length) return this.join(this._src);
  this._src = path;
  return this;
};

/**
 * Get or set the destination directory.
 *
 * @param {String} path
 * @return {String or Metalsmith}
 */

Metalsmith.prototype.destination = function(path){
  if (!arguments.length) return this.join(this._dest);
  this._dest = path;
  return this;
};

/**
 * Get or set whether the destination directory will be removed before writing.
 * @param  {Boolean} clean
 * @return {Boolean or Metalsmith}
 */
Metalsmith.prototype.clean = function(clean){
  if (!arguments.length) return this._clean;
  this._clean = clean;
  return this;
};

/**
 * Join path `strs` with the working directory.
 *
 * @param {String} strs...
 * @return {String}
 */

Metalsmith.prototype.join = function(){
  var strs = [].slice.call(arguments);
  strs.unshift(this.dir);
  return path.join.apply(path, strs);
};

/**
 * Build with the current settings to the dest directory.
 *
 * @param {Function} fn
 */

Metalsmith.prototype.build = function(fn){
  fn = fn || noop;
  var self = this;

  this.read(function(err, files){
    if (err) return fn(err);
    self.run(files, function(err, files){
      if (err) return fn(err);
      self.write(files, function(err){
        fn(err, files);
      });
    });
  });
};

/**
 * Run a set of `files` through the middleware stack.
 *
 * @param {Object} files
 * @param {Function} fn
 */

Metalsmith.prototype.run = function(files, fn){
  this.ware.run(files, this, fn);
};

/**
 * Read the source directory, parsing front matter and call `fn(files)`.
 *
 * @param {Function} fn
 * @api private
 */

Metalsmith.prototype.read = function(fn){
  var files = {};
  var src = this.source();

  readdir(src, function(err, arr){
    if (err) return fn(err);
    each(arr, read, function(err){
      fn(err, files);
    });
  });

  function read(file, done){
    var name = path.relative(src, file);
    fs.stat(file, function(err, stats){
      if (err) return done(err);
      fs.readFile(file, function(err, buffer){
        if (err) return done(err);
        var file = {};

        if (utf8(buffer)) {
          var parsed = front(buffer.toString());
          file = parsed.attributes;
          file.contents = new Buffer(parsed.body);
        } else {
          file.contents = buffer;
        }

        file.mode = Mode(stats).toOctal();
        files[name] = file;
        done();
      });
    });
  }
};

/**
 * Write a dictionary of `files` to the dest directory.
 *
 * @param {Object} files
 * @param {Function} fn
 * @api private
 */

Metalsmith.prototype.write = function(files, fn){
  var dest = this.destination();
  var clean = this.clean();

  if (clean) rm(dest);
  each(Object.keys(files), write, fn);

  function write(file, done){
    var data = files[file];
    var out = path.join(dest, file);
    return fs.outputFile(out, data.contents, function(err){
      if (err) done(err);
      if (!data.mode) return done();
      fs.chmod(out, data.mode, done);
    });
  }
};

Metalsmith.io

.build()

Thanks!

Questions?