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() の戻り値には主に以下のプロパティが含まれる。

プロパティ内容
datafrontmatterをパースしたオブジェクト
contentfrontmatterを除いた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の設計指針

このブログのフロントマター設計をもとに、実用的な設計のポイントをまとめる。

必須フィールドは最小限に

titledate だけを必須とし、あとは省略可能にすることで記事作成のハードルを下げられる。

---
title: "記事タイトル"     # 必須
date: "2026-02-24"       # 必須
---

description は省略可能にして自動補完

description がない場合、本文の冒頭から自動生成する generateExcerpt 関数を用意しておくと、毎回書く手間が省ける。

const description: string = data.description ?? generateExcerpt(content);

null許容フィールドにはデフォルト値を

tagsnull のままだと .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-matterYAML/JSON/TOML/CoffeeScript対応。デリミタカスタマイズ可。型定義内蔵Next.js・Gatsby・Astroなどフレームワーク問わずに使いたいとき
front-matterYAML専用、非常にシンプル小規模スクリプトや YAML しか使わない場合
remark-frontmatterremarkパイプラインに統合できるunifiedパイプラインの中でfrontmatterを処理したいとき

Next.js や Astro の静的ブログなら gray-matter が事実上の標準で、エコシステムのサンプルコードも最も多い。remark-frontmatter はパイプライン内でfrontmatterをAST上で扱えるが、単純にメタデータをオブジェクトとして取り出したいだけなら gray-matter の方がシンプルだ。

まとめ

  • gray-mattermatter(fileContents) の1行で frontmatter と本文を分離できる
  • TypeScript では matter.GrayMatterFile<string> & { data: YourType } でキャストして型安全にする
  • description の自動補完や tags || [] のデフォルト値など、読み込み時に欠損を吸収しておくのが実装を楽にするコツ
  • 日付フィールドは必ずクォートして文字列として扱う
  • シンプルなメタデータ取得なら gray-matter、パイプライン統合なら remark-frontmatter と使い分ける