gray-matter 使用指南
gray-matter 的安装、API 使用、摘要提取、工程分层结构和完整代码示例。
[!info] related notes
- 前置概念: gray-matter
- 生态位置: gray-matter 在内容管线中的位置
- 相关实践: 知识库批量更新指南
gray-matter 使用指南
目标
在 Node.js / TypeScript 项目中使用 gray-matter 解析和生成 Markdown front matter,包含安装、API 详解、摘要提取、工程分层结构和完整代码示例。
前置条件
- Node.js 18+ 项目
- pnpm / npm / yarn 任一包管理器
- TypeScript 项目建议开启
esModuleInterop
安装
pnpm add gray-matter
# 或
npm install --save gray-matter
TypeScript 类型已内置在包中,不需要额外安装 @types/gray-matter。
导入方式
ESM(推荐):
import matter from 'gray-matter'
如果 TypeScript / ESM 配置不支持默认导入:
import * as matter from 'gray-matter'
CommonJS:
const matter = require('gray-matter')
步骤一:matter(input, options) — 解析
最核心的 API。传入字符串,返回解析结果。
import matter from 'gray-matter'
const raw = `---
title: Hello
slug: home
tags:
- ai
- frontend
draft: false
created: "2026-06-28"
---
# Hello world
这里是正文内容。
`
const file = matter(raw)
console.log(file.data)
// { title: 'Hello', slug: 'home', tags: ['ai', 'frontend'], draft: false, created: '2026-06-28' }
console.log(file.content)
// "\n# Hello world\n\n这里是正文内容。\n"
options 配置项
const file = matter(raw, {
// 自定义分隔符,默认 '---'
delimiters: '---',
// 或指定 open/close
// delimiters: ['~~~', '~~~'],
// 启用摘要提取
excerpt: false,
// 或传入自定义函数
// excerpt: (file) => { file.excerpt = file.content.split('\n\n')[0] }
// 是否保留分隔符之间的原始换行
keep: false,
// 自定义语言解析器
engines: {
toml: tomlEngine,
json: jsonEngine,
},
})
步骤二:matter.read(filepath, options) — 读取文件
同步读取文件并解析:
const file = matter.read('./content/blog-post.md')
注意:这是同步 API,会阻塞事件循环。现代项目建议自己用 fs/promises 读文件:
import fs from 'node:fs/promises'
import matter from 'gray-matter'
const raw = await fs.readFile(filepath, 'utf8')
const file = matter(raw)
好处:
- 异步不阻塞
- 可以统一错误处理
- 可以批量并发读取(
Promise.all) - 可以和 glob 扫描器组合
步骤三:matter.stringify(content, data, options) — 生成
把正文和 front matter 合成 Markdown:
const output = matter.stringify('# Hello', {
title: 'Home',
tags: ['demo', 'markdown'],
})
console.log(output)
// ---
// title: Home
// tags:
// - demo
// - markdown
// ---
//
// # Hello
典型用法:读取 → 修改 → 写回
import fs from 'node:fs/promises'
import matter from 'gray-matter'
async function addStatus(filepath: string) {
const raw = await fs.readFile(filepath, 'utf8')
const file = matter(raw)
// 补默认值
file.data.status ??= 'draft'
// 生成新的 Markdown
const next = matter.stringify(file.content, file.data)
await fs.writeFile(filepath, next, 'utf8')
}
适合场景:
- 批量补 front matter 字段
- 批量修改 title
- 批量加 status
- 批量规范 tags
- 自动生成 slug
- 知识库 lint / fix 工具
步骤四:matter.test(string, options) — 检测
判断字符串是否有 front matter:
if (matter.test(raw)) {
console.log('has front matter')
} else {
console.log('no front matter')
}
适合在批量处理前做前置判断,跳过没有 front matter 的文件。
摘要 excerpt
自动提取
const raw = `---
title: Blog
---
这是摘要。
---
这是正文后续内容。
`
const file = matter(raw, { excerpt: true })
console.log(file.excerpt)
// 这是摘要。
excerpt: true 时,gray-matter 会用第二个 --- 作为摘要和正文的分隔符。
自定义提取函数
const file = matter(raw, {
excerpt(file) {
// 取正文前 3 行作为摘要
file.excerpt = file.content
.split('\n')
.filter(line => line.trim())
.slice(0, 3)
.join('\n')
},
})
工程建议
摘要生成不要强依赖 gray-matter 的 excerpt 功能。更优雅的策略:
frontmatter.description 有值 → 直接用
没有 description → 从正文前几段提取
需要更智能 → AI 异步总结
这样比在 front matter 里用 --- 分隔摘要更灵活。
步骤五:工程分层 — 和 Zod 组合
file.data 是用户手写的,本质上是不可信输入。推荐用 Zod 做 schema 校验:
import { z } from 'zod'
import matter from 'gray-matter'
const FrontmatterSchema = z.object({
title: z.string().min(1),
slug: z.string().optional(),
aliases: z.array(z.string()).default([]),
tags: z.array(z.string()).default([]),
type: z.enum(['note', 'moc', 'asset', 'project']).default('note'),
status: z.enum(['draft', 'evergreen', 'archived']).default('draft'),
created: z.string().optional(),
updated: z.string().optional(),
related: z.array(z.string()).default([]),
})
function parseMarkdownNote(raw: string) {
const file = matter(raw)
const frontmatter = FrontmatterSchema.parse(file.data)
return {
frontmatter,
content: file.content,
rawMatter: file.matter,
}
}
为什么需要这一步:
tags: frontend(字符串)→ Zod 会报错或转成[frontend]status: xxx(不在 enum 里)→ Zod 会报错title缺失 → Zod 会报错- 没有
tags字段 → Zod 自动补[]
步骤六:批量扫描和处理
结合 fast-glob 批量处理整个知识库:
import fs from 'node:fs/promises'
import path from 'node:path'
import fg from 'fast-glob'
import matter from 'gray-matter'
import { z } from 'zod'
const NoteSchema = z.object({
title: z.string().min(1),
slug: z.string().optional(),
tags: z.array(z.string()).default([]),
status: z.enum(['draft', 'published', 'archived']).default('draft'),
created: z.string().optional(),
updated: z.string().optional(),
})
type Note = {
filepath: string
meta: z.infer<typeof NoteSchema>
content: string
}
async function loadNotes(root: string): Promise<Note[]> {
const files = await fg('**/*.md', {
cwd: root,
absolute: true,
})
const notes: Note[] = []
for (const filepath of files) {
const raw = await fs.readFile(filepath, 'utf8')
const parsed = matter(raw)
const result = NoteSchema.safeParse(parsed.data)
if (!result.success) {
console.warn(`Invalid frontmatter: ${filepath}`)
console.warn(result.error.flatten())
continue
}
notes.push({
filepath,
meta: result.data,
content: parsed.content.trimStart(),
})
}
return notes
}
// 使用
const notes = await loadNotes('./z')
console.log(`Loaded ${notes.length} notes`)
也可以用 Promise.all 并发读取(文件多时更快):
async function loadNotesParallel(root: string): Promise<Note[]> {
const files = await fg('**/*.md', { cwd: root, absolute: true })
const results = await Promise.all(
files.map(async (filepath) => {
const raw = await fs.readFile(filepath, 'utf8')
const parsed = matter(raw)
const result = NoteSchema.safeParse(parsed.data)
if (!result.success) return null
return { filepath, meta: result.data, content: parsed.content.trimStart() }
}),
)
return results.filter((n): n is Note => n !== null)
}
验证
安装后写一个最小测试:
import matter from 'gray-matter'
const raw = `---
title: Test
tags: [a, b]
---
# Content
`
const file = matter(raw)
console.assert(file.data.title === 'Test')
console.assert(Array.isArray(file.data.tags))
console.assert(file.content.trim() === '# Content')
console.log('All assertions passed')
常见问题
Q: TypeScript 报错 default import not allowed
在 tsconfig.json 中开启 esModuleInterop: true,或改用:
import * as matter from 'gray-matter'
Q: file.data 里日期变成了 Date 对象
YAML 解析器会自动把 2026-06-28 这样的值转成 Date 对象。解决方案:
# 用引号包起来
created: "2026-06-28"
或者在 Zod schema 里做转换:
created: z.union([z.string(), z.date()]).transform(v =>
typeof v === 'string' ? v : v.toISOString().slice(0, 10)
)
Q: stringify 后格式变了
matter.stringify() 可能把 tags: [ai, frontend] 变成多行数组,也可能改变引号风格。这是 YAML 序列化器的默认行为,无法完全控制。建议:
- 批量修改前先
git diff检查 - 只对明确需要规范化的字段做修改
- 对于格式敏感的文件,考虑手动修改而非批量 stringify
Q: 正文里的代码块包含 --- 会误判吗
不会。gray-matter 不依赖正则,能正确处理正文代码块里出现的 ---、YAML 示例等。这是它相比自己写正则的核心优势。