gray-matterでfrontmatterをパース — Markdownのメタデータ管理
このブログでも使っている gray-matter は、Markdownファイルの先頭に書くYAMLヘッダー(frontmatter)をJavaScriptオブジェクトとして取り出すライブラリだ。Next.jsの静的ブログを作る際に必ずと言っていいほど登場するので、基本的な使い方から型安全な活用法まで整理する。
frontmatterとは
Markdownファイルの先頭に --- で囲んで書くYAML形式のメタデータ領域を「frontmatter」と呼ぶ。
---
title: "記事タイトル"
date: "2026-02-24"
tags: ["Next.js", "TypeScript"]
---
ここから本文が始まる。
--- より前の部分が frontmatter、それ以降が Markdown 本文だ。gray-matter はこの2つを分離して返してくれる。
インストール
npm install gray-matter
TypeScript で使う場合、型定義は本体に同梱されているので @types/gray-matter は不要だ。
基本的な使い方
import fs from "fs";
import matter from "gray-matter";
const fileContents = fs.readFileSync("posts/hello.md", "utf-8");
const { data, content } = matter(fileContents);
console.log(data); // frontmatter オブジェクト
console.log(content); // Markdown 本文(frontmatter を除いた部分)
matter() の戻り値には主に以下のプロパティが含まれる。
| プロパティ | 内容 |
|---|---|
data | frontmatterをパースしたオブジェクト |
content | frontmatterを除いたMarkdown本文 |
orig | 元のファイル内容(Buffer) |
TypeScriptで型安全に使う
data の型はデフォルトで { [key: string]: any } になる。TypeScriptで型安全に扱うには、frontmatterの形を型として定義し、キャストする。
import fs from "fs";
import matter from "gray-matter";
type PostFrontmatter = {
title: string;
description?: string; // 省略可能
date: string;
image: string | null;
tags: string[] | null;
};
const fileContents = fs.readFileSync(filePath, "utf-8");
const { data, content } = matter(fileContents) as matter.GrayMatterFile<string> & {
data: PostFrontmatter;
};
// data.title は string として型推論される
// data.description は string | undefined
matter.GrayMatterFile<string> に data のみ上書きするインターセクション型を使うのがポイントだ。matter() の戻り値全体を捨てずに済む。
実プロジェクトでの実装例
このブログの src/app/lib/functions.ts では、posts/ ディレクトリのMarkdownファイルを一括で読み込んでいる。
// src/app/lib/functions.ts(抜粋)
import fs from "fs";
import path from "path";
import matter from "gray-matter";
type PostFrontmatter = {
title: string;
description?: string;
date: string;
image: string | null;
tags: string[] | null;
};
const getPostData = async (): Promise<PostItem[]> => {
const postsDirectory = path.join(process.cwd(), "posts");
const filenames = fs.readdirSync(postsDirectory);
const posts = filenames
.map((filename) => {
const filePath = path.join(postsDirectory, filename);
const fileContents = fs.readFileSync(filePath, "utf-8");
const { data } = matter(fileContents) as matter.GrayMatterFile<string> & {
data: PostFrontmatter;
};
return {
slug: filename.replace(/\.md$/, ""),
title: data.title,
description: data.description,
date: data.date,
image: data.image,
tags: data.tags || [],
contentHtml: "",
};
})
.sort((postA, postB) =>
new Date(postA.date) > new Date(postB.date) ? -1 : 1
);
return posts;
};
記事一覧取得では content(本文)は不要なので分割代入で data だけ取り出している。日付降順ソートも data.date をそのまま Date コンストラクタに渡すだけでシンプルに書ける。
記事詳細ページ:本文も使う場合
個別記事ページでは content もパースして HTML に変換する。
// src/app/posts/[slug]/page.tsx(抜粋)
import matter from "gray-matter";
import { unified } from "unified";
import remarkParse from "remark-parse";
import remarkRehype from "remark-rehype";
import rehypeStringify from "rehype-stringify";
async function createPostData(slug: string) {
const filePath = path.join(process.cwd(), "posts", `${slug}.md`);
const fileContents = fs.readFileSync(filePath, "utf8");
const { data, content } = matter(fileContents);
// content を remark → rehype → HTML に変換
const processedContent = await unified()
.use(remarkParse)
.use(remarkRehype)
.use(rehypeStringify)
.process(content);
return {
title: data.title,
date: data.date,
contentHtml: processedContent.toString(),
};
}
matter() で本文と frontmatter を分離してから、content だけを Markdown パイプラインに流すのが基本的な流れだ。
RSSフィード生成でも同じパターン
RSS フィード生成スクリプト(tool/generate-feed.mjs)でも同じように使える。ESM 形式でも import matter from "gray-matter" で問題なく動く。
// tool/generate-feed.mjs(抜粋)
import matter from "gray-matter";
function getPostData() {
const filenames = fs.readdirSync(postsDirectory);
return filenames.map((filename) => {
const fileContents = fs.readFileSync(filePath, "utf-8");
const { data } = matter(fileContents);
return {
title: data.title,
description: data.description || "",
date: data.date,
tags: data.tags || [],
};
});
}
frontmatterの設計指針
このブログのフロントマター設計をもとに、実用的な設計のポイントをまとめる。
必須フィールドは最小限に
title と date だけを必須とし、あとは省略可能にすることで記事作成のハードルを下げられる。
---
title: "記事タイトル" # 必須
date: "2026-02-24" # 必須
---
description は省略可能にして自動補完
description がない場合、本文の冒頭から自動生成する generateExcerpt 関数を用意しておくと、毎回書く手間が省ける。
const description: string = data.description ?? generateExcerpt(content);
null許容フィールドにはデフォルト値を
tags が null のままだと .map() でエラーになるので、読み込み時に空配列に変換しておく。
tags: data.tags || [],
ハマりやすいポイント
YAML の配列記法
frontmatter の配列は以下の2通りどちらでも書けるが、gray-matter は両方正しくパースしてくれる。
# インライン記法
tags: ["Next.js", "TypeScript"]
# ブロック記法
tags:
- Next.js
- TypeScript
日付はダブルクォートで囲む
date: 2026-02-24 と書くと、YAML パーサーが Date オブジェクトとして解釈してしまう場合がある。文字列として扱いたいなら必ずクォートする。
date: "2026-02-24" # 文字列として扱われる
date: 2026-02-24 # Date オブジェクトになることがある
content にはfrontmatterが含まれない
matter() が返す content には frontmatter 部分(--- で囲まれた領域)は含まれない。独自に --- を除去する処理を書く必要はない。
関連ツールとの比較
frontmatter をパースするライブラリは gray-matter 以外にもある。
| ライブラリ | 特徴 | 向いているケース |
|---|---|---|
gray-matter | YAML/JSON/TOML/CoffeeScript対応。デリミタカスタマイズ可。型定義内蔵 | Next.js・Gatsby・Astroなどフレームワーク問わずに使いたいとき |
front-matter | YAML専用、非常にシンプル | 小規模スクリプトや YAML しか使わない場合 |
remark-frontmatter | remarkパイプラインに統合できる | unifiedパイプラインの中でfrontmatterを処理したいとき |
Next.js や Astro の静的ブログなら gray-matter が事実上の標準で、エコシステムのサンプルコードも最も多い。remark-frontmatter はパイプライン内でfrontmatterをAST上で扱えるが、単純にメタデータをオブジェクトとして取り出したいだけなら gray-matter の方がシンプルだ。
まとめ
gray-matterはmatter(fileContents)の1行で frontmatter と本文を分離できる- TypeScript では
matter.GrayMatterFile<string> & { data: YourType }でキャストして型安全にする descriptionの自動補完やtags || []のデフォルト値など、読み込み時に欠損を吸収しておくのが実装を楽にするコツ- 日付フィールドは必ずクォートして文字列として扱う
- シンプルなメタデータ取得なら gray-matter、パイプライン統合なら remark-frontmatter と使い分ける