Usage guide

← Docs overview

The Metalsmith directory

The Metalsmith constructor takes a working directory as single argument (in 99% of cases the parent directory of metalsmith.js). With ES modules there are 2 extra lines of code.

metalsmith.mjs
import { dirname } from 'path'
import { fileURLToPath } from 'url'
import Metalsmith from 'metalsmith'

const __dirname =  dirname(fileURLToPath(import.meta.url))

Metalsmith(__dirname)
metalsmith.cjs
const Metalsmith = require('metalsmith')

Metalsmith(__dirname)

If you are using the CLI with a metalsmith.json config file, there is no need to specify metalsmith.directory explicitly, it will default to __dirname.

Using plugins

A metalsmith plugin is just a function that is passed the Files object and the Metalsmith instance. In fact, we can even use console.log as a plugin!

metalsmith.js
const Metalsmith = require('metalsmith')

Metalsmith(__dirname)
  .use(console.log)
  .build((err, files) => {
    if (err) throw err
    console.log('Build success!')
  })
metalsmith.json
{
  "plugins": [
    {"./plugin-console-log": true}
  ]
}

*Note that for this example to work if you are using the CLI, you need to create a file plugin-console-log.js with the contents module.exports = (...args) => console.log(...args) and reference it as a local plugin in metalsmith.json

This is super-convenient when you want to quickly have a look at all the files and the metalsmith instance. You could use this little plugin to inspect how the file metadata and the metalsmith instance change in-between plugins:

metalsmith.js
const Metalsmith = require('metalsmith')
const drafts = require('@metalsmith/drafts')
const markdown = require('@metalsmith/markdown')
const layouts = require('@metalsmith/layouts')

Metalsmith(__dirname)
  .use(console.log)
  .use(drafts())
  .use(console.log)
  .use(markdown())
  .use(console.log)
  .use(layouts())
  .use(console.log)
  .build((err, files) => {
    if (err) throw err
    console.log('Build success!')
  })
metalsmith.json
{
  "plugins": [
    {"./plugin-console-log": true},
    {"@metalsmith/drafts": {}},
    {"./plugin-console-log": true},
    {"@metalsmith/markdown": {}},
    {"./plugin-console-log": true},
    {"@metalsmith/layouts": { "pattern": "**/*.html" }},
    {"./plugin-console-log": true},
  ]
}

This example also demonstrates that you can re-use the same plugin multiple times across the plugin chain, each time with different input.

Logging is cool, but what about actually manipulating the files? Say, defining additional metadata, rendering markdown files, wrapping them in a layout, adding SASS stylesheets, and optimizing everything for production. Just as the Apple iPhone's famous 2009 commercial "There's an app for that", the answer to how can I do X with Metalsmith? is - there's a plugin for that. Browse the official plugin registry for inspiration!

Plugin types

Plugins can be broadly divided into a few categories:esbuildesbuild

A plugin could fit into multiple categories:

Plugins that start with the @metalsmith/ prefix are core plugins. They are officially supported by Metalsmith and there's a good chance that you will need most of them when building a static site. Here are some of the most common ones:

Plugin order

Plugin order is very important in Metalsmith. As a rule of thumb, .use(common sense): you only want to minify HTML after the markdown file has been processed with @metalsmith/markdown and then wrapped in @metalsmith/layouts. Generally, you want plugins that inject new files or add metadata to be run at the start of the plugin chain so it is available in layouts and for other plugins to process. @metalsmith/drafts is efficient as the first plugin because in a production build it immediately removes the files you don't want to process anyway.

Conditionally running plugins

If you're using the Metalsmith CLI, there's only one way to run plugins conditionally: create multiple metalsmith.json configs. The common use case is having a development config and a production config. For example, we would like to remove draft files and minify the HTML only in production:

metalsmith.dev.json
{
  "plugins": [
    { "@metalsmith/markdown": { } },
    { "@metalsmith/layouts": { } }
  ]
}
metalsmith.json
{
  "plugins": [
    { "@metalsmith/drafts": { } },
    { "@metalsmith/markdown": { } },
    { "@metalsmith/layouts": { } },
    { "metalsmith-html-minifier": { } }
  ]
}

And run it with:

metalsmith --config metalsmith.dev.json
metalsmith --config metalsmith.json

If you have more than 2-3 conditions we recommend using the JS API. You can run a plugin conditionally by assigning the metalsmith build to a variable, and using native javascript if statements. The same example from above using the JS API:

metalsmith.js
const Metalsmith = require('metalsmith');
const minifyHTML = require('metalsmith-html-minifier');
const isProduction = process.env.NODE_ENV !== 'development';

const metalsmith = Metalsmith(__dirname);
if (isProduction) {
  metalsmith.use(minifyHTML());
}
metalsmith.build(err => {
  if (err) throw err
  console.log('Build success!')
})

If you need to check multiple conditions at different places in the build, the metalsmith-if plugin might be a better match:

metalsmith.js
const when = require('metalsmith-if');
const minifyHTML = require('metalsmith-html-minifier');
const production = process.env.NODE_ENV !== 'development';

Metalsmith(__dirname)
  .use(when(production, minifyHTML())
  .build(err => {
    if (err) throw err
    console.log('Build success!')
  })

Defining metadata

You can define global metadata for a Metalsmith build using the metalsmith.metadata method:

metalsmith.js
const Metalsmith = require('metalsmith')

Metalsmith(__dirname)
  .metadata({
    sitename: 'My Static Site & Blog',
    description: 'It\'s about saying »Hello« to the World.',
    generator: 'Metalsmith',
    url: 'https://metalsmith.io/'
  })
metalsmith.json
{
  "metadata": {
    "sitename": "My Static Site & Blog",
    "description": "It's about saying »Hello« to the World.",
    "generator": "Metalsmith",
    "url": "https://metalsmith.io/"
  }
}

Global metadata can be dynamically added from files in metalsmith.directory or metalsmith.source with a plugin like @metalsmith/metadata, which can help keep your main build file clean. Here is the same metadata in a separate yaml file:

src/site.yaml
sitename: My Static Site & Blog
description: It's about saying »Hello« to the World.
generator: Metalsmith
url: 'https://metalsmith.io/'

...that we can then refer to in the build like:

metalsmith.js
const Metalsmith = require('metalsmith')
const metadata = require('@metalsmith/metadata')

Metalsmith(__dirname)
  .use(metadata({
    site: 'src/site.yaml'
  }))
metalsmith.json
{
  "plugins": [
    { "@metalsmith/metadata": { "site": "src/site.yaml" } }
  ]
}

File metadata can be defined as front-matter in any file (provided that you didn't disable it with metalsmith.frontmatter(false)):

---
title: My first post went a little like this
description: A turn, an ending, and a twist
---
No more drafts and no more waiting

File metadata can be added dynamicaly with plugins like @metalsmith/default-values or metalsmith-filemetadata. Below is an example using @metalsmith/default-values to automatically assign the post.hbs layout to files by folder and mark all files in the drafts folder as draft:

metalsmith.js
const Metalsmith = require('metalsmith')
const defaultValues = require('@metalsmith/default-values')
const drafts = require('@metalsmith/drafts')
const markdown = require('@metalsmith/markdown')
const layouts = require('@metalsmith/layouts')

Metalsmith(__dirname)
  .use(defaultValues([
    {
      pattern: 'posts/**/*.md',
      defaults: {
        layout: 'post.hbs'
      }
    },
    {
      pattern: 'drafts/**',
      defaults: { draft: true }
    }
  ]))
  .use(process.env.NODE_ENV === 'production' ? drafts() : () => {})
  .use(markdown())
  .use(layouts())
  .build((err, files) => {
    if (err) throw err
    console.log('Build success')
  })
metalsmith.dev.json
{
  "plugins": [
    {
      "@metalsmith/default-values": [
        { "pattern": "posts/**/*.md", defaults: { "layout": "post.hbs" }},
        { "pattern": "drafts/**", defaults: { "draft": true }},
      ] 
    },
    { "@metalsmith/drafts": {} },
    { "@metalsmith/markdown": {} },
    { "@metalsmith/layouts": {} }
  ]
}

Rendering content

There are a multitude of plugins which can be used to render content. For rendering markdown contents and file metadata keys, there is @metalsmith/markdown. @metalsmith/layouts combined with a jstransformer wraps content in layouts, and @metalsmith/in-place is useful if you need to use a templating language within a file's contents (for example within markdown files).

There are also other rendering plugins like metalsmith-twig or metalsmith-handlebars-x that provide full integrations for specific templating languages.

Using the Metalsmith environment

Since version 2.5.0, Metalsmith has its own Metalsmith.env method. Metalsmith plugins can read and use conventional variables to set more sensible defaults. A few notable conventions are NODE_ENV, DEBUG and TZ:

metalsmith.js
const Metalsmith = require('metalsmith')
Metalsmith(__dirname)
  .env({
    DEBUG: true,
    NODE_ENV: 'development',
    TZ: 'Europe/London',
  })
metalsmith.json
{
  "env": {
    "DEBUG": true,
    "NODE_ENV": "development",
    "TZ": "Europe/London",
  }
}

The @metalsmith/sass plugin for example will output source maps and skip minifying the resulting CSS if metalsmith.env('NODE_ENV') === 'development' (to minimize build time and maximize ability to debug).

Debugging

Most Metalsmith plugins use the debug package for logging and debugging. You can enable targeted or global debugging to get a better idea of what your metalsmith plugin chain is doing. Since version 2.5.0, debugging in metalsmith can be enabled by passing a debug value to metalsmith.env like so:

metalsmith.js
const Metalsmith = require('metalsmith')
Metalalsmith(__dirname)
  .env('DEBUG', true)
metalsmith.dev.json
{
  "env": {
    "DEBUG": true
  }
}

The previous example sets DEBUG: true which is the same as the globstar wildcard *, meaning debug all. If you wanted to debug a specific plugin, say @metalsmith/markdown, you would set metalsmith.env('DEBUG', '@metalsmith/markdown*').

Using the DEBUG environment variable

Older plugins released prior to Metalsmith 2.5.0 often use the debug package directly: these can only be controlled by the DEBUG (operating system-level) environment variable. To get those logs to conform the best solution is to pass process.env.DEBUG to metalsmith:

metalsmith.js
const Metalsmith = require('metalsmith')
Metalalsmith(__dirname)
  .env('DEBUG', process.env.DEBUG)
metalsmith.json
{
  "env": {
    "DEBUG": "$DEBUG"
  }
}

You can choose the DEBUG value every time you run a metalsmith build, for example:

DEBUG=* node metalsmith.js
set DEBUG=* node metalsmith.js

To avoid having to mess with Mac/Linux vs Windows syntax, use the cross-env NPM package: cross-env DEBUG=* node metalsmith.js

Debug values

The list below shows the different types of values you could choose to pass to debug:

Storing debug logs in a file

You can choose to store debug logs in a file instead of logging them to the console by specifying metalsmith.env('DEBUG_LOG', 'path/relative/to/ms/dir.log'). Note that this will affect only plugins using metalsmith.debug.

metalsmith.js
const Metalsmith = require('metalsmith')
Metalalsmith(__dirname)
  .env('DEBUG', process.env.DEBUG)
  .env('DEBUG_LOG', 'metalsmith.log')
metalsmith.json
{
  "env": {
    "DEBUG": "$DEBUG",
    "DEBUG_LOG": "metalsmith.log"
  }
}

The log can only be output either to console, or a log file. Therefore enabling DEBUG_LOG is more suitable for server environments with file system persistence, or if you want to git version the build log or store it as a CI artifact.

Adding your own debug logs

You can use metalsmith.debug for your own build logs as well. The method returns a debugger with 3 channels with their own colors: info (cyan), warn (orange), and error (red). Make sure to enable DEBUG through metalsmith.env before logging your first log. Run the example below with DEBUG=build* node metalsmith.js (prefix with SET for Windows):

metalsmith.js
const metalsmith = Metalsmith(__dirname)
const markdown = require('@metalsmith/markdown')
const layouts = require('@metalsmith/layouts')
const debug = metalsmith.debug('build')
const timeStart = performance.now()

function logFilesAfter(step) {
  return (files) => {
    debug.info('File list after %s: %O', step, Object.keys(files))
  }
}

metalsmith
  .env('DEBUG', process.env.DEBUG)
  .use(() => {
    debug('Starting')        // logs "build Starting" in gray

    debug.warn('An info')    // logs "build:info  An info" in cyan
    debug.info('A warning')  // logs "build:warn  A warning" in orange
    debug.error('An error')  // logs "build:error  An error" in red
  })
  .use(logFilesAfter('start'))
  .use(markdown())
  .use(logFilesAfter('markdown'))
  .use(layouts())
  .use(logFilesAfter('layouts'))
  .build(err => {
    if (err) throw err
    const timeEnd = performance.now()
    debug.info('Build successful after %s seconds', (timeEnd - timeStart) / 1000)
  })

Placeholders like %O (object, multi-line) and %s (string) can be used as in the example, see debug formatters. The metalsmith debugger also adds a %b formatter for Node buffers, which is ideal for logging file contents: it will log the first 250 characters of text files followed by ... not to clutter your console. Happy debugging!

You can enable the metalsmith debugger to log outside the metalsmith build by running metalsmith.debug.enable('*') first.

Development setup

Auto-rebuild and browser live reload

Web developers have grown accustomed to the ease of development making a change in the source code and have their builds update automatically and the browser reload. Though Metalsmith does not (yet) offer partial rebuilds, you can quite easily set up an automatic rebuild using a file watcher library (recommended: chokidar) and a browser synchronization package (recommended: browsersync).

In order to do so wrap your Metalsmith build in a function:

metalsmith.mjs
import { fileURLToPath } from "url"
import { dirname } from "path"
import Metalsmith from 'metalsmith'
import layouts from '@metalsmith/layouts'
import markdown from '@metalsmith/markdown'

const __dirname = dirname(fileURLToPath(import.meta.url))

async function msBuild() {
  try {
    const ms = Metalsmith(__dirname)
    const files = await ms
      .use(markdown())
      .use(layouts({
        pattern: '**/*.html'
      }))
      .build()

    console.log('Build success')
    return files
  } catch (err) {
    console.error(err)
    return err
  }
}

const isMainScript = process.argv[1] === fileURLToPath(import.meta.url)) 

if (isMainScript) {
  msBuild()
}

export default msBuild
metalsmith.cjs
const Metalsmith = require('metalsmith')
const layouts = require('@metalsmith/layouts')
const markdown = require('@metalsmith/markdown')

async function msBuild() {
  try {
    const ms = Metalsmith(__dirname)
    const files = await ms
      .use(markdown())
      .use(layouts({
        pattern: '**/*.html'
      }))
      .build()

    console.log('Build success')
    return files
  } catch (err) {
    console.error(err)
    return err
  }
}

const isMainScript = require.main === module

if (isMainScript) {
  msBuild()
} else {
  module.exports = msBuild
}

The last few lines detect whether the file was executed as entry point by Node (node metalsmith.js -> isMainScript) or it was required or imported from another file. Now let's create a second script dev.js which will be the entry point for local development with file watching and browser sync:

dev.js
const browserSync = require('browser-sync')
const chokidar = require('chokidar')
const msBuild = require('./metalsmith.js') // <-- here is our metalsmith build

chokidar
  .watch(['src','layouts'], {
    // avoids causing duplicate builds for initially detected files and folders
    ignoreInitial: true
  })
  .on('ready', () => browserSync.init({
    host: 'localhost',
    port: 3000,
    server: './build',
    injectChanges: false,  // false = prefer full reload
    interval: 2000         // adjust if the build hasn't finished before the browser opens
  }))
  .on('all', async (...args) => {
    await msBuild()
    browserSync.reload()
  })

(async function() {
  await msBuild()
}())

That's all! You can also check out the full setup of this example on Replit:

Run it on replit.com

× This website may use local storage for purely functional purposes (for example to remember preferences), and anonymous cookies to gather information about how visitors use the site. By continuing to browse this site, you agree to its use of cookies and local storage.