- 技術 💻
MDXとは?使い方とMarkdownとの違い|エラー原文と実測
結論
MDXとは、Markdown の文書の中に JSX(React などの部品を呼び出す書き方)と JavaScript の式を書ける形式です。拡張子は .mdx で、ブラウザーがそのまま読むのではなく、ビルドのときに JavaScript のコンポーネントへ変換されてから HTML になります。
この記事でわかること
- @mdx-js/mdx 3.1.1 で .mdx をコンパイルした JavaScript と、表示された HTML の実物
- Markdown との違いと、
<・{・<br>などで出るエラーの原文と直し方 - Next.js 16・Astro 7 で動かした手順と、先にコンパイルする場合と表示のときにコンパイルする場合のサイズ・時間の差
MDX のファイルは、そのままではブラウザーで表示できません。コンパイラーが Markdown と JSX を読み取り、「HTML の要素と部品を並べて返す関数」に変えます。その関数を React などが描画して、最後に HTML になります。
flowchart TD
A[hello.mdx<br>Markdown + JSX + 式] --> B[MDX のコンパイラー<br>@mdx-js/mdx]
B --> C[JavaScript<br>MDXContent 関数]
C --> D[React などが描画]
D --> E[HTML]
F[Note.jsx などの部品] --> C
よく調べられることの答えを先にまとめます。
| 知りたいこと | 答え | 詳しい節 |
|---|---|---|
| MDXとは(Web開発) | Markdown に JSX・式・import/export を足した形式。拡張子は .mdx | MDXとは |
| MDX の別の意味 | SQL Server の多次元クエリ言語、ホンダ(アキュラ)の車名、辞書ファイルの拡張子など | 場面別の意味の表 |
| MDX の使い方(最小) | npm i @mdx-js/mdx → compile() で JavaScript にする → React で描画 |
最初の5分 |
| MDX と Markdown の違い | 部品と式が書ける代わりに、HTML コメント・<br>・インデントのコード・自動リンクが使えない |
Markdownとの違い |
MDX で < や { を書きたい |
\<・\{ とエスケープするか、{'<'} のように式で書く |
よくあるエラー |
| Could not parse expression with acorn | { } の中が JavaScript の式として読めない。\{ にするか式を直す |
よくあるエラー |
| Next.js で MDX を使う | @next/mdx を入れ、mdx-components ファイルを置く(App Router では必須) |
Next.jsでMDXを使う |
| Astro で MDX を使う | @astrojs/mdx を入れ、integrations: [mdx()]。JS を送らずに HTML になる |
AstroでMDXを使う |
確認環境:Ubuntu 24.04(2 CPU・メモリ 8GB)、Node.js 22.23.4。作業用のフォルダーに npm で @mdx-js/mdx 3.1.1・@mdx-js/react 3.1.1・@mdx-js/esbuild 3.1.1・esbuild 0.28.2・React 19.3.1・react-dom 19.3.1・remark-gfm 4.0.1・remark-frontmatter 5.0.0・remark-mdx-frontmatter 6.0.0 を入れ、コンパイル・描画・エラーの原文・サイズを確かめました。Next.js は 16.4.3 と @next/mdx 16.4.3(テレメトリーは NEXT_TELEMETRY_DISABLED=1 で最初から無効)、Astro は 7.4.4 と @astrojs/mdx 8.1.2(ASTRO_TELEMETRY_DISABLED=1)で静的ビルドまで動かしました。表示の時間は Playwright の Chromium 153.0.8010.12 で、127.0.0.1 の自分のページを開いて測っています。Docusaurus・Storybook は動かしておらず、公式ドキュメントで確かめた範囲で書いています。計測した機械はほかの作業と共有しているため、時間の数字は目安です(確認日 2026年10月29日)。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「MDXとは?使い方とMarkdownとの違い|エラー原文と実測」
MDXとは(場面別の意味の表と、Markdown+JSXのMDX)
この節では、「MDX」が分野ごとに何を指すかと、Web 開発でいう MDX の位置づけが分かります。
「MDX」は分野によって指すものが変わる言葉です。ブログ・ドキュメント・React・Next.js の話で出てくる MDX は、ほぼ必ず「Markdown に JSX を書ける形式」です。
| 場面 | MDX が指すもの | 見分け方 |
|---|---|---|
| Web 開発・ドキュメント | Markdown+JSX の文書形式(この記事の話) | .mdx ファイル、@mdx-js、Next.js・Astro・Docusaurus |
| データベース・BI | MultiDimensional eXpressions。SQL Server Analysis Services などで多次元のデータ(キューブ)を問い合わせる言語 | SELECT ... ON COLUMNS、キューブ・メジャー |
| 自動車 | ホンダの海外ブランド「アキュラ」の SUV の車名 | 車・試乗・価格の話 |
| 辞書アプリ | MDict 形式の辞書ファイルの拡張子(.mdx と .mdd の組) | 辞書・単語帳のアプリの話 |
| 研究・大学 | 国内の大学などが共同で運用するデータ活用の基盤「mdx」 | 計算資源・仮想マシンの利用申請の話 |
ここから先は Web 開発の MDX の話です。MDX は、Markdown の書きやすさを残したまま、文書の途中に「部品」を置けるようにした形式です。たとえば、注意書きの枠・グラフ・タブ・動くデモなどを、HTML を手で書かずに <Note> のような1行で入れられます。
1Markdown がそのまま書ける
見出し・強調・リスト・リンク・コードブロックは、ふつうの Markdown(CommonMark)と同じ書き方。
2部品(JSX)を置ける
<Chart data={rows} /> のように、React などのコンポーネントを文書の中で呼び出せる。
3式と import/export が使える
{price * 1.1} のような計算や、別ファイルの部品・データの読み込みができる。
MDX 自体は特定のフレームワークの機能ではありません。公式のコンパイラー(@mdx-js/mdx)は、React のほか Preact・Vue・Solid など JSX を扱える仕組みと組み合わせられます。Next.js・Astro・Docusaurus・Storybook などは、このコンパイラーを中に組み込んで「.mdx をページや部品として使える」ようにしています。
Markdown そのものの書き方は、関連記事「Markdownの書き方一覧|記法早見表と環境別の対応・変換結果つき」で扱っています。この記事では、Markdown との「違い」と MDX に固有の部分に絞ります。
最初の5分:MDXを1ファイルコンパイルして表示する
この節では、MDX を入れて1つのファイルをコンパイルし、HTML として表示するまでの最小の手順と、その結果の実物が分かります。
パッケージを入れる
空のフォルダーで npm init -y のあと、npm i @mdx-js/mdx react react-dom。手元では 13 秒、依存を含めて 152 個・34MB でした(esbuild と remark のプラグインを含む)。
.mdx を書く
Markdown の中に、部品の呼び出し・式・export を混ぜたファイルを作ります(下のコード)。
compile() で JavaScript にする
compile(ファイルの中身) が JavaScript のソースを返します。中身は次の節で見ます。
React で描画する
esbuild などのバンドラーで .mdx を読み込み(@mdx-js/esbuild)、renderToStaticMarkup で HTML を出します。
使った MDX のファイルです。先頭の --- で囲んだ部分は front matter(記事の情報)で、remark-frontmatter と remark-mdx-frontmatter を足すと frontmatter という変数で読めるようになります。
---
title: はじめての MDX
date: 2026-10-29
---
import {Note} from './Note.jsx'
export const price = 1200
# {frontmatter.title}
税込みの価格は **{Math.round(price * 1.1)}円** です。
<Note type="info">
ここは *Markdown* のまま書けます。
</Note>
{/* これは MDX のコメント */}
- 項目1
- 項目2
呼び出している部品 Note.jsx は、ふつうの React コンポーネントです。
export function Note({type, children}) {
return <aside className={'note note-' + type}>{children}</aside>
}
コンパイルと描画のスクリプトです(esbuild に MDX のプラグインを入れて1本のファイルにまとめ、Node.js で実行)。
// build.mjs
import * as esbuild from 'esbuild'
import mdx from '@mdx-js/esbuild'
import remarkFrontmatter from 'remark-frontmatter'
import remarkMdxFrontmatter from 'remark-mdx-frontmatter'
await esbuild.build({
entryPoints: ['render.jsx'], bundle: true, platform: 'node', format: 'esm',
outfile: 'out/render.mjs', jsx: 'automatic', packages: 'external',
plugins: [mdx({remarkPlugins: [remarkFrontmatter, remarkMdxFrontmatter]})]
})
// render.jsx
import {renderToStaticMarkup} from 'react-dom/server'
import Hello, {frontmatter, price} from './content/hello.mdx'
console.log(JSON.stringify(frontmatter), price)
console.log(renderToStaticMarkup(<Hello />))
実行した出力です。front matter と export した値は、ほかのファイルから名前付きで読み込めています。式 {Math.round(price * 1.1)} は 1320 に計算され、<Note> の中の *Markdown* も強調(em)になりました。
$ node build.mjs && NODE_ENV=production node out/render.mjs
build ms 161
frontmatter = {"title":"はじめての MDX","date":"2026-10-29"} price = 1200
<h1>はじめての MDX</h1>
<p>税込みの価格は <strong>1320円</strong> です。</p>
<aside class="note note-info"><p>ここは <em>Markdown</em> のまま書けます。</p></aside>
<ul>
<li>項目1</li>
<li>項目2</li>
</ul>
コメント {/* ... */} は HTML に出ません。ふつうの Markdown の HTML コメントとは違い、閲覧者がソースを見ても残らない点も覚えておくと便利です。
ブラウザーで試すだけなら、MDX の公式サイトの Playground でも、書いた MDX がどう変換されるかを確かめられます。
MDXの書き方(JSX・式・import/export・コメント)
この節では、MDX で Markdown に足される4つの書き方と、行の置き方で結果が変わる点が分かります。
JSX
<Note type="info">…</Note>。大文字で始まる名前は部品、小文字は HTML の要素として扱われる。
閉じタグか /> が必須
式
{1 + 1}・{props.name}。中は JavaScript の「式」で、if や for の「文」は書けない。
波括弧で囲む
import / export
行頭の import・export は JavaScript として読まれる。部品や定数の受け渡しに使う。
行頭だけが対象
コメント
{/* メモ */}。HTML のコメント記法は使えない。
出力に残らない
同じ行か、別の行かで p が付くかが変わる
MDX では、タグを文の途中に置くと「文の一部(インライン)」、行を分けて置くと「かたまり(ブロック)」として扱われます。実際に描画した結果です。
| 書いた MDX | 出力された HTML | 読み方 |
|---|---|---|
文の途中に <b>太字</b> を入れる |
<p>文の途中に <b>太字</b> を入れる</p> |
段落の中に b が入る |
<b>(改行)太字(改行)</b> |
<b><p>太字</p></b> |
中身が段落になり、b の中に p ができる |
<section>(空行)**太字**(空行)</section> |
<section><p><strong>太字</strong></p></section> |
部品の中で Markdown が効く |
2つめのように、インラインの要素(b・span など)を行を分けて書くと、中に p ができて HTML として正しくない入れ子になります。インラインの要素は1行に書き、枠のような部品は行を分ける、と覚えると迷いません。
props と、部品の差し替え(components)
MDX のファイルは、読み込んだ側から見ると1つのコンポーネントです。props で値を受け取れ、components で見出しなどの要素を別の部品に差し替えられます。
// greeting.mdx の中身: # {props.name} さん、こんにちは
<Greeting name="田中" />
// → <h1>田中 さん、こんにちは</h1>
// h1 だけ class を付けた部品に差し替える
const H1 = (p) => <h1 className="title" {...p} />
<Hello components={{h1: H1}} />
// → <h1 class="title">はじめての MDX</h1>
この差し替えは、ブログ全体で見出しにアンカーを付ける、画像を最適化用の部品に替える、といった用途に使います。React の props と children の受け渡しは、関連記事「React propsの渡し方とchildrenの使い方|TS型定義」で扱っています。<Note> の中に書いた Markdown は、部品から見ると children として届きます。
MDXとMarkdownの違い(同じ文を2つの形式で通した結果)
この節では、同じ文を Markdown(format: 'md')と MDX(format: 'mdx')でコンパイルしたとき、何が同じで何が変わるかが分かります。
@mdx-js/mdx は、オプション format で「ふつうの Markdown として読む」か「MDX として読む」かを切り替えられます。同じ文字列を両方で通した結果です。
| 書いた文 | Markdown(md) | MDX(mdx) |
|---|---|---|
1 < 2 のとき |
そのまま文字 | そのまま文字(< の後ろが空白なので通る) |
a<b |
そのまま文字 | エラー(タグの始まりと読まれる) |
function f() { return 1 } |
そのまま文字 | エラー({ } を式として読む) |
1行目<br>2行目 |
br は捨てられ「1行目」「2行目」が続く | エラー(閉じタグが無い) |
<!-- メモ --> |
出力されない | エラー(HTML コメントは使えない) |
<https://example.com> |
リンクになる | エラー(自動リンクは使えない) |
4文字下げた const x = 1 |
コードブロック(pre・code) | ただの段落になる(エラーは出ない) |
import と export の違い(行頭) |
そのまま文字 | エラー(JavaScript の import 文として読む) |
<p style="color:red">赤</p> |
生の HTML は出力されない | コンパイルは通るが React の描画でエラー |
表の「Markdown(md)」は @mdx-js/mdx の md モードの結果で、生の HTML は既定で捨てられます(rehype-raw などを足すと残せます)。GitHub やエディターのプレビューのように HTML を表示する環境とは、ここが違います。
Markdown(.md)が向く
文章だけの README・メモ・議事録
GitHub・エディター・Obsidian でそのまま読む
記号やコードを本文に気軽に書きたい
MDX(.mdx)が向く
ブログ・ドキュメントサイトの本文
注意書き・タブ・グラフ・デモを部品で入れたい
ビルドの仕組み(Next.js・Astro 等)がある
とくに見落としやすいのは、4文字下げたコードと、行頭の「import」「export」です。前者はエラーにならずに段落へ変わるため、表示を見るまで気づきません。後者は日本語の文でも行頭にあるだけで JavaScript として読まれ、Could not parse import/exports with acorn になります。コードは必ず3つのバッククォートで囲み、行頭に import・export が来る文は語順を変えるか、`import` とコードの書き方にします。
GFM(表・取り消し線)は既定で入っていない
MDX の既定の文法は CommonMark で、表・取り消し線・URL の自動リンクなどの GFM(GitHub Flavored Markdown)は入っていません。remark-gfm の有無で同じ文を通した結果です。
入力:| 形式 | JSX |(改行)|---|---|(改行)| .md | 書けない |(空行)~~取り消し~~ と https://example.com
gfmなし: <p>{"| 形式 | JSX |\n|---|---|\n| .md | 書けない |"}</p> <p>{"~~取り消し~~ と https://example.com"}</p>
gfmあり: <table><thead><tr><th>{"形式"}</th><th>{"JSX"}</th></tr></thead><tbody>... <del>{"取り消し"}</del> ...
表を使うなら remarkPlugins: [remarkGfm] を足します。後で見るとおり、Astro の @astrojs/mdx は GFM が最初から有効で、Next.js の @next/mdx は自分で足す必要がありました。
MDXのコンパイル結果の中身(何に変換されるか)
この節では、.mdx がどんな JavaScript になるかを実物で見て、「MDX はコンポーネントである」という意味が分かります。
最初の5分で使った hello.mdx を、compile(src, {jsx: true}) で JSX のまま出力した結果です(読みやすさのため改行を足していません)。
/*@jsxRuntime automatic*/
/*@jsxImportSource react*/
/*これは MDX のコメント*/
export const frontmatter = {
"title": "はじめての MDX",
"date": "2026-10-29"
};
import {Note} from './Note.jsx';
export const price = 1200;
function _createMdxContent(props) {
const _components = {
em: "em",
h1: "h1",
li: "li",
p: "p",
strong: "strong",
ul: "ul",
...props.components
};
return <><_components.h1>{frontmatter.title}</_components.h1>{"\n"}<_components.p>{"税込みの価格は "}<_components.strong>{Math.round(price * 1.1)}{"円"}</_components.strong>{" です。"}</_components.p>{"\n"}<Note type="info"><_components.p>{"ここは "}<_components.em>{"Markdown"}</_components.em>{" のまま書けます。"}</_components.p></Note>{"\n"}{}{"\n"}<_components.ul>...</_components.ul></>;
}
export default function MDXContent(props = {}) {
const {wrapper: MDXLayout} = props.components || ({});
return MDXLayout ? <MDXLayout {...props}><_createMdxContent {...props} /></MDXLayout> : _createMdxContent(props);
}
読み取れることを3つにまとめます。
1既定のエクスポートは関数
MDXContent は props を受け取る React コンポーネント。ページにも部品にもなる。
2要素は差し替え口を通る
h1・p などは _components.h1 経由。props.components を渡すと差し替わる仕組みが見える。
3コメントは先頭へ移る
{/* */} は JavaScript のコメントとして先頭に出て、描画の中身は空 {} になる。
大きさも測りました。336 バイトの hello.mdx は、既定の出力(JSX を関数呼び出しに変えた形)で 1,427 バイトの JavaScript になりました。React 本体は含まれず、読み込み側で react/jsx-runtime を使います。
この仕組みのため、MDX は「Markdown を HTML に変える道具」というより「Markdown で書けるコンポーネント」と考えると分かりやすくなります。Markdown を HTML の文字列にするだけなら、marked や remark のほうが仕組みは単純です。部品を混ぜたいときに MDX を選びます。
MDXでよくあるエラーと原文(< と { の扱い)
この節では、MDX のコンパイルと描画で出るエラーの原文と、それぞれの原因・直し方が分かります。
エラーは2か所で出ます。1つはコンパイル(.mdx を JavaScript にする段階)、もう1つは描画(React が部品を動かす段階)です。手元で出した原文をそのまま載せます。行:列は入力の中の位置です。
| 書いたもの | エラーの原文 | 直し方 |
|---|---|---|
a<b |
1:4 Unexpected end of file in name, expected a name character such as letters, digits, `$`, or `_`; whitespace before attributes; or the end of the tag |
a\<b か a{'<'}b |
x <2 の場合 |
Unexpected character `2` (U+0032) before name, expected a character that can start a name, such as a letter, `$`, or `_` |
x \<2 か、空白を入れて x < 2 |
function f() { return 1 } |
1:20 Could not parse expression with acorn |
コードは `…` で囲むか、\{ にする |
合計は {1 +} です |
1:9 Could not parse expression with acorn |
式を正しい JavaScript にする |
1行目<br>2行目 |
Expected a closing tag for `<br>` (1:4-1:8) before the end of `paragraph` |
<br /> と閉じる |
<!-- メモ --> |
Unexpected character `!` (U+0021) before name, ... (note: to create a comment in MDX, use `{/* text */}`) |
{/* メモ */} にする |
<https://example.com> |
Unexpected character `/` (U+002F) before local name, ... (note: to create a link in MDX, use `[text](url)`) |
[https://example.com](https://example.com) |
<Note>(閉じ忘れ) |
Expected a closing tag for `<Note>` (1:1-1:7) |
</Note> を足す |
<b>太字 <i>斜体</b></i> |
1:12 Unexpected closing tag `</b>`, expected corresponding closing tag for `<i>` (1:7-1:10) |
開いた順と逆に閉じる |
行頭の import と export の違い |
Could not parse import/exports with acorn |
語順を変えるか `import` にする |
エラーを出したのは micromark-extension-mdx-jsx(タグの読み取り)、micromark-extension-mdx-expression(式)、micromark-extension-mdxjs-esm(import/export)、mdast-util-mdx-jsx(タグの対応)という部品で、エラーの source にこの名前が入ります。どの段階でつまずいたかの手がかりになります。
描画の段階で出るエラー(React)
コンパイルは通っても、React が動かすときに止まるものがあります。evaluate で実行して描画した原文です。
| 書いたもの | 結果の原文 | 直し方 |
|---|---|---|
<p style="color:red"> |
The `style` prop expects a mapping from style properties to values, not a string. For example, style={{marginRight: spacing + 'em'}} when using JSX. |
style={{color: "red"}} |
<Card title="A">(import 忘れ) |
Expected component `Card` to be defined: you likely forgot to import, pass, or provide it. |
import するか components で渡す |
<p class="lead"> |
HTML は出るが、開発モードで警告 Invalid DOM property `class`. Did you mean `className`? |
className にする |
< と { を文字として書く3つの方法
本文に記号として < や { を書く方法は3つあります。1 \< 2、{"<"}、< の3通り と書いて描画すると、どれも < になりました。
本文に記号を書く
if (a<b) { ... } を地の文にそのまま書く。タグと式として読まれ、コンパイルが止まる`…` かコードブロックに入れる。地の文なら \<・\{ とエスケープするコードブロックの中は、MDX でもそのまま書ける
MarkdownをMDXにするとどれだけ壊れるか(README 152本の実測)
この節では、既存の Markdown を拡張子だけ .mdx に変えたとき、実際にどのくらいの割合でコンパイルが止まるか、その原因の内訳が分かります。
手元の node_modules にある README.md 152 本(合計 1,319,577 文字。MDX・remark・React などのパッケージの説明書)を、そのまま MDX としてコンパイルしました。エラーはファイルごとに最初の1件だけを数えています。
Unexpected character `!`最初に止まった原因の内訳と、HTML コメントを機械的に取り除いてから通し直した結果です。
| 原因(エラーの書き出し) | そのまま | コメント削除後 |
|---|---|---|
| HTML コメント(Unexpected character `!`) | 113本 | 0本 |
閉じていない <br>(Expected a closing tag for `<br>`) |
3本 | 7本 |
自動リンク <https://…>(Unexpected character `/`) |
2本 | 2本 |
閉じていない <img>(Unexpected closing tag `</a>`) |
1本 | 1本 |
引用符の無い属性値 width=200 など |
0本 | 2本 |
段落の中で閉じない <details> |
0本 | 1本 |
コメントを消すと、それまで隠れていた2番目以降の原因(<br>・引用符の無い属性など)が表に出てきます。HTML として許される「ゆるい書き方」が、JSX では通らないという点に集約されます。
- 1HTML コメントを {/* */} に置き換える今回はこれだけで 33本→139本 に増えた。最も効く1手
- 2br・img・hr を自分で閉じる
<br />・<img ... />の形にする - 3属性値を引用符で囲み、class・style を JSX の形にする
className、style={{…}} - 4<URL> を [文字](URL) にする自動リンクは MDX では使えない
- 54文字下げのコードを ``` で囲み直すエラーにならず段落になるため、目で探す
コンパイルの速さも比べました。通った 139 本(1,106,144 文字)を md と mdx で続けて2回ずつ通すと、1回目は md 12,516ms・mdx 9,180ms、2回目は md 6,601ms・mdx 7,366ms でした。共有の機械でばらつきが大きく、どちらが速いかは回によって入れ替わります。文書の量が同じなら、MDX にしてもビルド時間は同じ桁と考えてよい結果です。
Next.jsでMDXを使う(@next/mdx)
この節では、Next.js 16 の App Router で .mdx をページにする手順と、手元のビルドで出た結果・エラーの原文が分かります。
Next.js の入門は別の記事「Next.jsとは?使い方入門|Reactとの違いをJSオフで実測【v16】」で、App Router の基本は関連記事「Next.js App Router 入門|v16対応・実際のビルド結果つき」で扱っています。ここでは MDX の部分だけを見ます。
パッケージを入れる
npm install @next/mdx @mdx-js/loader @mdx-js/react @types/mdx(公式の手順。手元は JavaScript なので @types/mdx を除いた)。
next.config.mjs に withMDX を足す
pageExtensions に md・mdx を加えると、app/blog/page.mdx がそのままページになります。
mdx-components.jsx を置く
プロジェクトの最上位(app と同じ階層、src を使うなら src の中)に置きます。App Router では必須です。
next build で確かめる
手元では /blog が静的ページ(○ Static)として書き出されました。
// next.config.mjs
import createMDX from '@next/mdx'
const withMDX = createMDX({ options: { remarkPlugins: ['remark-gfm'] } })
export default withMDX({ pageExtensions: ['js', 'jsx', 'md', 'mdx'] })
// mdx-components.jsx(すべての MDX の h1 を差し替える例)
export function useMDXComponents(components) {
return { h1: (p) => <h1 style={{color: 'teal'}} {...p} />, ...components }
}
import {Note} from '../../components/Note.jsx'
export const metadata = { title: 'はじめての MDX' }
export const price = 1200
# はじめての MDX
税込みの価格は **{Math.round(price * 1.1)}円** です。
ビルドの出力と、書き出された HTML の一部です。export const metadata は Next.js のメタデータとして title に入り、h1 には mdx-components.jsx の style が付きました。
▲ Next.js 16.4.3 (Turbopack)
✓ Compiled successfully in 23.8s
✓ Generating static pages using 1 worker (3/3) in 582ms
Route (app)
┌ ○ /_not-found
└ ○ /blog
○ (Static) prerendered as static content
<title>はじめての MDX</title>
<h1 style="color:teal">はじめての MDX</h1>
<p>税込みの価格は <strong>1320円</strong> です。</p>
<aside class="note note-info"><p>ここは <em>Markdown</em> のまま書けます。</p></aside>
Next.js で出たエラーの原文
| 状況 | 原文 | 直し方 |
|---|---|---|
| remark-gfm を入れずに表を書いた | エラーは出ず、表が <p>| 形式 | JSX |…</p> の文字のまま出た |
remarkPlugins に remark-gfm を足す |
Turbopack でプラグインを関数のまま渡した([remarkGfm]) |
Error: loader .../@next/mdx/mdx-js-loader.js for match "{*,next-mdx-rule}" does not have serializable options. Ensure that options passed are plain JavaScript objects and values. |
['remark-gfm'] と名前の文字列で渡す |
| mdx-components.jsx を置かなかった | Error: Failed to collect configuration for /blog / [cause]: TypeError: c.default.createContext is not a function |
mdx-components.jsx を最上位に置く |
.mdx に <br> を書いた |
Error: Turbopack build failed with 1 error: ./app/blog/page.mdx ... page.mdx:18:1-18:11: Expected a closing tag for `<br>` (18:4-18:8) before the end of `paragraph` |
<br /> にする |
Next.js 16 の next build は既定で Turbopack を使います。公式ドキュメントにも「シリアライズできないオプションを持つ remark・rehype のプラグインは Turbopack ではまだ使えない(JavaScript の関数を Rust へ渡せないため)」とあり、手元でもそのとおりのエラーになりました。また、@next/mdx は front matter に既定では対応していません。記事の情報は export const metadata で書くか、remark-frontmatter などを足します。
CMS やデータベースから取ってきた MDX を表示する(ファイルではない)場合は、@next/mdx ではなく next-mdx-remote-client などの別のライブラリを使う形になります。その場合のサイズの考え方は、後の「先にコンパイルするか」の節を参照します。
AstroでMDXを使う(@astrojs/mdx)
この節では、Astro 7 で .mdx をページにする手順と、ビルドされた HTML の実物が分かります。
Astro の全体像は別の記事「Astroとは・使い方入門|JSゼロのブログをNext.jsと実測比較」で扱っています。MDX を使うには、npx astro add mdx(手元では手で npm install @astrojs/mdx と設定ファイルの追記)を行います。
// astro.config.mjs
import { defineConfig } from 'astro/config';
import mdx from '@astrojs/mdx';
export default defineConfig({ integrations: [mdx()] });
Astro では、部品に .astro のコンポーネントを使えます。中身を受け取るのは children ではなく <slot /> です。
---
// src/components/Note.astro
const { type = 'info' } = Astro.props;
---
<aside class={`note note-${type}`}><slot /></aside>
src/pages/blog/hello.mdx に、最初の5分と同じ本文と表を書いてビルドした結果です。
$ npx astro build
generating static routes
├─ /blog/hello/index.html (+23ms)
[build] 1 page(s) built in 1.29s
$ ls -l dist/blog/hello/index.html
434 dist/blog/hello/index.html
<!DOCTYPE html><meta charset="utf-8"/><h1 id="はじめての-mdx">はじめての MDX</h1>
<p>税込みの価格は <strong>1320円</strong> です。</p>
<aside class="note note-info"><p>ここは <em>Markdown</em> のまま書けます。</p></aside>
<table>
<thead>
<tr>
<th>形式</th>
...
1JavaScript はゼロ
dist に出たのは 434 バイトの HTML 1つだけ。部品は HTML に展開され、ブラウザーへ JS は送られなかった。
2表がそのまま出た
remark-gfm を足さなくても表になった。Astro の Markdown 設定(GFM が有効)を MDX も引き継ぐため。
3見出しに id が付いた
id="はじめての-mdx" が自動で付き、目次やアンカーリンクに使える。
front matter は追加のプラグイン無しで {frontmatter.title} のように使えます。要素の差し替えは、MDX の中で export const components = {…} と書くか、Content Collections から取り出した <Content components={…} /> に渡します。動く部品(React の部品など)を置いたときだけ、client:load などの指示を付けた部分の JavaScript が送られます。
DocusaurusなどでのMDXの扱い(Storybook・Marpとの違い)
この節では、ドキュメントサイトやコンポーネントのカタログで MDX がどう使われているかと、Markdown だけで動く道具との違いが分かります(この節は公式ドキュメントで確かめた範囲です)。
| 道具 | MDX の扱い | 気をつける点 |
|---|---|---|
| Docusaurus 3 | MDX 3 を使う。markdown.format の既定は 'mdx' で、.md も MDX として読む |
.md を CommonMark として読ませたいときは 'detect'(拡張子で判定)。実験的な機能 |
| Storybook | ドキュメントのページを MDX 3 で書ける。@storybook/addon-docs/blocks から Meta・Canvas を import |
表など GFM は remark-gfm を別に設定。ドキュメントの描画は React |
| Nuxt Content | MDX ではなく、Vue の部品を Markdown に書ける独自の MDC 記法を使う | 書き方が MDX と違う |
| Marp | MDX ではなく Markdown でスライドを作る | 部品は置けないが、.md をそのまま使える |
Docusaurus では、既定のままだと .md のファイルも MDX として読まれます。Markdown のつもりで書いた文書に HTML コメントや <br> があると、前の節の README と同じ理由でビルドが止まります。Docusaurus の公式ドキュメントは、Prettier が MDX v1 までしか対応していない点(崩れるなら {/* prettier-ignore */} か .prettierignore)と、独自の部品は大文字で始める点(小文字は HTML の要素として扱われる)も注意として挙げています。部品を import なしで全ページから使うには src/theme/MDXComponents.js で登録します。
Storybook の使い方は別の記事「Storybookとは・使い方|React+Viteで導入からテストまで実測」、Markdown でスライドを作る方法は別の記事「Marpとは・使い方|MarkdownからPDF・PPTXを出す手順と実測」で扱っています。Vue で Markdown に部品を書く方法は、関連記事「Nuxt Content v3の使い方|ブログ実装コードと実行結果」で扱っています。
先にコンパイルするか、表示のときにコンパイルするか(サイズと時間の実測)
この節では、MDX をビルドのときに JavaScript にしておく方法と、ブラウザーで表示するときにコンパイルする方法で、送るファイルの大きさと表示までの時間がどれだけ違うかが分かります。
同じ hello.mdx を表示するページを3通り作り、esbuild で圧縮(minify、production)してバンドルしました。
| 作り方 | JS の大きさ | gzip 後 | React だけとの差 |
|---|---|---|---|
| React だけ(h1 を1つ描くだけ) | 223,467 B | 69,102 B | 基準 |
| 先にコンパイルした MDX を import | 224,440 B | 69,516 B | +973 B(gzip +414 B) |
| ブラウザーで evaluate() してから描画 | 683,414 B | 202,458 B | +459,947 B(gzip +133,356 B) |
表示のときにコンパイルする方法では、MDX のコンパイラー一式(パーサーと acorn など)がブラウザーへ送られるため、JS が約3倍になりました。127.0.0.1 に置いたページを Chromium 153 で毎回新しいコンテキスト(キャッシュ無し)で7回開き、本文の p が現れるまでの時間の中央値を測った結果です。CPU の速さを4分の1にした条件も測りました。
| 条件 | 先にコンパイル | 表示のときにコンパイル | 差 |
|---|---|---|---|
| CPU そのまま | 225ms(182〜427) | 364ms(310〜600) | +139ms |
| CPU 4分の1 | 540ms(447〜963) | 785ms(695〜1,389) | +245ms |
記事の本文のように「書いた後は変わらない」文書は、ビルドのときにコンパイルしておくのが基本です。Next.js の @next/mdx と Astro の @astrojs/mdx はどちらもこの形でした。CMS から取ってくる MDX のようにビルドの時点で中身が決まらない場合も、サーバー側(Server Components や API)でコンパイルし、ブラウザーへはコンパイラーを送らない構成にすると、この差を避けられます。
なお、evaluate() や run() は文字列を JavaScript として実行します。MDX の公式ドキュメント(Getting started の Security の節)も「MDX はプログラミング言語であり、書き手を信頼できないなら安全ではない」と注意しています。利用者が投稿する文章に MDX を使うのは避け、投稿にはふつうの Markdown(HTML を無害化する設定)を使います。
MDXを使う・使わないの判断表
この節では、手元の結果をもとに、どんな文書・チームで MDX を選ぶとよいかが分かります。
| 状況 | 向く形式 | 理由 |
|---|---|---|
| 技術ブログ・製品ドキュメントで、注意書きやデモを部品で入れたい | MDX | 部品の追加が1行。見た目をサイト全体でそろえられる |
| README・議事録・メモ | Markdown | GitHub やエディターでそのまま読める。MDX にする利点が少ない |
| 書き手にエンジニアでない人が多い | Markdown(または CMS) | < や { の1文字でビルドが止まる。エラーの原文が英語 |
| 既存の .md が大量にある | まず Markdown のまま | 手元では 152本中119本がそのままでは止まった。移すなら一括の置換と確認が要る |
| 利用者の投稿を表示する | Markdown(無害化) | MDX は JavaScript として実行されるため |
| Vue・Nuxt のサイト | MDC(Nuxt Content)など | MDX の部品は React 系が中心。Vue 向けの記法がある |
| JavaScript をなるべく送りたくない | MDX+Astro | 部品が HTML に展開され、JS はゼロだった |
導入の順番は、次の段の形で考えると迷いません。
- 1Markdown だけで足りるか確かめる見出し・表・コード・画像で済むなら、MDX にしない
- 2使う部品を3〜5個に絞るNote(注意書き)・Tabs・Figure など、書き手が覚えられる数にする
- 3フレームワークの統合を入れるNext.js は @next/mdx と mdx-components、Astro は @astrojs/mdx
- 4GFM と front matter を決める表を使うなら remark-gfm。記事の情報の持ち方(front matter か export)をそろえる
- 5ビルドで止まる書き方を一覧にして共有するHTML コメント・br・<・{・行頭の import。CI でビルドを通して早めに気づく
esbuild や Vite で自分で組む場合は、@mdx-js/esbuild や @mdx-js/rollup をプラグインとして足します。Vite の設定ファイルの書き方は、関連記事「vite.configの書き方|用途別コピペ一覧とVite 8の変更点」で扱っています。
よくある質問
この節では、MDX について検索されやすい疑問に短く答えます。
Q. MDX とは何の略ですか。
公式の説明では Markdown と JSX を組み合わせた形式とされ、名前もこの2つから来ています。データベースの分野の MDX(MultiDimensional eXpressions)とは別のものです。
Q. .mdx ファイルはどうやって開けばいいですか。
中身はテキストなので、VS Code などのエディターで開けます。表示した結果を見るには、Next.js・Astro などでビルドするか、MDX の公式サイトの Playground に貼り付けます。辞書アプリ(MDict)の .mdx は別の形式で、エディターでは読めません。
Q. MDX は React がないと使えませんか。
React は必須ではありません。@mdx-js/mdx は JSX の実行環境を設定で選べ、Preact・Vue・Solid などと組み合わせられます。Astro では .astro の部品も使えました。ただし Storybook のドキュメントのように、道具の側が React 前提のこともあります。
Q. MDX の中で < や { を文字として書くには。\<・\{ とバックスラッシュでエスケープするか、{'<'} のように式で書きます。1 < 2 のように後ろが空白なら通りましたが、a<b や <2 は止まりました。
Q. MDX でコメントを書くには。{/* コメント */} と書きます。HTML のコメント記法はエラーになり、手元の README では止まった原因の9割以上がこれでした。
Q. MDX で表(テーブル)が表示されません。
MDX の既定は CommonMark で、表は GFM の機能です。remark-gfm を remarkPlugins に足します。Next.js の Turbopack では 'remark-gfm' と文字列で指定します。Astro は最初から表になりました。
Q. 「Could not parse expression with acorn」の意味は。{ } の中を JavaScript の式として読もうとして失敗した、という意味です。本文に波括弧を書いたなら \{ に、式のつもりなら文法の誤りを直します。if や for などの「文」は書けないため、別ファイルの関数にして import します。
Q. Markdown のファイルを .mdx に変えるだけで使えますか。
文章だけなら使えることもありますが、手元では README 152本のうち 119本がそのままでは止まりました。HTML コメント・<br>・自動リンク・引用符の無い属性を直す必要があります。
Q. MDX にすると表示が遅くなりますか。
ビルドのときにコンパイルしておけば、MDX の分の JS は 973 バイト(gzip 414 バイト)しか増えませんでした。ブラウザーでコンパイルすると約46万バイト増え、表示も 139ms 遅くなりました。
Q. MDX と Markdoc の違いは。
どちらも Markdown に部品を足す形式ですが、Markdoc は {% callout %} のような独自のタグを使い、JavaScript の式は書けません。Next.js の MDX のガイドの関連リンクにも挙がっています。式やコードを書き手に書かせたくない場合の候補になります。
まとめ
この節では、記事の要点を5つに絞って振り返ります。
- MDX とは、Markdown に JSX の部品・JavaScript の式・import/export を書ける形式。拡張子は .mdx で、ビルドのときに React などのコンポーネントへ変換される。
- 336 バイトの hello.mdx は 1,427 バイトの JavaScript になり、既定のエクスポートは props と components を受け取る関数だった。
- Markdown との違いは「HTML として許されるゆるい書き方」が通らない点。HTML コメント・
<br>・自動リンク・a<b・本文の{ }・行頭の import で止まり、4文字下げのコードは黙って段落になる。 - README 152本をそのまま MDX にすると 33本しか通らず、HTML コメントを消すと 139本に増えた。移すときは一括の置換とビルドでの確認が要る。
- Next.js は @next/mdx と mdx-components(必須)、Turbopack ではプラグインを文字列で指定。Astro は @astrojs/mdx で、表も見出しの id も最初から付き、JS はゼロだった。ブラウザーでコンパイルすると JS が約3倍になるため、先にコンパイルするのが基本。
参考・出典
確認日はいずれも 2026年10月29日です。
- MDX「What is MDX?」:https://mdxjs.com/docs/what-is-mdx/
- MDX「Troubleshooting MDX」:https://mdxjs.com/docs/troubleshooting-mdx/
- MDX「@mdx-js/mdx」(compile・evaluate・format オプション):https://mdxjs.com/packages/mdx/
- MDX「Getting started」(Security の節):https://mdxjs.com/docs/getting-started/
- MDX「Playground」:https://mdxjs.com/playground/
- Next.js「How to use markdown and MDX in Next.js」:https://nextjs.org/docs/app/guides/mdx
- Astro「@astrojs/mdx」:https://docs.astro.build/en/guides/integrations-guide/mdx/
- Docusaurus「MDX and React」:https://docusaurus.io/docs/markdown-features/react
- Docusaurus「docusaurus.config.js」(markdown.format):https://docusaurus.io/docs/api/docusaurus-config
- Storybook「MDX」:https://storybook.js.org/docs/writing-docs/mdx
- remark-gfm:https://github.com/remarkjs/remark-gfm
- Microsoft Learn「多次元式(MDX)リファレンス」:https://learn.microsoft.com/ja-jp/sql/mdx/multidimensional-expressions-mdx-reference
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「MDXとは?使い方とMarkdownとの違い|エラー原文と実測」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。