# Metalsmith taxonomy

Metalsmith plugin that organizes files into taxonomy trees in global metadata and auto-generates taxonomy page objects.

[![metalsmith: plugin][metalsmith-badge]][metalsmith-url]
[![npm: version][npm-badge]][npm-url]
[![ci: build][ci-badge]][ci-url]
[![code coverage][codecov-badge]][codecov-url]
[![license: LGPL-3.0][license-badge]][license-url]

## Features

- can pre-filter files by pattern
- adds `taxonomies` tree to global metadata
- supports multiple taxonomy namespaces (e.g. blog, products, etc.)
- works well in combination with metalsmith-filemetadata, collections and permalinks
- automatically generates overview, taxonomy & taxonomy term pages (e.g. taxonomies > categories > category)

## Install

NPM:

```bash
npm i -D metalsmith-taxonomy
```

Yarn:

```bash
yarn add metalsmith-taxonomy
```

## Usage

### Quickstart

Simplest usage without parameters or with default parameters (all examples below yield the same results).

```js
var default_taxonomy_set = {
  pattern: '**/*.{md,html}',
  namespace: null,
  pages: ['index', 'taxonomy', 'term'],
  taxonomies: ['tags', 'category']
};

metalsmith.use(taxonomy());

// is the same as
metalsmith.use(taxonomy(default_taxonomy_set));

// or the same as
metalsmith.use(taxonomy([default_taxonomy_set]);
```

### Options

Pass one or more taxonomy sets to the plugin.
A taxonomy set is an object with the following properties:

| Property     | Type            | Default               | Description                                                                                                                                                                                                                                                                                                 |
| :----------- | :-------------- | :-------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `pattern`    | `string\|array` | `**/*.{md,html}`      | One or more [glob patterns][1] supported by [multimatch][2].                                                                                                                                                                                                                                                |
| `namespace`  | `string`        | `null`                | Subkey in `metadata.taxonomies[namespace]` in which the taxonomy tree of this set will be stored.                                                                                                                                                                                                           |
| `pages`      | `boolean`       | `true`                | If `true`, a taxonomy set index page, pages for each taxonomy in the set, and each term of each taxonomy in the set will be generated. If `false`, no pages will be generated.                                                                                                                              |
|              | `array`         |                       | An array with one or more of: `index`, `taxonomy`, `term` allows limiting the type of pages generated.                                                                                                                                                                                                      |
| `taxonomies` | `array`         | `['category','tags']` | An array containing all file metadata keys to use as taxonomies                                                                                                                                                                                                                                             |
|              | `Object`        |                       | Passing an object allows more flexibility in mapping taxonomy terms and values. The object's _keys_ will become the _taxonomy names_, and its _values_ will match metadata file keys. The file metadata key can also be a keypath (e.g. `meta.keywords`), and the file metadata value can also be an array. |

### Global metadata

If you had two files with the following content:

<table>
  <tr>
    <th><code>blog/article1.md</code></th>
    <th><code>blog/article2.md</code></th>
  </tr>
  <tr>
    <td><pre>---
title: Article 1
category: category1
tags:
  - tag1
  - tag2
---</pre></td>
    <td><pre>---
title: Article 2
category: category2
tags:
  - tag2
---</pre></td>
  </tr>
</table>

The [Quickstart](#quickstart) example will generate the following results in global metadata:

```js
  {
    ...metadata,
    taxonomies: {
      tags: {
        tag1: [article1],
        tag2: [article1, article2]
      },
      category: {
        category1: [article1],
        category2: [article2]
      }
    }
  }
```

### Auto-generated index, taxonomy, and term pages

If the `pages` property of a taxonomy set is not an empty array, or `false`, metalsmith-taxonomy will auto-generate pages at the following paths:

| Page type  | Path                              | Example                       |
| :--------- | :-------------------------------- | :---------------------------- |
| `index`    | `:namespace.html`                 | blog.html                     |
| `taxonomy` | `:namespace/:taxonomy.html`       | blog/category.html            |
| `term`     | `:namespace/:taxonomy/:term.html` | blog/category/metalsmith.html |

If `namespace` is not defined, the `index` page path will default to `index.html`.

With the [Quickstart](#quickstart) example, metalsmith-taxonomy will generate:

```js
{
  'index.html',
  'category.html',
  'category/category1.html',
  'category/category2.html',
  'tags.html',
  'tags/tag1.html',
  'tags/tag2.html',
  ...other_files
};
```

Generated file objects get the following metadata:

#### Index page metadata

| Property     | Type               | Description                                                                                             |
| :----------- | :----------------- | :------------------------------------------------------------------------------------------------------ |
| `type`       | `'taxonomy:index'` | Page type                                                                                               |
| `path`       | `string`           | Destination path of the page                                                                            |
| `namespace`  | `null\|string`     | Namespace passed in taxonomy set                                                                        |
| `taxonomies` | `object`           | Copy of the object at `metadata.taxonomies[namespace]` (or `metadata.taxonomies` if `namespace===null`) |
| `contents`   | `string`           | Empty string                                                                                            |

#### Taxonomy page metadata

| Property     | Type                  | Description                                                                                             |
| :----------- | :-------------------- | :------------------------------------------------------------------------------------------------------ |
| `type`       | `'taxonomy:taxonomy'` | Page type                                                                                               |
| `path`       | `string`              | Destination path of the page                                                                            |
| `namespace`  | `null\|string`        | Namespace passed in taxonomy set                                                                        |
| `taxonomy`   | `string`              | Name of the current taxonomy                                                                            |
| `terms`      | `array`               | Array with the terms found for the current taxonomy                                                     |
| `taxonomies` | `object`              | Copy of the object at `metadata.taxonomies[namespace]` (or `metadata.taxonomies` if `namespace===null`) |
| `contents`   | `string`              | Empty string                                                                                            |

#### Term page metadata

| Property     | Type              | Description                                                                                             |
| :----------- | :---------------- | :------------------------------------------------------------------------------------------------------ |
| `type`       | `'taxonomy:term'` | Page type                                                                                               |
| `path`       | `string`          | Destination path of the page                                                                            |
| `namespace`  | `null\|string`    | Namespace passed in taxonomy set                                                                        |
| `taxonomy`   | `string`          | Name of the current taxonomy                                                                            |
| `terms`      | `array`           | Array with the terms found for the current taxonomy                                                     |
| `taxonomies` | `object`          | Copy of the object at `metadata.taxonomies[namespace]` (or `metadata.taxonomies` if `namespace===null`) |
| `contents`   | `string`          | Empty string                                                                                            |

#### Adding extra metadata to the generated pages

If a file already exists at the target path of a generated page, the generated metadata will be merged into the existing metadata of that file. Extra metadata can also be added with plugins like [metalsmith-filemetadata][5] further in the plugin chain.

### Sorting the term matches

By default, the files will be sorted as they are read from the filesystem (alphabetically by file name).

The data available to metadata is a _reference_ to the items under `taxonomies[namespace][taxonomy][term]`,
so you could use [metalsmith-keymaster](https://github.com/MorganConrad/metalsmith-keymaster) or a custom plugin to sort the data:

```js
metalsmith.use(taxonomy).use(function (files, metalsmith) {
  var taxonomies = metalsmith.metadata().taxonomies;

  Object.keys(taxonomies.tags).forEach(function (tagName) {
    taxonomies.tags[tagName].sort(function (a, b) {
      return a.order < b.order ? -1 : a.order > b.order ? 1 : 0;
    });
  });
});
```

The example above shows how to sort all term collections under the `tag` taxonomy by an `order` property defined in each file's metadata.

### Custom metadata & rendering with metalsmith-layouts/filemetadata/default-values

You can use [metalsmith-filemetadata][5] to add custom data to the generated file objects by pattern,
e.g. to specify a `layout` property to be used later in the chain by [metalsmith-layouts][6].

```js
var taxonomy = require('metalsmith-taxonomy')({
  namespace: 'taxonomies',
  taxonomies: ['category', 'tags']
});

var filemetadata = require('metalsmith-filemetadata')([
  {
    pattern: 'taxonomies/**/*.html',
    metadata: { layout: 'taxonomy-term.hbs' }
  },
  {
    pattern: 'taxonomies/*.html',
    metadata: { layout: 'taxonomy.hbs' }
  },
  {
    pattern: 'taxonomies.html',
    metadata: { layout: 'taxonomy-index.hbs' }
  }
]);

var layouts = require('metalsmith-layouts')({
  directory: 'src/layouts',
  default: 'default.hbs',
  pattern: '**/*.{md,html}'
});

metalsmith.use(taxonomy).use(filemetadata).use(layouts);
```

[metalsmith-default-values](https://github.com/metalsmith/metalsmith-default-values) works exactly the same as above.

### Custom or nested page paths with metalsmith-permalinks

You can use [metalsmith-permalinks][4] to move or nest taxonomy pages:

```js
var taxonomy = require('metalsmith-taxonomy')({
  pages: ['index', 'taxonomy', 'term'],
  taxonomies: ['category', 'tags']
});

var collections = require('metalsmith-collections')({
  posts: 'posts/**/*.md'
});

var permalinks = {
  linksets: [
    {
      match: { collection: 'posts' },
      pattern: 'posts/:category/:title'
    },
    {
      match: { type: 'taxonomy:index' },
      pattern: 'posts'
    },
    {
      match: { type: 'taxonomy:term', taxonomy: 'category', namespace: 'blog' },
      pattern: 'posts/:category'
    },
    {
      match: { type: 'taxonomy:taxonomy' },
      pattern: 'posts/:taxonomy'
    },
    {
      match: { type: 'taxonomy:term', namespace: 'blog' },
      pattern: 'posts/:category'
    }
  ]
};

metalsmith.use(taxonomy).use(collections).use(permalinks);
```

with the example files from [Global metadata](#global-metadata) would result in a directory tree like:

```
└── posts
    ├── index.html --> index page
    ├── tags
    |    ├── index.html --> taxonomy page
    |    ├── tag1
    |    |    └── index.html --> term page
    |    └── tag2
    |         └── index.html --> term page
    ├── category
    |    └── index.html --> taxonomy page
    ├── category1
    |    ├── index.html --> term page
    |    └── article-1
    |         └── index.html
    └── category2
         ├── index.html --> term page
         └── article-2
              └── index.html

```

## Examples

Clone this repository and navigate to the [`example/licenses`](example/open-source-licenses) or more advanced [`example/blog`](example/blog) directory, run npm install & npm start.

## License

[LGPL v0.3](./LICENSE)

[1]: https://en.wikipedia.org/wiki/Glob_%28programming%29 'glob patterns on Wikipedia'
[2]: https://github.com/sindresorhus/multimatch 'multimatch on Github'
[3]: https://github.com/segmentio/metalsmith-collections 'metalsmith-collections on Github'
[4]: https://github.com/segmentio/metalsmith-permalinks 'metalsmith-permalinks on Github'
[5]: https://github.com/dpobel/metalsmith-filemetadata 'metalsmith-filemetadata on Github'
[6]: https://github.com/metalsmith/metalsmith-layouts 'metalsmith-layouts on Github'
[npm-badge]: https://img.shields.io/npm/v/metalsmith-taxonomy
[npm-url]: https://www.npmjs.com/package/metalsmith-taxonomy
[ci-badge]: https://github.com/metalsmith/metalsmith/actions/workflows/test.yml/badge.svg
[ci-url]: https://github.com/metalsmith/metalsmith/actions/workflows/test.yml
[license-badge]: https://img.shields.io/github/license/webketje/metalsmith-taxonomy
[license-url]: https://choosealicense.com/licenses/lgpl-3.0/
[codecov-badge]: https://img.shields.io/coveralls/github/webketje/metalsmith-taxonomy
[codecov-url]: https://coveralls.io/github/webketje/metalsmith-taxonomy
[metalsmith-badge]: https://img.shields.io/badge/metalsmith-plugin-green.svg?longCache=true
[metalsmith-url]: https://metalsmith.io/
