This plugin development reference will walk you through starting with a simple local plugin and finishing with a flexible multi-option plugin that follows best practices, ready for publishing on NPM. Feel free to drop out at any time or jump to the sections that you need.
Writing a quick local plugin
Writing a plugin is super-simple! A metalsmith plugin is just a function that is passed the Files object and the Metalsmith instance. even console.log can be a plugin:
Let us give our plugin a more fitting name, snapshot, because it takes a snapshot of the files’ state at a given moment in the plugin chain.
If you run this code with node metalsmith, you will see the Metalsmith instance with plugins: [ [Function: snapshot] ]. It is important that the plugin is a named function. The function name is the plugin’s name. It is good practice to use a named function for your plugin:
it allows other plugins and debug tools (like metalsmith-debug-ui ) to identify which plugins the metalsmith build uses through metalsmith.plugins. For example, this is the output of console.log(metalsmith.plugins) in a plugin in the metalsmith.io (this site)’s build:
Anonymous functions all get the name Function (anonymous) which is harder to inspect
The same argument of clarity applies for error stack traces. Naming the plugin allows users to immediately pin the plugin that causes issues:
UnhandledPromiseRejectionWarning: EINVALID_ARGUMENT: Requiredoption cannot be empty
at Ware.snapshot (/home/user/ms/metalsmith.js:9:19)
at Ware.<anonymous> (/home/user/ms/node_modules/wrap-fn/index.js:45:19)
Cool, cool. But the snapshot plugin is pretty limited. We would like to be able to re-use it to target only certain files, and only certain metadata properties. And perhaps also write the results to a file.
Adding options to a plugin
To pass options to a plugin we simply wrap and return the plugin body in a closure function, — an initializer —, then call it in our plugin chain. Mind that code in the initializer runs before the plugin is passed to metalsmith.use. In the initializer you can pass, map & validate options and run other setup logic that doesn’t need metalsmith build info.
As the snapshot plugin becomes more powerful and reusable, we move it to its own file snapshot.js in a plugins folder next to metalsmith.js, and const snapshot = require('./plugins/snapshot') in metalsmith.js.
We also add a default options object in case none are passed to the plugin. We define the (glob) pattern option, so we can target specific files to log, and default it to '**' (= all files). We also define the keys option, so we can target specific file metadata to log)
We passed options to the plugin but we’re not doing anything with them yet. We need to know how to manipulate files, file paths and metadata first. The following sections Manipulating filepaths, Manipulating files, and Manipulating metadata provide general info about how to use JS Object & array methods, the NodeJS path library, and the metalsmith instance inside a plugin. If you would rather skip right to the rest of the implementation of the snapshot plugin, go to The plugin body
Manipulating file paths
Inevitably when working with a filesystem, metalsmith works with file paths. Windows has different directory separators \ (backslash) than other systems’ / (forward slash). With the metalsmith.match(pattern) method, you can use forward slashes for both. However, be aware that when you modify file paths and write them back to the Files object, you need to use the OS-specific path separators.
NodeJS includes a handy standard path module that you can use for this: require('path'). Inside a plugin you can use the path module to get all variations of a path. The plugin below will attach different types of path data to each file in the metalsmith build:
Manipulating files
Looping over files
You can use any of the Object static methods to easily loop over metalsmith files inside a plugin and apply manipulations to them with the Javascript Array methods. Metalsmith#match is a helper to loop over a subset of normalized (i.e. Windows & Linux-compatible) file path matches.
You can use any of the Object methods described above and then array-filter or slice them before applying manipulations, or you could use metalsmith.match to target them easily with a glob pattern:
Adding, updating, renaming, moving and removing files
Add a file dynamically by assigning a File object to a key in the Metalsmith Files object Update a file’s metadata by re-assigning its keys. Remove a file simply by deleting its key from the files object.
Manipulating metadata
Plugins can read and make changes to the metadata specified with Metalsmith.metadata much like they can with files. They can add metadata, update, rename, or remove metadata, and they can also make metadata available to files or make files available in metadata! A plugin like @metalsmith/collections does both. Below is an example of how plugins can implement most of these:
function plugin(files, metalsmith) {
// read metadata
const metadata = metalsmith.metadata()
// add a metadata key:value
metadata.buildTimestamp = Date.now()
// add multiple metadata key:value's
Object.assign(metadata, {
// make all html files available on metadata as "pages"
pages: metalsmith.match('**/*.html').map(key => files[key])
})
// pass global metadata to all files in the "globalMetadata" key (not recommended)
Object.keys(files).forEach(filepath => {
files[filepath].globalMetadata = metadata
})
}
The plugin body
Armed with enough info about how to manipulate files, paths, and metadata, we can now continue writing the snapshot plugin. To be able to get metadata at any.key.path, we will use lodash.get and lodash.set (install it with npm i lodash.get lodash.set). A code snippet is worth a thousand words, so here we go:
We can now run this plugin to provide different outputs:
const Metalsmith = require('metalsmith')
const snapshot = require('./plugins/snapshot')
Metalsmith(__dirname)
// log all file data
.use(snapshot())
// log each .html file's last modified date
.use(snapshot({ keys: ['stats.mtime'], pattern: '**/*.html' }))
.build((err, files) => {
if (err) throw err
console.log('Build success!')
})
Let’s also add an extra option to write the metadata to a log file in the build directory, that will be write: true || false. We will output the files as <filename>.snapshot<index>.json in the build directory, right next to the file itself. We add an index to the snapshot so we can see how the file metadata evolves after each plugin:
Let us now quickly race through a boring but very important part: handling errors
Handling errors
Until now the snapshot plugin has used 2 parameters: files and metalsmith; but as you can see from the API docs, there is a third, done callback. To let the user decide what to do with errors, it is a good idea not to throw an error that occurs during the plugin’s run, but instead pass it to done(error).
For demo purposes, here is an abort plugin that will stop the build by throwing a custom error unless you pass false to it:
The plugin also demonstrates how you can create a custom error in a simple way. Note that you can also return a promise if you prefer:
For static options validation (=options which don’t require extra metalsmith build info) you may choose to throw an error immediately in the initPlugin wrapper:
function initMyPlugin(options) {
if (!options.requiredOption) {
throw new Error('requiredOption is required')
}
return function MyPlugin() { ... }
}
Adding debug logs to the plugin
Plugins implement debugging with the debug NPM package. Require it in your plugin, and pass it the name of your plugin. For the snapshot plugin this could be:
You can also ‘divide’ the debug logs for your plugin in multiple channels:
const warn = debug.extend('warn') // warn('Careful!') will output "metalsmith-snapshot:warn Careful!"
const error = debug.extend('error') // error('Oops!') will output "metalsmith-snapshot:error Oops!"
debug provides some handy formatters for objects and JSON logging. Here are some usage examples:
// Pretty-print an Object on multiple lines.
debug('Running with options: %O', { pattern: '**' })
// log JSON.stringified version
debug('Metalsmith.metadata: %j', metalsmith.metadata())
Things that are generally interesting to log are:
the plugin options, after normalization (eg. filled with defaults)
the files or metadata that were processed
errors & warnings
Asynchronous manipulations
If a plugin does some manipulations asynchronously, it needs to notify metalsmith when it’s done by calling done() or returning a promise.
Manipulations within a plugin can happen in parallel, but the plugin should only call done when all manipulations of the plugin are done. To such effect we can use Promise.all. Below we change the addExternalFile plugin from the previous example to handle multiple files:
× 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.