gray-matter
gray-matter splits a string into its front matter and its body. Give it a markdown file that starts with a fenced block of YAML and you get back an object with data (the parsed keys), content (everything after the block), plus extras such as excerpt, isEmpty and the raw matter string for debugging. YAML is the default, JSON and JavaScript engines ship with it, and you can register your own for TOML or anything else. It also goes the other way with stringify, which is what makes it useful for scripts that rewrite front matter rather than only reading it. This is the parser most static site generators reach for.
The default front matter parser for good reasons: a small API, honest edge-case handling, and every static site tool already depends on it. Just treat it as frozen software, and keep it away from content you did not write.
Use it if
- You are building anything that reads markdown with YAML front matter, which is the case it handles well and has handled for a decade
- You need to write front matter back out, for example a script that adds a field across a content directory, since stringify round-trips
- Your content uses something other than YAML, or non-standard delimiters, both of which are configurable through engines and delimiters
- You want an excerpt extracted in the same pass, either up to the next delimiter or through a separator such as an HTML comment
- You parse files you do not control: the built-in javascript engine runs eval, and language detection picks the engine from the text after the opening delimiter, so untrusted content can select it
- You want a current dependency tree, because it is pinned to js-yaml 3 through the removed safeLoad and safeDump calls and cannot move to js-yaml 4 without changes upstream
- You need ESM: the package is CommonJS with no exports map and no module build, so native ESM consumers rely on interop
- You expect maintenance: the last publish to npm was 4.0.3 in April 2021 and 54 items sit open on the tracker
- You are in the unified or remark ecosystem already, where vfile-matter does the same job against the vfile you are passing around anyway
Setup reality
Install and call it, there is nothing to configure. The behaviour worth knowing before you ship is the module-level cache: results are stored in matter.cache keyed by the entire content string whenever you call it without options, so a build that parses ten thousand files keeps ten thousand file bodies in memory until you call matter.clearCache(). Worse, a cache hit returns a shallow copy, meaning file.data is the same object every time, so mutating it poisons every later parse of that string. Two smaller traps: matter.test only checks that the string starts with the delimiter, so a document that opens with a horizontal rule reads as having front matter, and matter.read is synchronous readFileSync, which will not thread through an async pipeline.
Patterns
Split front matter from contentparse-string
const matter = require('gray-matter');
const { data, content } = matter(
'---\ntitle: Home\n---\nOther stuff'
);
// data => { title: 'Home' }
// content => 'Other stuff'A file with no front matter still returns successfully, with data set to an empty object rather than throwing.
Read and parse a file from diskread-file
const file = matter.read('./content/blog-post.md');
console.log(file.path, file.data.title);This is readFileSync under the hood, so use fs.promises.readFile plus matter(str) when you need it async.
Import it from ESM or TypeScriptesm-typescript
import matter from 'gray-matter';
const file = matter(source);The package is CommonJS only, so this depends on your bundler or Node's interop; the README's TypeScript form is import matter = require('gray-matter').
Check whether a string has front mattertest-for-matter
if (matter.test(source)) {
const { data } = matter(source);
}It only checks the opening delimiter, so a document starting with a --- horizontal rule returns true; use file.isEmpty on the parsed result to be sure.
Write front matter back outstringify
const updated = matter.stringify('foo bar baz', { title: 'Home' });
// ---
// title: Home
// ---
// foo bar bazThe YAML is regenerated by js-yaml, so key order, quoting and comments from the original block are not preserved.
Add a field to an existing fileupdate-in-place
const fs = require('fs');
const file = matter.read('./post.md');
file.data.updated = new Date().toISOString();
fs.writeFileSync('./post.md', matter.stringify(file.content, file.data));Mutating file.data on a cached parse also mutates what later calls see, so clear the cache or pass an options object in bulk scripts.
Pull an excerpt out in the same passexcerpt
const file = matter(source, { excerpt: true });
console.log(file.excerpt);With excerpt: true the excerpt runs up to the next delimiter, and it stays part of file.content rather than being removed from it.
Use a custom excerpt separatorexcerpt-separator
const file = matter(source, { excerpt_separator: '<!-- more -->' });The separator convention most CMS content already uses, which saves rewriting your files to match the parser.
Parse non-standard delimiterscustom-delimiters
const file = matter.read('file.md', { delimiters: '~~~' });
// or an explicit open and close pair:
const other = matter(source, { delimiters: ['<!--meta', 'meta-->'] });delims is the deprecated spelling of the same option and still works; delimiters is the documented name.
Register a TOML enginetoml-engine
const TOML = require('@iarna/toml');
const file = matter(source, {
engines: {
toml: {
parse: TOML.parse.bind(TOML),
stringify: TOML.stringify.bind(TOML),
},
},
language: 'toml',
});An engine can be a bare function when you only parse; supply stringify too or matter.stringify throws for that language.
Let the file declare its own languagelanguage-detection
const source = '---toml\ntitle = "TOML"\n---\nbody';
const file = matter(source, { engines: { toml: TOML.parse.bind(TOML) } });
console.log(file.language); // 'toml'Detection reads whatever follows the opening delimiter, which is also how a hostile file reaches the eval-based javascript engine; pin options.language when the input is untrusted.
Stop the cache from growing without limitclear-cache
for (const filepath of files) {
const file = matter.read(filepath);
index.push({ path: filepath, ...file.data });
}
matter.clearCache();Caching only happens when no options object is passed, so a long-running process that always passes options never accumulates; one that does not, does.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| front-matter | npm | You want a smaller YAML-only parser with no eval-capable engine and no internal cache |
| vfile-matter | npm | You already use unified, remark or another vfile-based pipeline and want the matter attached to the file you are passing along |
| yaml | npm | You would rather split on the delimiters yourself and use a maintained YAML parser with source positions and comment preservation |