🗂

コンテンツ管理を楽にする、クロスフレームワーク対応のMarkdown/MDXパーサーを作ってみた

に公開

こんにちは。

プロジェクトでの Markdown 管理、面倒に感じたことはありませんか?私はずっとフラストレーションを感じていました。まず最初の悩みは、どのライブラリを使うかですよね。

一方では、unified や remark、rehype のような「レゴブロック」的なソリューションがあります。これらは強力ですが、AST パイプライン全体を構築したりプラグインシステムを設定したりするのは、正直言って複雑すぎると感じていました。
もう一方では @next/mdx などがありますが、これらはページ単位の扱いに寄りすぎていて、クライアントサイドで動かないのがネックでした。

そこで以前は markdown-to-jsx や react-markdown を好んで使っていました。
DX(開発体験)が良く、クライアント・サーバー両方で動くし、軽量だからです。
ただ、これらは標準で HTML や MDX をサポートしていないため、結局プラグインの沼にハマります。さらに、i18n(i18next や next-intl など)と組み合わせようとすると、言語ごとの if/else ロジックでコードがぐちゃぐちゃになり、バンドルサイズも肥大化してしまいます。front-matter の扱いにもいくつか課題がありました。それに、つい最近までこれらは React 専用のソリューションでした。

そこで、Intlayer のために新しいものを作ることにしました。設定なしで「ただ動く」ものです。

ちなみに、開発にあたっては markdown-to-jsx v7.7.14 (by quantizor) をフォークし、simple-markdown v0.2.2 (by Khan Academy) をベースに構築しました。

このパーサーで目指したのは以下の点です:

  • 軽量なソリューション
  • フレームワークを問わない(React, Vue, Svelte, Angular, Solid, Preact 対応)
  • シンプルなセットアップ:複雑なプラグインチェーンは不要
  • SSR とクライアントサイドの両方をサポート
  • デザインシステムのコンポーネントを Provider レベルでマッピング可能
  • コンポーネント指向:アプリの各パーツごとに細かいレンダリング制御が可能
  • 型安全(front-matter を型付きオブジェクトとして取得、コンポーネントの Props も型安全)
  • i18n フレンドリー(多言語対応に最適化されたロード処理)
  • Zod スキーマによる front-matter のバリデーション

デモ:

単体のユーティリティとして使えます:

import { renderMarkdown } from "react-intlayer"; // Vueなら vue-intlayer、Svelteなら svelte-intlayer など

// シンプルなレンダリング関数(文字列ではなく JSX/ノードを返します)
renderMarkdown("### タイトル", {
  components: { h3: (props) => <h3 className="text-xl" {...props} /> },
});

コンポーネントやフック経由での利用:

import {
  MarkdownRenderer,
  MarkdownProvider,
  useMarkdownRenderer,
} from "react-intlayer";

// コンポーネントスタイル
<MarkdownRenderer components={{ h3: MyCustomH3 }}>
  ### タイトル
</MarkdownRenderer>;

<MarkdownProvider components={{ h3: MyCustomH3 }}>{children}</MarkdownProvider>;

// Providerのコンテキストを利用したフック形式
const render = useMarkdownRenderer();
return <div>{render("# Hello")}</div>;

Intlayer のコンテンツ宣言と組み合わせると、ロジックとコンテンツを綺麗に分離できて、真価を発揮します:

// ./myMarkdownContent.content.ts
import { md } from "intlayer";

export default {
  key: "my-content",
  content: md("## 多言語対応のMDです"),

  // ファイルシステムから読み込む場合
  //   content: md(readFileSync("./myMarkdown.md", "utf8")),

  // リモートから読み込む場合
  //   content: md(fetch("https://api.example.com/content").then((res) => res.text())),
};

コンポーネント側では、ただの変数として扱えます。パース処理を自分で書く必要はありません:

const { myContent } = useIntlayer("my-content");

return (
  <div>
    {myContent} {/* グローバル設定を使って自動レンダリング */}
    {/* または */}
    {/* その場でコンポーネントを上書き */}
    {myContent.use({
      h2: (props) => <h2 className="text-blue-500" {...props} />,
    })}
  </div>
);

何が新しいの?

  • 真のユニバーサル対応:React でも Vue でも Svelte でも、全く同じロジックが使えます。
  • 軽量な MDX 風コンパイラ:エッジやサーバーでもシームレスに動作。
  • ロード時間ゼロ:fs でも fetch でも、コンテンツはビルド時にロードされます。
  • 再利用性:小さな Markdown セクションを複数のドキュメントやページで簡単に使い回せます。
  • front-matter の型安全なパース(以前の Contentlayer のような使い心地)。

どんなユースケース向け?

  • ブログ / ドキュメント / プライバシーポリシー / 利用規約
  • バックエンドから取得した動的なデータ
  • ヘッドレス CMS へのコンテンツ出し分け
  • .md ファイルの直接読み込み

既存のツールへの不満からこれを作りました。同じような悩みを持っている方はいませんか?皆さんが普段 Markdown をどう扱っているか、ぜひ教えてください!

公式ドキュメント: https://intlayer.org/ja/doc/concept/content/markdown
GitHub: https://github.com/intlayer/intlayer/

Discussion