☰
用 Metalsmith 复刻 Jekyll 博客:从一篇带 YAML Front-Matter 的 Markdown 文章看完整构建流水线
2026/10/15 16:48:20 网站建设 项目流程
  • 开发工具
  • CLI
  • 前端

【免费下载链接】metalsmith

An extremely simple, pluggable static site generator for Node.js

项目地址:https://gitcode.com/gh_mirrors/me/metalsmith
点击查看免费下载

导读

本文以 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 等文章保持一致,这篇博文包含两个部分:

  1. YAML Front-Matter:由---分隔符包裹的元数据块,声明了title、date、layout三个字段;
  2. 正文:分隔符之后的一段普通文本(无 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:

选项默认值说明
excerptfalse是否从正文提取摘要(配合excerpt_separator)
excerpt_separator---摘要分隔符,正文中第一次出现该分隔符之前的内容视为摘要
languageyamlFront-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.html

3.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 命令: metalsmith

npm 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

项目地址:https://gitcode.com/gh_mirrors/me/metalsmith
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询