tech-book-labs
ガイド(言語 / 開発ツール) · 2 セクション統合

MDX 実装テクニック

MDX でドキュメント / 技術記事を書くための実装ノート。Shiki によるコードハイライト(dual theme / 差分表示 / 行ハイライト)、MDX に外部コンポーネントを注入するパターン(components prop / scope)までを 1 ページに統合。

著者:TechBook.net編集部 · 最終検証 2026-05-10
セクション · 2026-05-10 · shiki + @astrojs/mdx shiki@1.x / Astro 6.3 / MDX v3

MDX × Shiki でコードブロックを整える

Astro 6.x の MDX と Shiki の組み合わせで、技術記事のコードブロックを VSCode 同等の精度で見せるための設定パターン。dual テーマ / ファイル名タブ / 行ハイライト / diff / inline code を実装観点で整理。

検証日: 2026-05-09

使用バージョン: shiki@1.x / astro@6.3.0 / @astrojs/mdx@5.0.3

環境: Node 22.12 / pnpm 10

想定読者: Astro / Next.js / Docusaurus で MDX を書いている、コードブロックの見た目を整えたい人

技術記事の 半分はコードブロック。コードブロックの見た目で、記事の信頼度がはっきり変わります。 Shiki は VSCode と同じシンタックスハイライタで、TextMate grammar ベースの精度の高さが強みです。

なぜ Shiki にしたか

別の選択肢(Prism / highlight.js / Starry Night)と比べた時、Shiki が選ばれる理由:

  • VSCode と同じ grammar で言語精度が高い(TypeScript の細かい型構文や JSX 内ネストにも強い)
  • dual テーマ(light / dark)を 1 回のレンダリングで両対応 できる(色を CSS 変数として埋め込み、prefers-color-scheme で切替)
  • Astro が標準で組み込んでいる(astro.config.mjsshikiConfig を渡すだけ)

逆に、ランタイムでハイライトする(LLM 出力をその場で表示するなど)用途では、shiki/wasm の bundle サイズが課題になることも(後述)。

Shiki dual-theme の仕組み — 1 回の build で 2 色を CSS 変数として同居させる (クリックで拡大)

最小設定(Astro 6.x + MDX)

Astro 標準の shikiConfigdual theme(light + dark)と defaultColor: false を指定するだけで、コードブロックに 2 色分の inline style 変数が埋まる:

import { defineConfig } from "astro/config";
import mdx from "@astrojs/mdx";

const shikiConfig = {
  themes: { light: "github-light", dark: "github-dark" },
  wrap: true,
};

export default defineConfig({
  integrations: [
    mdx({ shikiConfig }),
  ],
  markdown: { shikiConfig },
});

デュアルテーマ(自動切替)

themes: { light: ..., dark: ... } を指定すると、Astro / Shiki は <pre style="--shiki-light: #...; --shiki-dark: #..."> の形で 両テーマ分の色 CSS 変数を埋め込みます

CSS 側で prefers-color-scheme に応じて変数を上書きすれば、自動的にダークモード対応になる:

@media (prefers-color-scheme: dark) {
  pre, code {
    color-scheme: dark;
  }
}

class 切り替えで dark mode を制御するなら defaultColor: false を指定して、自前 CSS で --shiki-dark を有効化する処理を書きます。

ファイル名タブ

fence の言語名の後に title="..." を付けると、shiki が data-filename 属性を出してくれる。CSS で「タブ風」表示に整える:

import { sql } from "./client";
export async function findUser(id: string) {
  return sql`SELECT * FROM users WHERE id = ${id}`;
}

上の例は title="src/lib/db.ts"info string(言語の後ろに半角スペース)で書いています。Astro / Next.js / Docusaurus いずれもこの記法を概ねサポート。

js:filename.js のようにコロン区切りで書く流派もありますが、MDX v3 ベースでは title="..." のほうが portable

差分表示(diff)

```diff fence で 行頭 + / - が緑 / 赤に色付く。before/after の対比をコードで書ける:

- const result = await fetch(url);
+ const result = await fetch(url, { signal: controller.signal });

```` の後ろに diff を指定するだけで + / - の行が緑 / 赤に着色されます。 コードブロックの diff として使うのは「小さなスニペットの before/after」が中心。長大な diff は GitHub のリンクに貼ったほうが読みやすい。

行ハイライト(rehype 拡張)

特定の行を強調する記法は Astro 標準には組み込まれていません(rehype プラグインが必要)。 推奨は expressive-code(Astro 専用 integration あり)で、ファイル名タブ + 行ハイライト + 差分を統合的に扱えます。

// 例: expressive-code を使う場合の astro.config.mjs(抜粋)
import expressiveCode from "astro-expressive-code";
import mdx from "@astrojs/mdx";

export default defineConfig({
  integrations: [
    expressiveCode({ themes: ["github-light", "github-dark"] }),
    mdx(),  // ← expressive-code の **後** に置く
  ],
});

inline code(highlight 付き)

backtick の中で {:言語名} を付けるとインラインも shiki でハイライトされる(地の文中の短いコード片に効果的):

インラインで {`const x: number = 1`} のように書ける。

Astro の <Code> コンポーネントを使えば確実にハイライトされる:

---
import { Code } from "astro:components";
---

<Code code="const x: number = 1" lang="ts" inline />

API リファレンスのような inline code を多用する記事では、専用 component を 1 つ作っておくと一貫性が出ます。

つまずいたポイント

  • mdx({ shikiConfig })markdown.shikiConfig のどちらかを忘れる.md.mdx で見た目が違う事故が起きる。両方に同じ config を渡す
  • CSS 変数の prefix を変えると Astro 標準のダークモード切替が壊れる--shiki- prefix は変えない
  • 大きいコードブロックで build が遅くなる — Shiki は build 時にレンダリング。数千行貼ると build が重くなるので、抜粋 + 「全文は別 link」 に分割する
  • 言語指定子のタイポは無音で fallback するjs / javascript は OK だが、javasc は灰色になる(エラーにならない)
  • VSCode の表示と若干違う — VSCode は別 grammar を併用していることがある。完全一致は期待しない

評価

観点評価コメント
言語精度VSCode と同じ grammar、TypeScript / JSX に強い
設定の手軽さAstro / Next.js では config 数行で動く
dual テーマCSS 変数経由で 1 回の build で両対応
ランタイム性能build 時レンダリングが基本、ランタイムは bundle 重め
エコシステムrehype-pretty-code / expressive-code など補助プラグインが揃う

向く / 向かないケース

  • 向く: MDX/MD ベースの技術記事、ドキュメントサイト、ブログ、書籍
  • 向かない: ランタイム生成コード(LLM 出力など)→ shiki/wasm を browser ロードする選択肢はあるが、bundle サイズに注意
  • 向かない: 数千行級のコードブロック → 抜粋 + link or embed gist のほうが読まれる

関連 Topic / 関連書籍

この記事と関係する tech-book.net の Topic と、それぞれの Topic に紐づく書籍:

tech-book.net /books/9784814401093

Effective TypeScript(第2版) : 型システムの力を最大限に引き出す83項目

Dan Vanderkam/今村 謙士 · オライリージャパン · 2025年 · ¥4,620

急速に普及が進んでいるTypeScriptの実用書! TypeScriptの実用書。TypeScript…

詳細を tech-book.net で見る
tech-book.net /books/9784297129163

TypeScriptとReact/Next.jsでつくる実践Webアプリケーション開発

手島 拓也/吉田 健人/高林 佳稀 · 技術評論社 · 2022年

新しいフロントエンドの入門書決定版! 本書はReact/Next.jsとTypeScriptを用いてWebアプリケーションを開…

詳細を tech-book.net で見る
tech-book.net /books/9784297137397

かんたん TypeScript

HIRO · 技術評論社 · 2023年

本書は、「広く・正しく・新しく」をコンセプトにTypeScriptでプログラミングをはじめるにあたって基本的なことはすべて学習できる内容となっています。また、イラストによる図解方式で概念をやさしく解説している…

詳細を tech-book.net で見る
tech-book.net /books/9784297126353

ゼロからわかる TypeScript入門

WINGSプロジェクト 齊藤新三/山田 祥寛
詳細を tech-book.net で見る
tech-book.net /books/9784297127473

プロを目指す人のためのTypeScript入門 安全なコードの書き方から高度な型の使い方まで

鈴木 僚太 · 技術評論社 · 2022年

TypeScriptは、JavaScriptに静的型付けの機能を加えたオープンソースのプログラミング言語です。本書では、根幹となるJavaScr…

詳細を tech-book.net で見る
セクション · 2026-05-10 · @astrojs/mdx 5.0.x

MDX に Astro / React コンポーネントを組み込む実装パターン

MDX への component 注入の 4 つの流儀 — frontmatter import、components prop、グローバル MDX 拡張(remark/rehype)、Provider による context 共有。Astro と React Island の使い分けと、よく詰まる SSR/hydration 問題を実装メモ化。

検証日: 2026-05-10

使用バージョン: @astrojs/mdx@5.0.x / astro@6.3.x

対象: MDX で素朴な markdown は書けた、components を埋め込んで動的記事にしたい人

MDX = Markdown 構文の中に JSX(component)を直接書けるフォーマット。Astro では .mdx ファイルが Astro / React コンポーネントの両方を受け入れる。本稿は 何種類かある「component の流し込み方」 を整理する。

4 流儀の選び分け — 大半は 1(frontmatter import)+ 2(components prop)で十分、3-4 は特殊用途 (クリックで拡大)

1. frontmatter から import(最頻出)

.mdx の冒頭で他のファイルから component を import すれば、そのまま JSX として使える。

---
title: "..."
---

import Callout from "@/components/Callout.astro";
import { MyChart } from "@/components/MyChart.tsx";

# 記事タイトル

<Callout type="warn" title="重要">
  本文中で出した警告。
</Callout>

<MyChart client:load />

ポイント:

  • .astro.tsx も import 可
  • React 系は hydration directive が必要(client:load / client:visible / client:idle / client:only="react")
  • path alias(@/components)が tsconfig.jsonpaths で動く
  • import 文は frontmatter の 、本文の前 に書く

2. components prop で「グローバル差し替え」

特定の HTML タグ(a / h2 / code / img 等)を 記事ごとに統一して上書き したい場合は、render 時の components prop を使う。

---
// src/pages/articles/[...slug].astro
import { getCollection, render } from "astro:content";
import BookLink from "@/components/BookLink.astro";
import CustomLink from "@/components/CustomLink.astro";

const { entry } = Astro.props;
const { Content } = await render(entry);
---

<Content
  components={{
    BookLink,                  // MDX で <BookLink /> を有効化
    a: CustomLink,             // すべての <a> を CustomLink に差し替え
    code: SmartInlineCode,     // インラインコードを別 component に
  }}
/>

ポイント:

  • HTML タグ名 lowercase で markdown が出力する要素を上書き(a, h1-h6, code, pre, img, table, etc.)
  • PascalCase 名で MDX の <BookLink /> を有効化(import なしでも使える)
  • 記事側で都度 import するより楽だが、grep ability(検索可能性)が下がるので使い分け

3. 全 MDX に共通注入(MDXProvider 風 / Astro での代替)

MDX 公式の MDXProvider は Astro では使わない代わりに、全 MDX で共通の wrapper を作る:

---
// src/components/MdxArticle.astro
import BookLink from "@/components/BookLink.astro";
import Callout from "@/components/Callout.astro";

const { Content } = Astro.props;
const components = { BookLink, Callout };
---

<article class="prose">
  <Content components={components} />
</article>

<!-- 使用 -->
<MdxArticle Content={entry.Content} />

これで BookLink / Callout は全 MDX で import 不要に使える。記事側のフロントマター import を簡素化できる。

4. remark / rehype プラグインで「構文レベル」拡張

文中で繰り返す表記(例:{{book:isbn}} を BookLink に変換、@user を mention に変換)は remark プラグイン で AST 操作する:

// astro.config.mjs
import { defineConfig } from "astro/config";
import mdx from "@astrojs/mdx";
import { visit } from "unist-util-visit";

function remarkBookShortcode() {
  return (tree: any) => {
    visit(tree, "text", (node, index, parent) => {
      const match = node.value.match(/\{\{book:([0-9]+)\}\}/);
      if (!match) return;
      const isbn = match[1];
      // text node を JSX 要素に置換
      parent.children.splice(index, 1, {
        type: "mdxJsxFlowElement",
        name: "BookLink",
        attributes: [{ type: "mdxJsxAttribute", name: "isbn", value: isbn }],
        children: [],
      });
    });
  };
}

export default defineConfig({
  integrations: [mdx({ remarkPlugins: [remarkBookShortcode] })],
});

これで {{book:9784297127473}} と書くだけで <BookLink isbn="9784297127473" /> に変換される。長文記事で同じパターンを大量に書く場合にだけ採用(maintain コストとの trade-off)。

5. SSR vs Client(hydration)

directiveタイミング用途
なし(.astro 内)SSR のみ、JS なし静的表示
client:loadmount 直後 hydrate即座にインタラクション必要
client:visibleviewport 入った時下にある interactive demo
client:idleアイドル時重要度低めの enhancement
client:only="react"SSR せずに client 側だけwindow / leaflet / dnd-kit 等

client:only="react" を選ぶ典型ケース:

  • window / document を module 評価時に触る(leaflet / 一部 d3)
  • useSortable の aria 属性が SSR と client で違って hydration mismatch
  • uuid() のランダム値useState 初期値で呼ぶ
  • localStorage に依存する初期 state

6. .mdx と .astro の相互運用

.astro から .mdxContent を呼び出せるし、.mdx から .astro を import もできる。

---
title: "..."
---

import StaticAstroComponent from "@/components/Static.astro";
import InteractiveReactComponent from "@/components/Interactive.tsx";

<StaticAstroComponent />              <!-- SSR、追加 JS なし -->
<InteractiveReactComponent client:visible />  <!-- viewport hydrate -->

実装ルール:

  • 静的要素は .astro(BookLink / Callout / Sparkline 等)
  • 状態管理が必要な要素は .tsx(form / editor / chart)
  • どちらでも書ける時は .astro 優先(bundle 削減)

7. MDX 内で動かない / 詰まる構文

MDX で「型の説明」「テンプレ literal の説明」を書く時は、以下の構文がトラブルになる:

  • {...}(波括弧) = MDX が JSX expression と解釈する。fenced code block の中なら安全、外で書きたい時は HTML entity(&#123;&#125;)へ
  • < を含む文字列(generic / HTML タグの説明)= JSX タグ開始と誤認。バッククォート inline code で囲むか HTML entity に
  • <Type<T>> のような generic = inline code(バッククォート)の中なら OK、地の文では破綻する
  • > 5 連続 = blockquote と誤認(あまり起きないが SQL の例で要注意)

つまずいたポイント

  • <Callout> を使ったら Callout is not defined:frontmatter の import 文を書き忘れ。component を使う前に import が必須
  • components prop で a を上書きしたら全部 onClick が壊れる:CustomLink 内で href / class / その他の prop を spread 渡し忘れ。<a {...props} /> で透過する
  • remark プラグインを書いたら build が遅くなった:すべての text node を visit するため。AST 操作は 必要な node 型に絞る(visit(tree, "text", ...) ではなく具体的な条件で早期 return)
  • client:only="react" で children が消える:Astro の component を子に渡せない、React tree のみ
  • client:load を書いたのに動かない:framework="react" integration が足りていない、@astrojs/react を install + integrations に追加

関連 Topic / 関連書籍

この記事と関係する tech-book.net の Topic と、それぞれの Topic に紐づく書籍:

tech-book.net /books/9784814401093

Effective TypeScript(第2版) : 型システムの力を最大限に引き出す83項目

Dan Vanderkam/今村 謙士 · オライリージャパン · 2025年 · ¥4,620

急速に普及が進んでいるTypeScriptの実用書! TypeScriptの実用書。TypeScript…

詳細を tech-book.net で見る
tech-book.net /books/9784297129163

TypeScriptとReact/Next.jsでつくる実践Webアプリケーション開発

手島 拓也/吉田 健人/高林 佳稀 · 技術評論社 · 2022年

新しいフロントエンドの入門書決定版! 本書はReact/Next.jsとTypeScriptを用いてWebアプリケーションを開…

詳細を tech-book.net で見る
tech-book.net /books/9784297137397

かんたん TypeScript

HIRO · 技術評論社 · 2023年

本書は、「広く・正しく・新しく」をコンセプトにTypeScriptでプログラミングをはじめるにあたって基本的なことはすべて学習できる内容となっています。また、イラストによる図解方式で概念をやさしく解説している…

詳細を tech-book.net で見る
tech-book.net /books/9784297126353

ゼロからわかる TypeScript入門

WINGSプロジェクト 齊藤新三/山田 祥寛
詳細を tech-book.net で見る
tech-book.net /books/9784297127473

プロを目指す人のためのTypeScript入門 安全なコードの書き方から高度な型の使い方まで

鈴木 僚太 · 技術評論社 · 2022年

TypeScriptは、JavaScriptに静的型付けの機能を加えたオープンソースのプログラミング言語です。本書では、根幹となるJavaScr…

詳細を tech-book.net で見る

全ガイドは ガイド一覧 から。連載は 連載一覧 へ。