gray-matter 使用指南

gray-matter 的安装、API 使用、摘要提取、工程分层结构和完整代码示例。

#type / howto #status / evergreen #tech / javascript #package / gray-matter

[!info] related notes

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 示例等。这是它相比自己写正则的核心优势。

创建于 2026/6/28 更新于 2026/7/15