記事一覧に戻る

Next.jsでMDXを扱う方法

はじめに

MarkDownファイル(以下、MDファイル)は、GitHubのREADME.mdなどにも使われる、マーク・アップ言語を使って記述するファイルです。拡張子が.mdのファイルですね。HTMLに変換することも出来るので、ウェブ・ページのコンテンツとしても広く使われます。

Next.jsでも、公式チュートリアルのPre-rendering and Data Fetchingで、MDファイルからブログ・ページを生成する例が紹介されています。Next.jsを使っている方は、MDファイルを利用している方が多いのではないでしょうか?このサイトも例外ではありません。

とはいえ、HTMLへの変換は機械的に行われるので、制約は出ます。例えば、ディスプレイの幅に応じて画像を切り替えようとすると、MarkDown表記だけでは対応できません。直接、imgタグを埋め込んでsrcset属性を設定したり、pictureタグを埋め込んだり、HTMLを直接記述していく必要が出てきます。これがとにかく面倒です。

他方、Next.jsではnext/imageコンポーネントがあり、画像の最適化を自動で行ってくれます。当然ながら、これはReactのコンポーネントなのでMDファイル内で扱うことはできません。

しかし、出来てしまうのです、、、MarkDown X(以下、MDXファイル)なら、、、!

そして、Next.jsもMDXファイルをサポートしています。

今回、Next.jsでMDXファイルからページの生成を行う方法を試してみたので、その方法と注意点を解説します。

ちなみに、このページの記事部分はMDXから生成していますよ!

MDXとは

MDファイルは、MarkDownと呼ばれるマーク・アップ言語で記述されたテキスト・ファイルです。通常のHTMLを直接埋め込むことも可能です。

MDXは、MDを拡張しReactのコンポーネントの埋め込みが出来るようにしたものです。拡張子は.mdxとなります。

Next.jsのnext/imageImageコンポーネントのように、便利なコンポーネントをMDファイル内で直接扱うことが出来るので、使い方によってはかなり便利です。

記事の範囲

Next.jsでMDXを扱う方法を、公式の手順の内容に基づいて解説します。

MDやMDXの文法は範囲外となります。

Next.jsはApp Routerの利用を前提に記載しています。Pages RouterでもMDXはサポートされていますが、設定方法等が微妙に異なる可能性がありますので、ご了承ください。

前提知識

  • MarkDownに関する基礎的な知識
  • remarkrehypeといったMarkDown⇔HTML変換に必要なパッケージに関する基礎的な知識
  • Next.jsのApp Routerに関する基礎的な知識

既にMDファイルからウェブ・ページのコンテンツを生成されている方なら、問題無いと思います。

前提知識として記載したものの、Next.jsに関する基礎的な知識があれば、読み進められると思います。

別の記事でunified(remarkやrehype)や、App Routerについて記載しているので、よかったら参考にしてください。

環境

私の環境は以下のとおりです。

{
    "next":"^13.4.1",
    "typescript":"^4.8.4"
}

Next.jsはv13.4.1、TypeScriptはv4.8.4です。

MDX利用までの準備

大まかに3ステップあります。どれも簡単です。

  1. 必要なパッケージのインストール
  2. mdx-components.tsxの作成
  3. next.config.jsの修正

1.必要なパッケージのインストール

公式の記載されている4つインストールする必要があります。

npm install @next/mdx @mdx-js/loader @mdx-js/react @types/mdx

私の環境では以下のようなバージョンになりました。

{
    "@next/mdx": "^13.4.19",
    "@mdx-js/loader": "^2.3.0",
    "@mdx-js/react": "^2.3.0",
    "@types/mdx": "^2.0.7",
}

2.mdx-components.tsxの作成

Next.jsのプロジェクトのルート・フォルダ(next.config.jsと同じ階層です)に、mdx-components.tsxの名前で、以下の内容を保存します。公式ドキュメントからのコピペです。

TypeScriptを使わない場合、拡張子は.jsxにしてください。

import type { MDXComponents } from "mdx/types";

// This file allows you to provide custom React components
// to be used in MDX files. You can import and use any
// React component you want, including components from
// other libraries.

export function useMDXComponents(components: MDXComponents): MDXComponents {
    return {
        // Allows customizing built-in components, e.g. to add styling.
        // h1: ({ children }) => <h1 style={{ fontSize: "100px" }}>{children}</h1>,
        ...components,
    };
}

TypeScriptを使っていない場合、1行目のimportを削除し、useMDXComponentsのパラメタと戻り値の型情報を削除すればOKです。

コメントに記載されているとおり、このファイルを置くことで、MDXファイル内でReactのコンポーネントを使うことが可能になります。

3.next.config.jsの修正

Next.jsのconfigファイルであるnext.config.jsを修正します。

/** @type {import('next').NextConfig} */
const nextConfig = {
  /**ここはいじらなくてOK */
}
const withMDX = require("@next/mdx")();
export default withMDX(nextConfig);

既にあるnextConfigには手を加えなくてOKです。

1.でインストールした@next/mdxをインポートして、nextConfigをパラメタに渡して呼び出し、その結果をデフォルト・エクスポートするだけです。

これで最小限のセットアップは完了です!

基礎編:MDXを使ってみる

それではさっそく使ってみます、

.mdxファイル

/app/mdxに、以下の内容でMDXファイルを作成してみます。ファイル名はpage.mdxとしますが、何でも良いです。

import Image from "next/image";
import pic from "./console.jpg";

# MDX TEST!!!

## bash script

```bash
cd ~
mkdir test
```
## next/imageのテスト

<Image src={pic} alt="" width={500} height={300}/>

せっかくなので、画像をimportしてnext/imageImageコンポーネントを使ってみました。しかし、MarkDownにimportとかReactのコンポーネントが入ると不思議な気分ですね。

page.tsxファイル

上記と同じ階層に、page.tsxを作成します。

import MdxContent from "./page.mdx";

export default async function Page() {
  return (
    <article>
      <MdxContent />
    </article>
  )
}

MDXファイルをReactのコンポーネントのようにimportできるのがミソです。なお、MdxContentという名前でimportしていますが、default exportなので名前は任意のもので大丈夫です。

ブラウザで見てみる

実際にhttp://localhost:3000/mdxを開いてページを確認してみます。

basic-example

ちゃんと表示されました!画像も大丈夫ですね。

応用編:MDXを利用してみる

上記の例では、MDXをそのままコンポーネントのように扱っているだけです。

実際に裏ではremarkrehypeでHTMLに変換されているので、これらのプラグインを適用させることも可能です。

後、今までMDを使われていた方は、メタ情報などをFront-Matterに記述していた方も多いのではないでしょうか?それこそNext.jsのチュートリアルで紹介されていたので、私も使っていました。

こういうMDファイルの冒頭に記入するyaml形式のデータです。

---
title: TEST
author: ZEN
---

# ふろんと・まったー

ここでは、remarkやrehypeのプラグインの適用方法と、MDXでFront-Matterに代わる機能を紹介します。

プラグインの適用

私の場合、MDを使う場合はremark-gfmrehype-prismを使っていましたので、MDXにも適用させてみようと思います。

ちなみに、remark-gfmは「GitHub版のMarkDown」をHTML等に変換させる際に使うプラグインです。rehype-prismは、シンタックス・ハイライト用に使います。

next.configの修正

remark-gfmやrehype-prismはESMのみに対応しているため、next.config.jsの拡張子を.mjsに修正する必要があります。

そして、configの中身を以下のように修正します。

import remarkGfm from 'remark-gfm';
import rehypePrism from 'rehype-prism';
import mdx from "@next/mdx"

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
}

const withMDX = mdx({
  options: {
    // remarkとrehypeは指定しなくてよいとドキュメントに指定あり。
    // remarkParse,remarkRehype,rehypeStringifyは指定しなくても大丈夫。
    remarkPlugins: [remarkGfm],
    rehypePlugins: [rehypePrism],
  },
});

export default withMDX(nextConfig);

remark-gfmやrehype-prismをimportし、オプションとしてmdxのパラメタに指定しています。これでMDXファイルをパースする際に、プラグインが適用されます。

なお、MDファイルをHTMLに変換にする場合、通常であればremark-parse、remark-rehype、rehype-stringifyといったパッケージも必要ですが、これらはNext.jsが自動でやってくれるので指定不要です。

rehype-prismのシンタックス・ハイライトについて

シンタックス・ハイライトしたい言語(jsやpython等)は、next.config.mjs内でimportすれば大丈夫です。

import remarkGfm from 'remark-gfm';
import rehypePrism from 'rehype-prism';
import mdx from "@next/mdx"

// Prismでシンタックス・ハイライトしたい言語
import 'prismjs/components/prism-python.js';
import 'prismjs/components/prism-bash.js';

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
}

const withMDX = mdx({
  options: {
    // remarkとrehypeは指定しなくてよいとドキュメントに指定あり。
    // remarkParse,remarkRehype,rehypeStringifyは指定しなくても大丈夫。
    remarkPlugins: [remarkGfm],
    rehypePlugins: [rehypePrism],
  },
});

export default withMDX(nextConfig);

MDファイルの場合、page.tsx等でimportしても問題ありませんでしたが、何故かMDXファイルの場合はnext.config内でimportしないとReferenceError: Prism is not definedとエラーになってしまいます。

私の使い方が悪いだけかもしれませんが、、、もし同じエラーが出るようだったら、next.config内でimportしてみて下さい。

ブラウザで確認

page.tsxとMDXファイルを準備して、ブラウザで確認してみます。

page.tsxには、シンタックス・ハイライト用のCSSのimportを追加しています。後は基礎編と同じです。

import MdxContent from "./page.mdx";

 // syntax hightlight用 CSS
import "prismjs/themes/prism-tomorrow.css";

export default async function Page() {
  return (
    <article>
      <MdxContent />
    </article>
  )
}

MDXファイルは、GitHub Flavored MarkDownの拡張部分(テーブル)とシンタックス・ハイライト部分が分かるように、以下の内容にしてみます。

# MDX TEST!!!

## bash script

```bash
cd ~
mkdir test
```

```python
def test():
  print("hello,world!")

test()
```

## テーブルのテスト

| foo | bar |
| --- | --- |
| baz | bim |

ブラウザで見てみると、ちゃんとtableタグに変換されていることと、シンタックス・ハイライトがされていることが分かります。

plugin-example

Front-Matterの代替

MDファイルにFront-Matterを埋め込んでいる方は、gray-matterのようなパッケージを使ってパースをされていると思います。

ただし、MDXでは同じような使い方は出来ません。少なくとも、私はドキュメントからは見つけられませんでした。

しかし、MDXではimportだけでなくexportも利用することができます。なので、Front-Matterを使わなくても、以下のように、MDXファイル内で直接exportしてしまえば良いのです。

export const meta = {
  title: "テスト",
  author: "全力君",
};

# Hello,World

情報をエクスポートします。

後は、page.jsx内でimportすればOKです。

// MDXのコンテンツ
import MdxContent from "./page.mdx";
// MDX内のexportしたオブジェクトをimport
import { meta } from "./page.mdx"

// syntax hightlight用 CSS
import "prismjs/themes/prism-tomorrow.css";

export default async function Page() {
  return (
    <article>
      <div>{meta.title}</div>
      <div>{meta.author}</div>
      <MdxContent />
    </article>
  )
}

ブラウザで見てみると、MDX内でexportしたmetaのtitleとauthorが取得出来ていることが分かります。

export-example

TypeScript利用時の注意点

MDXファイル内でexportしたデータをimportする際、TypeScript利用時は注意点があります。

上記の例で、page.tsx内でimport { meta } from "./page.mdx"とすると、以下のエラーになってしまいます。

Module '".mdx"' has no exported member 'metadata'. Did you mean to use 'import metadata from ".mdx"' instead?

TypeScriptがMDXファイルをモジュールとして解釈するのですが、型情報が無いため発生してしまうエラーです。なので、.d.tsファイルを作成して、型情報を定義する必要があります。

型定義ファイルを作成

プロジェクト直下にmdx.d.tsの名前でファイル作成します。

今回exportするデータの型に合わせて、以下の内容で保存します。なお、拡張子が.d.tsであれば、ファイル名は何でも良いです。

declare module "*.mdx" {
    interface Meta {
        title: string;
        author: string;
    }
    export const meta: Meta
}

tsconfig.jsonに反映

tsconfigのincludeに、さきほど作成したmdx.d.tsを追加します。compilerOptionsの中身等は関係がないため省略しています。

{
  "compilerOptions": {},
  "include": [
    "next-env.d.ts",
    "**/*.ts",
    "**/*.tsx",
    ".next/types/**/*.ts",
    "mdx.d.ts"
  ]
}

これで、MDXファイルはMeta型のデータをexportすることをTypeScriptも理解してくれるので、エラーが出なくなります!

別の回避策

.d.tsファイルの作成が面倒であれば、page.tsx内で@ts-ignoreを入れてTypeScriptのエラーを無視する方法もあります。

import MdxContent from "./page.mdx";
// @ts-ignore
import { meta } from "./page.mdx"

export default function Page(){/* 省略 *//}

ただ、型情報は取得できなくなります。

その他の機能

私もまだ使えていませんが、Next.jsのMDX関連のその他の機能を紹介します。

Rustのコンパイラ

MDXのパースにRustのコンパイラを使うこともできます。高速に動作してくれるそうです。

next.confignextConfigに、experimental:{mdxRs:true}のオプションをつけると有効化できます。

/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    mdxRs: true,
  },
}
 
const withMDX = require('@next/mdx')()
module.exports = withMDX(nextConfig)

少し試してみたところ、体感ですがページの表示は少し早くなったように感じます。

ただし、残念ながらこちらはまだexperimentalな機能です。「本番では使わないで」とドキュメントにも記載されています。

MDXファイルを直接ページとして使う

App Routerならpage.jsx|tsx|js|tsがページとして解釈されますが、.mdxのファイルもページとして解釈させることも出来ます。

next.configpageExtensionsに、ページとして解釈する拡張子を羅列し、その中にmdxを含めればOKです。

const nextConfig = {
  pageExtensions: ["jsx","tsx","js","ts","mdx"],
}
 
const withMDX = require('@next/mdx')()
module.exports = withMDX(nextConfig)

こうすれば、/app/blog/page.mdxは、/blogのページとして解釈されます。layout.jsxも適用されますし、なんならMDXファイル内でコンポーネントをimportできるので、もしかするとこれで十分な場合も多いかもしれません。

MDXから変換されたHTMLをカスタムする

MDXのMarkDown部分は上述のとおり、remarkとrehypeでHTMLの各タグに変換されます。この変換されたタグにスタイルをつけたり、カスタムすることができます。

プロジェクト直下においたmdx-components.jsx|tsxに、カスタムしたいタグを指定します。ドキュメントのコメント欄に記載があるので、それに沿って記述すればOKです。

import type { MDXComponents } from "mdx/types";

// This file allows you to provide custom React components
// to be used in MDX files. You can import and use any
// React component you want, including components from
// other libraries.

function MyH1({ children }: { children: any }) {
    return <h1 style={{ backgroundColor: "blue" }}>{children}</h1>
}

export function useMDXComponents(components: MDXComponents): MDXComponents {
    return {
        // Allows customizing built-in components, e.g. to add styling.
        // h1: ({ children }) => <h1 style={{ fontSize: "100px" }}>{children}</h1>,
        h1: ({ children }) => <MyH1>{children}</MyH1>,
        h2: ({ children }) => <h2 style={{ backgroundColor: "pink" }}>{children}</h2>,
        ...components,
    };
}

h1タグをMyH1コンポーネントにカスタムし、h2タグはスタイルをつけてカスタムしています。自作のコンポーネントも使えるのは便利ですね。

以下のMarkDownをブラウザに表示させて、スタイルを確認してみます。

# H1タグ

よう

## H2タグ

へい
custom-example

ちゃんと適用されています。

少し困ったこと

MDファイルを、Next.jsのDynamic Routing機能を使って動的にページ作成していたのですが、MDXファイルだと出来ません。

例えば、/app/post/[dir]/page.tsxで、以下のように動的にページを生成しようとするとエラーになります。

export async function generateStaticParams(){
  /*
   * 省略
   * ディレクトリ一覧を返す関数
   */
}

function getProps(dir){
  // mdxのパスを生成する関数
  const mdxPath = genMdxPath(dir);
  // ここが悪さしている。。。?import(mdxPath)でも同じ。
  return require(mdxPath)
}

export default function ({params}){
  const {dir} = params;
  const content = genProps(dir)
  return (/**省略 */)
}

Cannot find modules パス名とエラーになりますが、実際にパスを確認するとちゃんとMDXファイルは存在しています。

おそらく動的にrequireしているのが良くないのだと思いますが、、、ちなみにimport(dir)にしても同じです。

もし解決方法を御存知の方がいたら教えてください。

最後に

Next.jsのApp Routerで、MDXファイルを扱う方法を解説しました。

このサイトでも、このページがMDXを使ったページ第一弾となります。今のところ、MDファイルよりカスタム性が高く、結構便利だなと感じています。

今後ページが増えていった時に、管理のしやすさがどうなるかですね。当面、新規の記事はMDXファイルを使って更新していこうと思います。

参考

記事一覧に戻る