gray-matter で Markdown の frontmatter をパース — TypeScript で型安全に使う

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(抜粋)
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 だけ取り出しています。

記事詳細ページで本文も使う場合

// 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 パイプラインに流すのが基本的な流れです。

frontmatter の設計指針

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

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

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

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

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

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

tagsnull のままだと .map() でエラーになるので、読み込み時に空配列に変換します。

tags: data.tags || [],

ハマりやすいポイント

日付はダブルクォートで囲む

date: "2026-02-24"  # 文字列として扱われる(推奨)
date: 2026-02-24    # Date オブジェクトになることがある

date: 2026-02-24 と書くと YAML パーサーが Date オブジェクトとして解釈してしまう場合があります。文字列として扱いたいなら必ずクォートします。

content には frontmatter が含まれない

matter() が返す content には frontmatter 部分(--- で囲まれた領域)は含まれません。独自に --- を除去する処理を書く必要はありません。

YAML の配列記法

# インライン記法
tags: ["Next.js", "TypeScript"]

# ブロック記法(どちらでも正しくパース)
tags:
  - Next.js
  - TypeScript

関連ツールとの比較

ライブラリ特徴向いているケース
gray-matterYAML/JSON/TOML/CoffeeScript 対応。型定義内蔵Next.js・Gatsby・Astro 問わず使いたいとき
front-matterYAML 専用、非常にシンプル小規模スクリプトや YAML しか使わない場合
remark-frontmatterremark パイプラインに統合できるunified パイプラインの中で frontmatter を処理したいとき

Next.js や Astro の静的ブログなら gray-matter が事実上の標準で、エコシステムのサンプルコードも最も多い選択肢です。

まとめ

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