Building a complex metalsmith plugin

In the last post, we learned about the way metalsmith uses plugins to manipulate the source data and generate a result. We built a simple logging plugin that helped us to understand:

  • the structure of a metalsmith plugin
  • how to add metadata to the files, and how to get them in a plugin
  • how to manage exceptional cases where the plugin can’t continue.

In this post, we’ll build upon this knowledge to create a more complex plugin.

Purpose of the plugin

Sometimes your post isn’t a one shot, but is part of a serie of closely related posts. You’d like your readers to know that there are other posts on this topic, and you’d like to ease their life by showing them the other posts without having them to search your site. You may even want to give a name to the serie and use it in the URL of the post, or add any information that may be useful to the reader (github repository, etc…)

In fact, this metalsmith plugin already exists because I developed it because I needed it to build this web site. You can found it on the npm offial site.

Design

Now that the purpose of the plugin is clearly defined. Let’s think about what we need to do to achieve this result.
Obviously, we need to have a link between the serie’s data and each post being part of it. How about that:

So in the post we’ll have something like:

---
....some YAML metadata
serie: my-wonderful-serie
... some more metadata
---
some great content

And the plugin config object would look like this:

{
    'my-wonderful-serie': {
        title: "This is a wonderful serie",
        // other properties
    },
    'another-great-serie': {
        title: "Self-confidence in 10 lessons",
        // other properties
    }
}

You can see that this config object contains the series informations. The title property is mandatory because it makes sense to always provide a user-friendly name for the serie.

Let’s summarize how we want the plugin to work:

  1. the user add to the posts’ YAML metadata a property - let’s call it seriename - which will give the serie’s internal name
  2. he also adds to the plugin config a way to map the serie’s internal name to a bunch of properties
  3. when the plugin runs, it searches the file metadata for the seriename property.
  4. If it doesn’t find the property, then it’s just that the file is not intended to be part of a serie. Skip it and look for the next one.
  5. If it finds it, it searches the plugin config for this serie (and it should find it !). The plugin creates a bidirectional link between the serie and the file by
    • adding the current file to a serie object that will be stored in metalsmith metadata
    • adding the serie object to the file metadata

Now that we have some ideas about what we want to do, let’s start to code !

Writing the code

Here is the main code of the plugin, which I’ll explain it piece by piece.

function series(config) {
    return function(files, metalsmith, done) {
        var metadata = metalsmith.metadata();
        metadata.series = [];

        for (var file in files) {
            var fileObject = files[file];
            var serieName = fileObject.seriename;
            if (serieName) {
                var serie = findSerieInMetadata(serieName);
                if (!serie) {
                    var serieConfig = config[serieName];
                    if (serieConfig === undefined) {
                        return done(new Error("Series plugin: Couldn't find
                        serie " + serieName + " in plugin configuration."));
                    }  
                    serie = initializeSerie(fileObject, serieConfig);
                }
                else {
                    serie.files.push(fileObject);
                }
                fileObject.serie = serie; 
            }
        }
        done();
    };
}

You may be surprised by the way the plugin function is created. In previous posts, we used to write a plain function this way:

var myplugin = function(files, metalsmith, done) {
    ...
}

But now, we want to pass some configuration data to our plugin. To achieve this goal, we write a function which takes the configuration as a parameter and returns the plugin function:

function series(config) {
    return function(files, metalsmith, done) {
        ...
    }
}

This is a very common usage of closures in javascript. Let’s dig further into this code now.

var metadata = metalsmith.metadata();
metadata.series = [];

We ask Metalsmith for its metadata, and then store our series object into it. Of course it’s empty for now. Next piece of code ?

 for (var file in files) {
    var fileObject = files[file];
    var serieName = fileObject.seriename;
    if (serieName) {
        var serie = findSerieInMetadata(serieName);
        if (!serie) {
            var serieConfig = config[serieName];
            if (serieConfig === undefined) {
                return done(new Error("Series plugin: Couldn't find
                serie " + serieName + " in plugin configuration."));
            }  
            serie = initializeSerie(fileObject, serieConfig);
        }
        else {
            serie.files.push(fileObject);
        }
        fileObject.serie = serie; 
    }
}

We loop over the files and look for the seriename property in the file’s YAML metadata (remember, Metalsmith has already done the hard work, so we just have to look for fileObject.seriename). If the property is present, we look in the metadata for this exact name. The findSerieInMetadata() function does exactly that:

function findSerieInMetadata(serieName) {
    for (var i = 0; i < metadata.series.length; i++) {
        var serie = metadata.series[i];
        if (serie.name === serieName) {
            return serie;
        }
    }
    return undefined;          
}

If we couldn’t find any serie with this name, then this must be the first post of the serie. In this case, we’ll have to search the plugin config for the serie’s data. This is rather easy because we designed the config object to be an object where each property is the serie’s internal name, and the value of the property is the serie’s data. So we have all the informations we need to create a new serie object, returned by the initializeSerie() function:

function initializeSerie(fileObject, serieConfig) {
    function copyProperties(src, dest) {
        Object.keys(src).forEach(function (property) {
            dest[property] = src[property];
        });
    }

    var serie = {};
    serie.name = fileObject.seriename;
    copyProperties(serieConfig, serie);
    serie.files = [fileObject];
    metadata.series.push(serie);

    return serie;
}

This function creates a new serie object with a name property which comes from the YAML metadata. The it copies all the properties of the serie , which where given in the plugin config. After that, it puts the current file in the serie’s files (and it feels lonely for the moment), and adds this new serie to the metadata.

Of course, if we can’t find the serie in the plugin config, we have a serious problem because someone put a serie name in the YAML metadata of a post but forgot to declare this serie in the config. The plugin can’t do his work and raises an error.
If a serie with this name already existed in the metadata, we just have to add the current post to its files.
Finally, we update the current file to add a pointer to the serie it belongs to, because we wanted to have a 2-way relationship between the series and their files.

And this is it ! With this example, you can see how easy it is to build things that can seem daunting with Metalsmith. Note that the real version of this plugin is a bit more complex, because I added some options in the configuration object, but nothing too fancy.

Usage

Writing code is fine, but what makes us really happy is when we see that it works as expected, isn’t it ? How can we use this plugin ?
Here is a simplified version of the way I use it on my blog (and in english too, because you may have noticed that it’s mainly a french blog).

{{#if serie }}
    <aside>
        <p>
            This article belongs to the serie <strong>"{{ serie.title }}"</strong>
        </p>
        <ol>
            {{#each serie.files }}
            <li>
                <a href="{{link this.path }}" rel="bookmark">{{ this.title }}</a>
            </li>
            {{/each}}
        </ol>
    </aside>
{{/if}}

I use Handlebars as my templating engine, but it’s easy to understand even if you’re not familiar with it. The context of evaluation is the current file, so expressions like {{#if serie }} will try to find a property named serie in the current file object.
You can get the properties of the serie ({{ serie.title }}) and loop over the files belonging to the same serie very easily ({{#each serie.files }}).

Conclusion

In this post, we learned how to write more complex plugins, involving passing configuration data. We also learned that we can use Metalsmith metadata to store our data in a very useful way.

I hope you also noticed that the code remains very concise, so don’t be afraid of creating your own plugin if you need to ! If you’re using my plugin, please let me know if you need additional features. And don’t be shy to ask about Metalsmith which is, I hope you agree, a very nice tool to build a static web site.

comments powered by Disqus