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/image
のImageコンポーネントのように、便利なコンポーネントをMDファイル内で直接扱うことが出来るので、使い方によってはかなり便利です。
記事の範囲
Next.jsでMDXを扱う方法を、公式の手順の内容に基づいて解説します。
MDやMDXの文法は範囲外となります。
Next.jsはApp Routerの利用を前提に記載しています。Pages RouterでもMDXはサポートされていますが、設定方法等が微妙に異なる可能性がありますので、ご了承ください。
前提知識
- MarkDownに関する基礎的な知識
- remarkやrehypeといった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ステップあります。どれも簡単です。
- 必要なパッケージのインストール
- mdx-components.tsxの作成
- 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/image
のImageコンポーネントを使ってみました。しかし、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
を開いてページを確認してみます。
ちゃんと表示されました!画像も大丈夫ですね。
応用編:MDXを利用してみる
上記の例では、MDXをそのままコンポーネントのように扱っているだけです。
実際に裏ではremarkとrehypeでHTMLに変換されているので、これらのプラグインを適用させることも可能です。
後、今までMDを使われていた方は、メタ情報などをFront-Matterに記述していた方も多いのではないでしょうか?それこそNext.jsのチュートリアルで紹介されていたので、私も使っていました。
こういうMDファイルの冒頭に記入するyaml形式のデータです。
---
title: TEST
author: ZEN
---
# ふろんと・まったー
ここでは、remarkやrehypeのプラグインの適用方法と、MDXでFront-Matterに代わる機能を紹介します。
プラグインの適用
私の場合、MDを使う場合はremark-gfmとrehype-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タグに変換されていることと、シンタックス・ハイライトがされていることが分かります。
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が取得出来ていることが分かります。
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.config
のnextConfigに、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.config
のpageExtensionsに、ページとして解釈する拡張子を羅列し、その中に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タグ
へい
ちゃんと適用されています。
少し困ったこと
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ファイルを使って更新していこうと思います。