- 开发工具
- CLI
- 前端
【免费下载链接】metalsmith
An extremely simple, pluggable static site generator for Node.js
导读
本文以 examples/jekyll 示例仓库中一篇典型博文 2012-09-28-third-post.md 为切入点,完整剖析 Metalsmith 静态站点生成器如何读取带 YAML Front-Matter 的 Markdown 文件,经由插件链(草稿过滤、Markdown 渲染、摘要提取、集合分组、永久链接、布局套用)最终产出 HTML 站点的全过程。读完本文,你将掌握:Metalsmith 中 Front-Matter 的解析规则与数据结构、metalsmith.json的插件链编排方式、Jekyll 风格博客目录结构与 Swig 模板的对应关系,以及如何用 CLI 一键构建并本地预览一个静态博客。
一、源文件解剖:一篇 Jekyll 风格博文的构成
仓库中 examples/jekyll/_posts/2012-09-28-third-post.md 是一篇结构极其典型的 Jekyll 风格博文,全文如下:
--- title: Third Post date: 2012-09-28 layout: post.swig --- A slightly late, less interesting piece of prose.与 2012-08-20-first-post.md、2012-08-23-second-post.md、2012-12-07-fourth-post.md 等文章保持一致,这篇博文包含两个部分:
- YAML Front-Matter:由
---分隔符包裹的元数据块,声明了title、date、layout三个字段; - 正文:分隔符之后的一段普通文本(无 Markdown 标题、列表等复杂语法)。
这里的layout: post.swig字段表明,该文件构建后应套用 examples/jekyll/_layouts/post.swig 这个 Swig 模板;date: 2012-09-28则与文件名中的日期前缀(2012-09-28-)呼应——Jekyll 惯例是用文件名承载日期,而 Metalsmith 允许你在 Front-Matter 中显式声明,二者可灵活互换。
为什么说这篇“普通”博文是理解流水线的理想样本
- 元数据字段精简:恰好覆盖
title(标题)、date(日期)、layout(模板选择)这三个博客场景最核心的变量,无多余干扰; - 正文不含 Markdown 语法:渲染前后对比一目了然——正文就是一段
<p>文本,便于观察 Markdown 插件与布局模板各自做了什么; - 位于
_posts目录:_posts前缀下划线、集合分组、永久链接等后续环节都以它所在目录为作用范围。
二、Metalsmith 如何解析 Front-Matter:底层实现与数据结构
2.1 解析入口:matter.parse
Metalsmith 对 Front-Matter 的解析封装在 lib/matter.js 的Matter.prototype.parse中,底层使用gray-matter库:
Matter.prototype.parse = function (fileBuffer) { const result = {} const parsed = matter(fileBuffer.toString(), this[_options]) Object.assign(result, parsed.data) // ... result.contents = Buffer.from(parsed.content) return result }也就是说:Front-Matter 中的每个键值对被平铺到文件对象的顶层。上述third-post.md解析后会变成这样一个对象(与 README.md 中描述的形态一致):
{ '2012-09-28-third-post.md': { title: 'Third Post', date: new Date(2012, 8, 28), // gray-matter 会把 YAML 日期转为 Date 对象 layout: 'post.swig', contents: Buffer.from('A slightly late, less interesting piece of prose.'), stats: fs.Stats // 由 Metalsmith.readFile 附加 } }在 lib/index.js 的Metalsmith.prototype.readFile中,读取流程是先stat+readFile,再调用this.matter.parse(buffer),并把mode(八进制权限字符串)与stats一并附加到文件对象上。若 Front-Matter 语法非法,会抛出code: 'invalid_frontmatter'的错误并被包装为failed_read。
2.2 可配置的解析选项
Front-Matter 解析默认开启,可通过Metalsmith#frontmatter方法关闭或定制(见 lib/index.js):
metalsmith.frontmatter(false) // 关闭解析,contents 直接保留原始 Buffer metalsmith.frontmatter({ excerpt: true }) // 开启摘要提取 metalsmith.frontmatter({ delimiters: ['+++', '°°°'] }) // 自定义分隔符默认选项定义在 lib/matter.js:
| 选项 | 默认值 | 说明 |
|---|---|---|
excerpt | false | 是否从正文提取摘要(配合excerpt_separator) |
excerpt_separator | --- | 摘要分隔符,正文中第一次出现该分隔符之前的内容视为摘要 |
language | yaml | Front-Matter 使用的语言(可改为js、json、toml等) |
delimiters | --- | 分隔符;可传双元素数组实现“开/闭”分隔符不同 |
test/fixtures/read-frontmatter/下的用例验证了多种形态:YAML(index.md)、JSON Front-Matter(json.md),以及 read-frontmatter-custom/src/index.md 中~~~自定义分隔符 +excerpt提取的形态;对应断言位于 test/index.js。metalsmith.json中同样支持frontmatter键(见 README.md)。
2.3 提取摘要的“加分项”
示例流水线中使用了metalsmith-excerpts插件:它会从每篇博文中提取摘要,写入file.excerpt字段。即便这篇博文的 Front-Matter 没有声明excerpt,构建后索引页仍能展示其第一段文字作为列表摘要——这正是 examples/jekyll/_site/index.html 中每个<h3>下方<p>的内容来源。
三、完整构建流水线:metalsmith.json插件链逐层拆解
本示例的“元配置”是 examples/jekyll/metalsmith.json。Metalsmith CLI 会读取该文件并按顺序执行插件。整体流程如下:
_posts/*.md │ ▼ ① @metalsmith/drafts 删除声明了 draft: true 的文件(本示例未标记) ▼ ② @metalsmith/markdown 把 Markdown 正文渲染为 HTML ▼ ③ metalsmith-excerpts 提取摘要到 file.excerpt ▼ ④ metalsmith-collections 按 pattern 收集成 metadata.posts ▼ ⑤ @metalsmith/permalinks 把 URL 改写为 :title 形式(third-post) ▼ ⑥ metalsmith-layouts 按 file.layout 套用 _layouts 下的 Swig 模板 ▼ _site/third-post/index.html3.1 配置原文与逐项说明
{ "source": "./_posts", "destination": "./_site", "metadata": { "site_title": "My Jekyll-Powered Blog", "site_description": "My second, super-cool, Jekyll-powered blog." }, "plugins": [ { "@metalsmith/drafts": {} }, { "@metalsmith/markdown": {} }, { "metalsmith-excerpts": true }, { "metalsmith-collections": { "posts": { "pattern": "*[!index].html" } } }, { "@metalsmith/permalinks": { "pattern": ":title" } }, { "metalsmith-layouts": { "directory": "_layouts" } } ] }各键位作用:
| 键 | 作用 |
|---|---|
source | 源目录(这里直接指向_posts,跳过仓库根目录的其它文件) |
destination | 构建产物输出目录 |
metadata | 注入到所有文件的全局元数据,可在模板中通过site_title、site_description访问 |
plugins | 按序执行的插件列表;对象形式支持传参,布尔值true表示使用默认参数 |
3.2 插件链对这篇博文的具体影响
- ① drafts 过滤:
@metalsmith/drafts会删除draft: true的文件。本示例所有文章均未标记 draft,全部保留;其最小实现可见 examples/drafts-plugin/index.js(遍历文件字典并删除带draft属性的条目)。 - ② Markdown 渲染:正文
A slightly late, less interesting piece of prose.被渲染为<p>A slightly late, less interesting piece of prose.</p>。 - ③ 摘要提取:
metalsmith-excerpts取正文第一段写入excerpt,供索引页列表展示。 - ④ 集合分组:
metalsmith-collections以pattern: "*[!index].html"收集所有非index的 HTML 文件,在元数据中生成posts数组。注意该模式匹配的是Markdown 渲染后的.html文件,因此四篇博文都会进入posts,而index.md(渲染为index.html)被排除。 - ⑤ 永久链接:
@metalsmith/permalinks按:title模式改写路径。2012-09-28-third-post.md→third-post/index.html,URL 即为/third-post(日期前缀被去除,纯title作为目录)。 - ⑥ 布局套用:
metalsmith-layouts读取directory: "_layouts"下的模板,根据file.layout = "post.swig"选择模板包裹渲染后的 HTML。
3.3 模板如何消费元数据
examples/jekyll/_layouts/post.swig 演示了元数据的消费方式:
<title>{{ title }} | {{ site_title }}</title> <h1>{{ title }}</h1> <time>{{ date | date('Y-m-d') }}</time> {{ contents | safe }}{{ title }}、{{ date }}:来自博文自身的 Front-Matter;{{ site_title }}:来自metalsmith.json的全局metadata;{{ contents | safe }}:渲染后的 HTML 正文(必须用safe防止被转义);{{ date | date('Y-m-d') }}:由jstransformer-swig提供的日期格式化过滤器,把 Date 对象输出为2012-09-28。
而 examples/jekyll/_layouts/index.swig 则遍历集合:
{% for post in posts %} <h3><a href="{{ post.path }}">{{ post.title | safe }}</a></h3> <p>{{ post.excerpt | striptags | safe }}</p> {% endfor %}post.path即永久链接后的路径(如third-post),post.excerpt是 ③ 提取的摘要,striptags用于去掉 HTML 标签后展示纯文本摘要。
四、构建结果:从源码到站点的可验证对照
执行构建后(详见下一节),examples/jekyll/_site/third-post/index.html 的内容如下:
<html> <head> <title>Third Post | My Jekyll-Powered Blog</title> ... <meta name="description" content="My second, super-cool, Jekyll-powered blog."> <meta name="generator" content="metalsmith"> </head> <body> <h1>Third Post</h1> <time>2012-09-28</time> <p>A slightly late, less interesting piece of prose.</p> </body> </html>而 examples/jekyll/_site/index.html 则列出了全部四篇博文,其中对本文主角的呈现为:
<h3><a href="third-post">Third Post</a></h3> <p>A slightly late, less interesting piece of prose.</p>对照关系清晰可见:
| 源文件元素 | 构建后位置 |
|---|---|
Front-Mattertitle: Third Post | <title>与<h1> |
Front-Matterdate: 2012-09-28 | <time>2012-09-28</time> |
Front-Matterlayout: post.swig | 决定使用_layouts/post.swig包裹 |
| 正文段落 | 永久链接页面中的<p>,以及索引页的摘要<p> |
metalsmith.json的metadata | 全站<meta>、站点标题与描述 |
这也印证了 README.md 描述的 Metalsmith 三步模型:读取源目录 → 插件链逐层改写文件 → 写入目标目录。
五、运行与验证:一条命令从零构建
examples/jekyll/README.md 给出了最简运行方式。在 examples/jekyll 目录下执行:
npm install # 安装 metalsmith 及全部示例插件 npm start # 等价于执行 CLI 命令: metalsmithnpm start的脚本定义在 examples/jekyll/package.json 中,即"start": "metalsmith"。CLI 会自动读取当前目录的metalsmith.json,输出大致如下(参考 README.md 的 CLI 行为描述):
Metalsmith · reading configuration from: /path/to/metalsmith.json Metalsmith · successfully built to: /path/to/_site构建完成后,用内置静态服务器预览:
npm run serve # 即 "serve _site",默认端口 3000然后打开浏览器访问http://localhost:3000,即可看到索引页与third-post等永久链接页面。
5.1 用 JavaScript API 替代 CLI(可选)
如果你偏好代码而非 JSON 配置,可以改用 README.md 的 Quickstart 模式,把 examples/jekyll/metalsmith.json 的插件链翻译为链式调用:
import Metalsmith from 'metalsmith' import drafts from '@metalsmith/drafts' import markdown from '@metalsmith/markdown' import excerpts from '@metalsmith/excerpts' import collections from '@metalsmith/collections' import permalinks from '@metalsmith/permalinks' import layouts from '@metalsmith/layouts' Metalsmith(__dirname) .source('./_posts') .destination('./_site') .metadata({ site_title: 'My Jekyll-Powered Blog', site_description: 'My second, super-cool, Jekyll-powered blog.' }) .use(drafts()) .use(markdown()) .use(excerpts()) .use(collections({ posts: { pattern: '*[!index].html' } })) .use(permalinks({ pattern: ':title' })) .use(layouts({ directory: '_layouts' })) .build((err) => { if (err) throw err console.log('Build finished!') })CLI 与 JS API 两条路径等价:前者适合配置驱动、与 Jekyll 风格更接近;后者适合在构建前后插入自定义逻辑。
六、要点总结
- Front-Matter 即元数据:
---块内的 YAML 键值会被平铺到文件对象顶层,contents保留正文;解析逻辑在 lib/matter.js,默认开启,可用frontmatter(false)关闭,支持自定义分隔符、语言与摘要提取。 - 插件链决定输出形态:Metalsmith 的核心哲学是“一切皆插件”——本示例仅用 6 个插件就复刻了 Jekyll 的草稿过滤、Markdown 渲染、摘要、集合、永久链接、模板布局全套能力(插件清单见 examples/jekyll/package.json)。
- 模板与元数据联动:
layout字段驱动模板选择,metadata提供全站变量,posts集合与excerpt支撑索引页列表。 - 可验证性:仓库已预置构建产物 examples/jekyll/_site,即使不运行命令,也能通过对比源文件与输出 HTML 理解每个插件的作用。
若想继续深入,可对照阅读 examples/static-site(Handlebars 布局版)、examples/wintersmith(Pug 模板版)等示例,以及 README.md 的完整 API 与 CLI 说明——它们都遵循本文展示的同一套“读取 → 插件链 → 写入”模型。
- 开发工具
- CLI
- 前端
【免费下载链接】metalsmith
An extremely simple, pluggable static site generator for Node.js
相关推荐
Metalsmith 仿 Jekyll 构建静态博客:从一篇带 frontmatter 的 Markdown 文章说起
Metalsmith 仿 Jekyll 构建静态博客:从一篇带 frontmatter 的 Markdown 文章说起 本篇以 Metalsmith 仓库自带的
开发工具CLI前端用 Metalsmith 复刻 Jekyll 静态博客:examples/jekyll 完整实战指南
用 Metalsmith 复刻 Jekyll 静态博客:examples/jekyll 完整实战指南 本指南以仓库中的 examples/jekyll http
开发工具CLI前端Metalsmith 中 Markdown 文件 YAML Front Matter 的解析与实战:以 wintersmith 示例博文为例
Metalsmith 中 Markdown 文件 YAML Front Matter 的解析与实战:以 wintersmith 示例博文为例 在 Metalsm
开发工具CLI前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考