Getting started

← Docs overview

简介

什么是 Metalsmith?

Metalsmith 是一个基于 NodeJS 开发的、极简的、插件化的静态网站生成器。 Let us break that down:

Metalsmith 是一款静态网站生成器

静态网站生成器的任务是生成可部署到网络服务器上的静态文件。对于静态网站生成器来说,这意味着:

  1. 获取源文件所在目录,读取源文件并提取其中的信息
  2. 使用插件处理信息
  3. 将处理后的信息写入目标目录的文件中(NPM 是)。

Metalsmith 就是基于这一原理而构建的。即,它从源文件中获取信息,并将处理后的信息写入目标目录的文件中。

Metalsmith 极其简单

Metalsmith 是插件化的

Metalsmith 将所有操作都留给了插件来实现。

这里所说的操作可以是任何功能:转换模板、转译代码、替换变量、用布局文件包裹内容、文件分组、移动和删除文件等等。这就是为什么我们说 一切皆插件。操作可以一个接一个地执行,或者 链式执行(chained) 或者 管道式执行(piped),随你喜欢。显然,在一个执行链条上,插件的顺序 就变得很重要了。

将 metalsmith 拆分成一个简洁、坚固的核心以及一系列插件,可以降低复杂性。这让你可以自由地使用你所 需要 的插件。并且分散了维护 Metalsmith 生态系统的荣誉和负担。

Metalsmith is more

根据你所面对的使用场景,Metalsmith 可以 不仅仅 是一个静态网站生成器:

得益于简单的内存抽象,metalsmith 的构建和插件无需大量模拟就能轻松进行单元测试。 看看 the Metalsmith tests ,你就会有所了解。

前提条件

Metalsmith 可在所有 NodeJS 长期支持版本(生命周期未结束)及其它更多版本上运行。下载并安装 NodeJs 和 NPM(Node 已内置 NPM): https://nodejs.org/en/download/ 。 如果你希望发布自己的插件,建议使用 nvmnvs (适用于 Windows)

如果你要使用 Metalsmith CLI,请在 PATH 环境变量中添加 node_modules/.bin 路径:

Metalsmith 已在 Linux 和 Windows 系统以及所有 官方支持的 NodeJS LTS 版本 上进行了测试。

安装

首先,为新项目创建一个项目文件夹并切换至该文件夹下:

mkdir ~/Documents/metalsmith-website && cd ~/Documents/metalsmith-website
mkdir %userProfile%\Documents\metalsmith-website && cd %userProfile%\Documents\metalsmith-website

然后使用任何 NodeJS 软件包管理器安装 Metalsmith:

npm install metalsmith
yarn add metalsmith
pnpm add metalsmith

可能还需要安装一些插件:

npm install @metalsmith/collections @metalsmith/markdown @metalsmith/permalinks @metalsmith/layouts
yarn add @metalsmith/collections @metalsmith/markdown @metalsmith/permalinks @metalsmith/layouts
pnpm add @metalsmith/collections @metalsmith/markdown @metalsmith/permalinks @metalsmith/layouts

如果你想使用 Typescript 写代码,请务必同时安装 @types/metalsmith 软件包。

概念

要了解如何使用 Metalsmith,你只需要熟悉几个核心概念,如果你使用过其他静态网站生成器,或者在终端中运行过一些命令(例如 git addls -la),你可能已经熟悉了其中的大部分了。

Files

Everyone has worked with files before. They have a name, a path, an extension, contents and metadata (like the last modified date). Metalsmith represents every file in the source directory as a javascript File object. For instance,

src/post/my-file.md:

---
title: A Catchy Title
draft: false
---

An unfinished article...

...becomes

{
  'post/my-file.md': {
    title: 'A Catchy Title',
    draft: false,
    contents: 'An unfinished article...',
    mode: '0664',
    stats: {
      /* keys with information on the file */
    }    
  }
}

...where the content of the file is always mapped to the property value of contents. For illustration purposes only we display the value of contents as a string. Technically, the property value of contents is mapped as a NodeJS Buffer , which can also handle binary data (for images, PDF's, etc). mode contains the file permission bit and stats has more technical information on the file such as size or birthtime. The file is also parsed for YAML front matter, which is merged into the File object. Thus, we finally have a javascript Files object of objects.

{
  'relative_to_sourcepath/file1.md': {
    title: 'A Catchy Title',
    draft: false,
    contents: 'An unfinished article...',
    mode: '0664',
    stats: {
      /* keys with information on the file */
    }    
  },
  'relative_to_sourcepath/file2.md': {
    title: 'An Even Better Title',
    draft: false,
    contents: 'One more unfinished article...',
    mode: '0664',
    stats: {
      /* keys with information on the file */
    }    
  }
}

Plugins can then manipulate the javascript File objects representing the original files however they want, and writing a plugin is super simple.

Front matter

To attach metadata to a JS File object, metalsmith reads front matter. Front matter is a term borrowed from the publishing industry meaning metadata about a written work. In Metalsmith this is a YAML document section (delineated by ---) containing metadata (matter) at the top (front) of a file (commonly, markdown). Metalsmith will recognize and read the front matter of a file and add it as metadata to the JS file representation when you run the build. Here is a typical example of an index.md file with YAML front-matter. If you don't like the YAML syntax you can use JSON front matter as well

index.md
---
title: Hello World
keywords:
  - hello
  - world
draft: false
---
Welcome to my blog
index.md
---
{
  "title": "Hello World",
  "keywords": ["hello","world"],
  "draft": false
}
---
Welcome to my blog

The front-matter will be parsed by Metalsmith as:

{
  title: 'Hello World',
  keywords: ['hello','world'],
  draft: false,
  contents: <Buffer>,
  mode: '0644',
  stats: { ... }
}

When the front matter is read into javascript, we refer to it as file metadata.

Multi-line strings

A common requirement is to write multi-line strings in YAML, either for readability or for output. There are a lot of ways to write multiline strings in YAML. Examples of the two most common ones are shown here:

Glob patterns

Metalsmith and its plugins make extensive use of glob patterns to target specific files (usually through the pattern option). A glob is a type of string pattern syntax that is commonly and conveniently used to match files by path with support for globstar wildcards *. Chances are you are already using glob patterns in .gitignore files, with the Linux/Mac or git terminal commands. Here are a few examples of how you can match with glob patterns:

You can always use DigitalOcean's handy Glob tool or globster.xyz to test your glob patterns.

The plugin chain

We believe that understanding the internal representation of files as JavaScript objects is really key to fully grasp the concept of Metalsmith. To understand this better, we follow the evolution of a file at each step of the build process (between each use statement). We are also using the writemetadata() plugin, which writes the {key: value} pairs excerpted from the File objects representing the files, to the filesystem as .json files. You can then view the .json files to find out how files are represented internally in Metalsmith.

metalsmith.js
Metalsmith(__dirname)            
  .source('src')      
  .destination('build')   
  .use(markdown())          
  .use(layouts())
  .use(writemetadata({            // write the JS object
    pattern: ['**/*'],            // for each file into .json
    ignorekeys: ['next', 'previous'],
    bufferencoding: 'utf8'        // also put 'content' into .json
  }))
  .build(function(err) {         
    if (err) throw err;          
  });
metalsmith.json
{
  "source": "src",
  "destination": "build",
  "plugins": [
    { "markdown": true },
    { "layouts": true },
    { "writemetadata": {
      "pattern": ["**/*"],
      "ignorekeys": ["next", "previous"],
      "bufferencoding": "utf8"
    } }
  ]
}

In the example above, after applying .use(markdown()) the initial representation of my-file.md becomes my-file.html. The markdown plugin changes the file extension and converts the contents to HTML.

{
  'relative_to_sourcepath/my-file.html': {
    title: 'A Catchy Title',
    draft: false,
    contents: '<p>An unfinished article...</p>',
    ...
  }
}

After applying .use(permalinks()) the file is renamed to original-name/index.html and a path property is added to the file's metadata:

{
  'relative_to_sourcepath/my-file/index.html': {
    title: 'A Catchy Title',
    draft: false,
    contents: '<p>An unfinished article...</p>',
    path: 'myfile',
    ...
  }
}

Assuming we have defined a very simple nunjucks layout file in a separate layouts folder...

./layouts/layout.njk
<!doctype html>
<html>
<head>
  <title>{{ title }}</title>
</head>
<body>
  {{ contents | safe }}
</body>
</html>

... after applying .use(layouts()) in our Metalsmith chain our JavaScript object becomes:

{
  'relative_to_sourcepath/my-file/index.html': {
    title: 'A Catchy Title',
    draft: false,
    contents: `<!doctype html><html><head>
               <title>A Catchy Title</title></head><body>
               <p>An unfinished article...</p>
               </body></html>`,
    path: 'myfile',
    ...      
  }
}

Finally when the .build(function(err)) is performed our JavaScript object is written to relative_to_destpath/myfile/index.html.

Quickstart

You want to build a website or blog with a static site generator. Well, here is our elevator pitch. It's as easy as that:

metalsmith.mjs
import { fileURLToPath } from 'node:url'
import { dirname } from 'path'
import Metalsmith from 'metalsmith'
import collections from '@metalsmith/collections'
import layouts from '@metalsmith/layouts'
import markdown from '@metalsmith/markdown'
import permalinks from '@metalsmith/permalinks'

const __dirname = dirname(fileURLToPath(import.meta.url))
const t1 = performance.now()

Metalsmith(__dirname)         // parent directory of this file
  .source('./src')            // source directory
  .destination('./build')     // destination directory
  .clean(true)                // clean destination before
  .env({                      // pass NODE_ENV & other environment variables
    DEBUG: process.env.DEBUG,
    NODE_ENV: process.env.NODE_ENV
  })           
  .metadata({                 // add any variable you want & use them in layout-files
    sitename: "My Static Site & Blog",
    siteurl: "https://example.com/",
    description: "It's about saying »Hello« to the world.",
    generatorname: "Metalsmith",
    generatorurl: "https://metalsmith.io/"
  })
  .use(collections({          // group all blog posts by internally
    posts: 'posts/*.md'       // adding key 'collections':'posts'
  }))                         // use `collections.posts` in layouts
  .use(markdown())            // transpile all md into html
  .use(permalinks())          // change URLs to permalink URLs))
  .use(layouts({              // wrap layouts around html
    pattern: '**/*.html'
  }))
  .build((err) => {           // build process
    if (err) throw err        // error handling is required
    console.log(`Build success in ${((performance.now() - t1) / 1000).toFixed(1)}s`)
  });
metalsmith.cjs
const Metalsmith  = require('metalsmith')
const collections = require('@metalsmith/collections')
const layouts     = require('@metalsmith/layouts')
const markdown    = require('@metalsmith/markdown')
const permalinks  = require('@metalsmith/permalinks')

const t1 = performance.now()

Metalsmith(__dirname)         // parent directory of this file
  .source('./src')            // source directory
  .destination('./build')     // destination directory
  .clean(true)                // clean destination before
  .env({                      // pass NODE_ENV & other environment variables
    DEBUG: process.env.DEBUG,
    NODE_ENV: process.env.NODE_ENV
  })           
  .metadata({                 // add any variable you want & use them in layout-files
    sitename: "My Static Site & Blog",
    siteurl: "https://example.com/",
    description: "It's about saying »Hello« to the world.",
    generatorname: "Metalsmith",
    generatorurl: "https://metalsmith.io/"
  })
  .use(collections({          // group all blog posts by internally
    posts: 'posts/*.md'       // adding key 'collections':'posts'
  }))                         // use `collections.posts` in layouts
  .use(markdown())            // transpile all md into html
  .use(permalinks())          // change URLs to permalink URLs))
  .use(layouts({              // wrap layouts around html
    pattern: '**/*.html'
  }))
  .build((err) => {           // build process
    if (err) throw err        // error handling is required
    console.log(`Build success in ${((performance.now() - t1) / 1000).toFixed(1)}s`)
  })
metalsmith.json
{
  "source": "src",
  "destination": "build",
  "clean": true,
  "env": {
    "DEBUG": "$DEBUG",
    "NODE_ENV": "$NODE_ENV"
  },
  "metadata": {
    "sitename": "My Static Site & Blog",
    "siteurl": "https://example.com/",
    "description": "It's about saying »Hello« to the world.",
    "generatorname": "Metalsmith",
    "generatorurl": "https://metalsmith.io/"
  },
  "plugins": [
    { "@metalsmith/collections": { "posts": "posts/*.md" }},
    { "@metalsmith/markdown": {}},
    { "@metalsmith/permalinks": {}},
    { "@metalsmith/layouts": { "pattern": "**/*.html" }},
  ]
}

Directory structure

A typical directory structure for a metalsmith (static-site) project looks more or less like this:

repo
├── metalsmith.js
├── package.json
├── node_modules
├── build
├── layouts
│   ├── default.hbs
│   └── post.hbs
├── lib
│   └── sass
│   │   └── style.scss
│   ├── data
│   │   └── nav.json
│   └── plugins
│       └── local-metalsmith-plugin.js
└── src
    ├── about.md
    ├── index.md
    └── posts
        ├── first-post.md
        └── second-post.md

where:

...but Metalsmith gives you total freedom about how you want to structure your project, so feel free to restructure things as you see fit.

Starter projects

The community has built a few interesting starter projects:

There is also a one-click Netlify CMS starter.

Github search for other metalsmith starters