npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

metalsmith-menu

v1.0.4

Published

A metalsmith plugin to read menu trees from frontmatter properties.

Readme

metalsmith-menu

This Metalsmith plugin reads a hierarchical menu structure from file properties and attaches it as a tree of nodes to Metalsmith's metadata and/or file data objects. In combination with a template engine you can then use this menu tree to create a sitemap, a global menu, breadcrumbs, local menus or even to merge all files into a single large file.

Installation

npm install metalsmith-menu

Overview

At first you need to mark your files with some property. The easiest way to do this is to use YAML frontmatter:

---
menu: "1.2.3"
---
file content

It is also allowed to define the key as an array; e.g. [1, 2, 3]. To be more accurate: How exactly you attach such keys to file objects is not important. You could also use Javascript to assign keys.

  • Notice: The default reader function works best with simple integer sequences defined as above. Custom reader functions can be used to support more complex components like roman numbers. (see Options.readMenuKeyFunc for more details)

Once the plugin runs, it reads these keys into a menu tree. As an example, such a tree could have the the following structure:

root
  [1] => dummy
  .  [1, 1] => 1.txt
  .  [1, 1] => 4.txt
  .  [1, 2] => 2.txt
  .  .  [1, 2, 3] => 3.txt
  [2] => 5.txt

Assuming you have used the following keys:

//- read (path, key) as files[path].menu = key
("1.txt", "1.1"), ("2.txt", "1.2"), ("3.txt", "1.2.3"), ("4.txt", "1.1"), ("5.txt", "2")

When the plugin builds the tree, it starts at the root and fetches the key from the current file. It then takes the first component of the key and compares it with the first component of all child nodes that are already attached to the root. The plugin will add a new node if that component wasn't found. It will then use the new node, or the child node it found as new root.

  • Notice: No file has key "1" assigned to it. As a result, the algorithm used to build the menu tree must add a dummy node (e.g. [1]). These nodes can easily be detected by testing the condition (node.file === undefined).
  • Notice: Files "1.txt" and "4.txt" use the same key. In other words: This plugin does not require that keys are unique.

Once the plugin has built the menu tree and attached it to metalsmith's data objects, you can use it in combination with a template engine to create:

  • a sitemap.
  • a global menu - to display the first levels of the global menu structure.
  • breadcrumbs - to display how to reach the current node (menu file) from the root.
  • local menus - to display what the parent menu is and what the sub-menus are.

You could even use the menu tree to merge all files into a single large file.

See the ./examples sub-folder for details on how to traverse the menu tree.

Usage

const menu = require('metalsmith-menu');

//- this will use only default options
.use(menu())

//- this will use the given string value for
//  menuKey, menuMetalsmith, menuFile
.use(menu("menu"))

//- supply an object with custom settings
.use(menu(options));

For options you only need to pass a simple javascript object that has the following properties (see ./src/Options.js for more details):

Options {
  //- a multimatch pattern to select which files to process.
  //- an array of patterns is supported.
  //- files that don't match (any pattern) will be ignored.
  filter: "**",

  //- the name of the file property to read from.
  //- files will be ignored if there is no such property.
  menuKey: "menu",

  //- if file[menuKey] holds a string value, then this setting
  //  defines which separator to use to split that value.
  menuKeySep: ".",

  //- to which metalsmith metadata property to attach the menu
  //  tree's root node.
  menuMetalsmith: "menu",

  //- to which file property to attach a file's menu tree node.
  //- all file[menuKey] properties will be overwritten if
  //  (options.menuKey == options.menuFile)!
  menuFile: "menu"

  //- a custom function used to read all file[menuKey]
  //- expected signature is: Array function(path, file, Options)
  //- see below for it's expected behavior
  readMenuKeyFunc: <reader function>,
}
  • Notice: If any property is missing (e.g. commented out), then a default value will be used. The above definition showcases the values used by default.
  • Notice: metadata[menuMetalsmith] and all file[menuFile] properties will refer to different nodes of the same menu tree.
  • Notice: By letting the plugin run multiple times with different values for menuKey, menuMetalsmith and menuFile it is possible to generate multiple distinct menu trees.

Once the plugin has run, node objects will be attached to metalsmith's metadata object and/or each menu file. These objects have the following properties (see ./src/Node.js for more details):

Node {
  //- undefined, or corresponds to files[i]
  //- e.g. one of your menu file is marked with "1.1",
  //  but none with "1"; in that case, a node is generated
  //  for "1" with no file object attached to it
  //  (i.e. undefined, aka dummy node).
  file: undefined,

  //- undefined, or corresponds to the path of files[i]
  //  i.e. the key of a file assigned to it by the files object
  //- e.g. files["test.txt"] = {...} will get you:
  //  node.file = {...}, node.path = "test.txt"
  path: undefined,

  //- the fully qualified menu path
  //- e.g. [1, 2, 3] if your file is marked with "1.2.3"
  keyArray: [],

  //- the topmost node of the menu tree
  //- root.root = root (circular!)
  root: this,

  //- undefined, or the next node when moving one step up
  //  towards the root
  parent: undefined,

  //- undefined, or the next sibling; same level, same parent
  next: undefined,

  //- undefined, or the previous sibling; same level, same parent
  previous: undefined,

  //- all direct child nodes of the current node
  //- nodes will be sorted in ascending order
  children: [ Node ],

  //- all child nodes of the current sub-tree
  //- nodes will be sorted in ascending order
  childrenAll: [ Node ],

  //- how deep into the tree a node is located
  //- root.level = 0, node.level = node.parent.level + 1
  //- this value is identical to keyArray.length
  //  and thus is only added as a matter of convenience
  level: 0
}

A crude method to visit all menu nodes in ascending order is:

.use(function(files, metalsmith, done) {
  const metadata = metalsmith.metadata();
  const root = metadata.menu;
  
  root.childrenAll.forEach(function(node, index, array) {
    //- assuming the menu tree from the above example,
    //  this will print the following values
    //  (one per line and in that order):
    //  1, 1-1, 1-1, 1-2, 1-2-3, 2
    console.log(node.keyArray.join('-'));
  });
})

Options.readMenuKeyFunc

If this property evaluates to undefined, a default reader function will be used. This default reader does some checking and only performs basic transformations on file[menuKey] values: (1) convert to an array (2) trim() all components (3) convert strings like "N" to numbers, if the value matches /[0-9]+/. Therefore, the default reader will best work with simple number sequences.

In the case that some pre-processing has already been done to turn all keys into valid arrays, set this property to false. This will tell the plugin to expect these arrays to already have their final value/form. The plugin will then do basic checks, but won't visit or alter any key components.

For a custom reader, assign a function to this property that has the following signature:
Array function(path, file, Options)

The basic behavior of such a reader function is:

function reader(path, file, options) {
  //- access the file's key
  var key = file[options.menuKey];

  //- transform the key into an array of values in a way that
  //  node1.keyArray[i] can be compared with node2.keyArray[i],
  //  which will only happen if (node1.parent === node2.parent)
  //  and for (i === node1.parent.level).
  //- comparison will when sorting the children of a node and
  //  using the '<' and '>' operators
  //- if key is a string you would probably use
  //  array = key.split(options.menuKeySep)
  var array = transform(key);

  if(file_should_be_ignored) {
    return false;
  } else {
    return array;
  }
}

This will allow you to use keys that have complex components like "XIII" (roman numbers), "Index A" or "Chapter 1". The most simplistic method to implement a custom reader would be to transform all key components into integer values.

License

MIT