homeIcon

ブログ記事をMDXで管理したくて、next-mdx-remote-clientを導入した話

フロントエンド
2025.08.09
2025.08.10

ブログ記事を Markdown で書いていると、「ここだけReactコンポーネントを差し込みたい」「コードブロックをサイトのデザインに合わせたい」と思う瞬間があります。
そこで出会ったのが MDX
さらに Next.js の App Router に合わせやすいのが next-mdx-remote-client でした。

この記事では、私がこのライブラリを選んだ理由、実装手順などを、microCMS連携&静的生成(SG)構成という実戦的な環境で解説します。

なぜ MDX を導入したか

当初は microCMS のリッチエディタで記事管理をしようとしていましたが、以下のような課題がありました。

  • 特定箇所にだけデザイン済みの UI 部品を入れたいのに、HTML直書きか画像で代用するしかない
  • コードブロックのハイライトや装飾を記事ごとに調整するのが面倒
  • OGPカードやカスタム埋め込み(Tweet、YouTubeなど)の統一が難しい

このように、Markdown はシンプルで扱いやすいですが、インタラクティブ要素や高度なレイアウトの埋め込みには不向きでした。
一方で、MDX はMarkdownにReactコンポーネントを埋め込めるので、こういった拡張が容易になります。たとえば、記事中に \<OgpCard url="..." /> と書くだけで、スタイルも機能も統一されたリンクカードを呼び出せます。

なぜ next-mdx-remote-client を選んだのか

MDXをNext.jsで扱う方法は以下のようにいくつかあります。

方法特徴弱点
@next/mdxApp Routerに組み込みやすいサーバーでの動的パースが難しい
next-mdx-remoteサーバーでシリアライズし、クライアントで描画可能App Router対応に工夫が必要
自前 (@mdx-js/mdx)自由度MAX設定が煩雑、保守コスト高

next-mdx-remote-clientApp Routerに対応しており、サーバーでパース → クライアントで描画という流れをシンプルに作れます。components の差し替えも容易で、記事ごとに動的なUIを組み込みやすいのが魅力です。
今回の構成では App Router + SSG + microCMS という要件だったため、ビルド時に microCMS から記事を取得 → MDX としてシリアライズ → 静的ページ生成 という流れに最も適していた next-mdx-remote-client を選びました。

GitHub - ipikuka/next-mdx-remote-client: A wrapper of `@mdx-js/mdx` for `Next.js` applications in order to load MDX content. It is a fork of `next-mdx-remote`.

A wrapper of `@mdx-js/mdx` for `Next.js` applications in order to load MDX content. It is a fork of `next-mdx-remote`. - ipikuka/next-mdx-remote-client

github.com

A wrapper of `@mdx-js/mdx` for `Next.js` applications in order to load MDX content. It is a fork of `next-mdx-remote`. - ipikuka/next-mdx-remote-client

技術スタック

主に以下の技術を使用して、本サイトを作成しました。

  • フロント:Next.js[App Router](React/TypeScript)
  • デザイン:TailwindCSS、vanilla-extract
  • ライブラリ:next-mdx-remote-client
  • コンテンツ管理:microCMS
  • デプロイ:Vercel

導入手順

Next.js[App Router] のプロジェクトは作成済みとして、進めていきます。

ライブラリのインストール

npm install next-mdx-remote-client remark-gfm open-graph-scraper
  • remark-gfm:GitHub Flavored Markdown(表・チェックボックスなど)対応
  • open-graph-scraper:OGPデータ取得

MDX 変換オプションの設定

/src/libs/mdxOptions.ts
import type { MDXRemoteOptions } from "next-mdx-remote-client/rsc";
import remarkGfm from "remark-gfm";
 
export const mdxOptions: MDXRemoteOptions = {
  mdxOptions: {
    remarkPlugins: [remarkGfm],
    format: "mdx",
  },
  parseFrontmatter: true,
};
  • MDXRemote(RSC版)に渡す変換オプションを事前定義。
  • remark-gfm で GFM(表/チェックボックス/打消し等) を有効化。
  • format: "mdx" により、本文内の JSX(MDX)構文を許可。
  • parseFrontmatter: true で、先頭の --- で囲うFrontmatterを source.frontmatter に抽出

ルーティングと静的生成(SG)/ データ取得

動的ルートの静的化(generateStaticParams

export async function generateStaticParams() {
  const slugs = await getAllBlogSlugs();
  return (slugs ?? []).map(({ articleSlug }) => ({ slug: articleSlug }));
}
  • ビルド時に microCMS からすべての slug を取得 → 各記事ページをSGで生成

ページ本体(RSC) + OGP 前処理 + MDX描画

/src/app/blog/[slug]/page.tsx
import React from "react";
import { getAllBlogSlugs, getBlogBySlug } from "@/libs/microcms";
import { notFound } from "next/navigation";
import { fetchOgpDataFromMdx } from "@/libs/fetchOgpDataFromMdx";
import { MdxComponents } from "@/components/PortfolioAndBlog/ContentPage/MdxComponents";
import { MDXRemote } from "next-mdx-remote-client/rsc";
import { mdxOptions } from "@/libs/mdxOptions";
import BlogClientWrapper from "@/components/PortfolioAndBlog/ContentPage/BlogClientWrapper";
 
 
type PageProps = {
  params: Promise<{ slug: string }>;
};
 
export async function generateStaticParams() {
  const slugs = await getAllBlogSlugs();
  return (slugs ?? []).map(({ articleSlug }) => ({ slug: articleSlug }));
}
 
export default async function BlogContentPage({ params }: PageProps) {
  const { slug } = await params;
  const blogContentData = await getBlogBySlug(slug);
 
  // 存在しなければ 404
  if (!blogContentData) return notFound();
 
  // Markdown → MDX に変換しつつ、OGPも取得
  const { ogpDataList } = await fetchOgpDataFromMdx(blogContentData.body);
  const components = MdxComponents(ogpDataList);
 
  return (
    <BlogClientWrapper blogContent={blogContentData}>
      <MDXRemote
        source={blogContentData.body}
        options={mdxOptions}
        components={components}
      />
    </BlogClientWrapper>
  );
}
 
  • getBlogBySlug(slug) で RSC側(サーバー)でデータ取得
  • fetchOgpDataFromMdx() で本文から OGPカードのURL を抽出 → ビルド/サーバーで OGPデータ を取得(クライアントで fetch しない)
  • MdxComponents(ogpDataList):取得したOGPデータのマップを差し込んだコンポーネント辞書
  • MDXRemote(RSC版)に source(MDX文字列)・optionscomponents を渡して描画

MDX コンポーネント辞書

上記の components={components} には、下記のようにhtml変換後の各要素に対応するカスタムコンポーネントをマッピングしたオブジェクトを渡します。

/src/components/MdxComponents/index.tsx
import React from "react";
import type { MDXComponents } from "next-mdx-remote-client/rsc";
import { OgpCard } from "@/components/PortfolioAndBlog/ContentPage/MdxComponents/OgpCard";
import * as styles from "./style.css";
 
type OgpCardProps = React.ComponentProps<typeof OgpCard>;
 
export const MdxComponents = (
  ogpMap: Record<string, OgpCardProps["ogp"]>
): MDXComponents => ({
  // ---- MDX全体のラッパー ----
  wrapper: ({ children }) => <div className={styles.wrapper}>{children}</div>,
 
  // ---- 見出し ----
  h2: (props) => <h2 className={styles.h2} {...props} />,
  h3: (props) => <h3 className={styles.h3} {...props} />,
  h4: (props) => <h3 className={styles.h4} {...props} />,
 
  // ---- テキスト要素 ----
  p: (props) => <p className={styles.p} {...props} />,
  strong: (props) => <strong className={styles.strong} {...props} />,
 
 
  // ---- OGPカード ----
  OgpCard: ({ url }: { url: string }) => {
    const ogp = ogpMap[url];
    if (!ogp) return null;
    return <OgpCard ogp={ogp} />;
  },
});
  • wrapper で本文全体のレイアウトや余白を統一
  • MDX の標準タグ(h2/h3/p/...)やカスタムタグ(<OgpCard />)を、見た目/機能つきの React に差し替え
  • \<OgpCard url="..."> は、事前収集した ogpMap[url] を引き当てて描画

実装のポイント

上記の実装により、ビルド時またはサーバーサイドでコンテンツの変換とOGP情報の取得が完結し、クライアントでは描画専用の軽量な処理だけを行えるため、パフォーマンス向上と安定した表示が両立できます。
また、以下のようなメリットもあります。

  • MDX により記事中に UI 部品を安全に再利用できる
  • Server Component でパースし、Client Component で描画 → App Router の構造に沿った役割分離
  • OGP情報をビルド/サーバーで確定 → Hydrationエラー回避に強い
  • microCMS 側はプレーン Markdown 保持 → エンジニアフレンドリーなコンテンツ管理

まとめ

next-mdx-remote-client は App Router 環境で Markdown を超える表現力 を記事に与えつつ、SSG との相性も非常に良いライブラリだと思いました。Markdown に React の力を組み合わせたい方には特におすすめです。
もし同じように App Router 環境で MDX を導入したいと考えている方は、ぜひ今回の実装例を参考にしてみてください!

最後に

ここまで読んでいただきありがとうございました!
本記事が、MDX で記事管理してみたいという方の参考になれば嬉しいです。
なお、本サイトのソースコードは以下で公開していますので、よければ覗いてみてください!

GitHub - miyazaki-dev01/My_profile: プロフィールサイト(React[Next.js]/TypeScript)

プロフィールサイト(React[Next.js]/TypeScript). Contribute to miyazaki-dev01/My_profile development by creating an account on GitHub.

github.com

プロフィールサイト(React[Next.js]/TypeScript). Contribute to miyazaki-dev01/My_profile development by creating an account on GitHub.

参考サイト

Share