Shikiとrehype-pretty-codeでMDXのシンタックスハイライトを移行した記録

Next.js(App Router)のMDXハイライトを rehype-highlight + highlight.js から Shiki + rehype-pretty-code に移行したメモ。 ライトテーマは vitesse-light、ダークは vitesse-dark。 行番号はとりあえずなし、シンプルなコードブロックを実装する。

Shikiとは

VS Codeと同じくTextMateベースのシンタックスハイライタ。テーマや言語定義をそのまま使えるため、エディタと近い配色を再現しやすい。
トークンごとにインラインstyleとCSSカスタムプロパティ(--shiki-*)を出力するのが特徴。

出力例:

console.log("test");

実際に出力されるコード:

<code data-language="tsx" data-theme="vitesse-light vitesse-dark" style="display:grid">
  <span data-line="">
    <span style="--shiki-light:#E45649;--shiki-dark:#BD976A">console</span>
    <span style="--shiki-light:#383A42;--shiki-dark:#666666">.</span>
    <span style="--shiki-light:#4078F2;--shiki-dark:#80A665">log</span>
    <span style="--shiki-light:#383A42;--shiki-dark:#666666">(</span>
    <span style="--shiki-light:#50A14F;--shiki-dark:#C98A7D77">"</span>
    <span style="--shiki-light:#50A14F;--shiki-dark:#C98A7D">test</span>
    <span style="--shiki-light:#50A14F;--shiki-dark:#C98A7D77">"</span>
    <span style="--shiki-light:#383A42;--shiki-dark:#666666">);</span>
  </span>
</code>

rehypeとは

Markdown / MDXをHTML(HAST)に変換して処理するためのパイプライン。
rehype-* プラグインを挟むことで、コードハイライトや要素変換、属性付与などの前処理を行える。今回利用する rehype-pretty-code も、rehype段でコードブロックを装飾するプラグインの一つ。

なぜShiki?これまでとの違い

  • これまで: rehype-highlight + highlight.js のCSSを読み込み
    • 導入は手軽だが、テーマ選択やダーク / ライト切り替えを自前のCSSで調整する必要があった。
  • これから: Shikiにテーマを指定し、VS Codeと近い配色を再現。
    rehype-pretty-code を組み合わせることで、MDXのコードフェンスを自動的にShikiハイライトへ変換できる。

rehype-pretty-codeとは(類似ライブラリとの違い)

rehype プラグインの一つで、MDX中のコードフェンスをShikiでハイライトし、行番号・行ハイライト・ファイル名表示などの装飾用HTMLを生成してくれる。

類似ライブラリとしては、rehype-prism-plus(Prism系)、rehype-highlight(highlight.js系)、rehype-shiki(シンプルにShikiを噛ませるだけ)などがある。
装飾機能まで含めて一括で管理したい場合は、rehype-pretty-code が便利。

参考: Shiki単体で使う場合

Shiki公式のNext.jsガイドでは、codeToHtml / codeToHast を直接呼び出し、生成したHTML / HASTをコンポーネントに埋め込む構成が紹介されている(例: サーバーコンポーネントで codeToHtml して dangerouslySetInnerHTML する)。

この方法だとDOM構造とCSSを自前で完全に制御できる一方で、MDXのコードフェンスを自動変換したい場合は、別途remark / rehype側の追加処理が必要になる。

参考コード(RSCでの直呼び):

import { codeToHtml } from "shiki";
 
type Props = {
  code: string;
  lang: "tsx" | "diff";
};
 
export default async function CodeBlock({ code, lang }: Props) {
  const html = await codeToHtml(code, {
    lang,
    theme: "vitesse-dark", // lightを使うなら vitesse-light など
  });
 
  return (
    <div className="markdown-body" dangerouslySetInnerHTML={{ __html: html }} />
  );
}

今回採用した方法: rehype-pretty-code経由

  • lib/note.ts / lib/schedule.tsrehypePluginsrehype-pretty-code を設定。テーマは { light: "vitesse-light", dark: "vitesse-dark" }
  • 行番号は使用しない(オプション無効)。
  • コードフェンスの言語はShikiが理解するものを使う。差分なら```diff、TSXなら```tsx。 Shiki本体には diff-tsx のような複合言語IDはない。差分表示をしたい場合は、lang="tsx" のまま @shikijs/transformerstransformerNotationDiff を使い、コード内で // [!code ++]// [!code --] の記法で差分を表現する。

参考コード(MDXのrehypePlugins設定):

// lib/note.ts 抜粋
import { compileMDX } from "next-mdx-remote/rsc";
import rehypePrettyCode from "rehype-pretty-code";
 
const rehypePrettyCodeOptions = {
  theme: {
    light: "vitesse-light",
    dark: "vitesse-dark",
  },
  // dual theme を指定すると、CSS変数が --shiki-light, --shiki-dark の形式で出力される
  // メディアクエリで切り替える必要がある
  // 行番号などの装飾を ON にしたいときはここにオプションを追加
};
 
const { content } = await compileMDX({
  source: fileContent,
  options: {
    parseFrontmatter: true,
    mdxOptions: {
      rehypePlugins: [[rehypePrettyCode, rehypePrettyCodeOptions]],
    },
  },
});

pre[data-theme] と --shiki-* の扱い

rehype-pretty-codepre/codedata-themestyle="--shiki-light: ..." のようなカスタムプロパティを埋め込む。 CSS側ではこれらをそのまま color / background-color にマッピングすればよい。最低限は次のイメージ。

.markdown-body pre[data-theme] {
  color: var(--shiki-light);
  background: var(--shiki-light-bg);
}
 
@media (prefers-color-scheme: dark) {
  .markdown-body pre[data-theme] {
    color: var(--shiki-dark);
    background: var(--shiki-dark-bg);
  }
}

Shikiだけではだめ? rehype-pretty-codeと併用する理由

  • Shikiの直呼びだけでもハイライト自体は可能。ただし、行番号や行ハイライト、コードフェンス → HTML変換の自動処理は自前実装が必要。
  • rehype-pretty-code を挟むと、MDXのコードフェンスをそのまま変換し、装飾用のDOMとクラス / 属性を用意してくれる。今回は行番号をオフにして最小構成で利用している。

実際の導入ステップ

1. 依存パッケージの入れ替え

既存の highlight.jsrehype-highlight を削除し、shikirehype-pretty-code をインストール。

yarn remove rehype-highlight highlight.js
yarn add shiki rehype-pretty-code

2. CSSの修正

2-1. highlight.jsのCSS importを削除

app/layout.tsxapp/globals.css などから、以下のようなhighlight.jsのテーマ読み込みを削除。

/* 削除: highlight.js のテーマ */
@import 'highlight.js/styles/github.css';

2-2. Shiki用のCSSを追加

app/globals.css や専用のスタイルファイルに、ShikiのCSS変数を使ったスタイルを追加。

/* Shiki のライト/ダークテーマ切り替え */
.markdown-body pre[data-theme] {
  color: var(--shiki-light);
  background: var(--shiki-light-bg);
  padding: 1rem;
  border-radius: 0.5rem;
  overflow-x: auto;
}
 
@media (prefers-color-scheme: dark) {
  .markdown-body pre[data-theme] {
    color: var(--shiki-dark);
    background: var(--shiki-dark-bg);
  }
}
 
/* コードブロック内のテキストは親から継承 */
.markdown-body pre[data-theme] code {
  color: inherit;
  background: transparent;
  font-size: 0.875rem;
  line-height: 1.6;
}

3. MDX設定の変更

3-1. lib/note.tsの修正

compileMDXrehypePlugins 設定を変更。

変更前:

import rehypeHighlight from 'rehype-highlight';
 
const { content } = await compileMDX({
  source: fileContent,
  options: {
    mdxOptions: {
      rehypePlugins: [rehypeHighlight],
    },
  },
});

変更後:

import rehypePrettyCode from 'rehype-pretty-code';
 
const rehypePrettyCodeOptions = {
  theme: {
    light: "vitesse-light",
    dark: "vitesse-dark",
  },
};
 
const { content } = await compileMDX({
  source: fileContent,
  options: {
    parseFrontmatter: true,
    mdxOptions: {
      rehypePlugins: [[rehypePrettyCode, rehypePrettyCodeOptions]],
    },
  },
});

3-2. 他のMDX使用ファイルをの修正

このサイトでは lib/schedule.ts でもMDXを使っているため、同じ変更を適用。

4. コードフェンスの言語指定を確認

MDXファイル内のコードブロックで、Shikiが認識する言語名を使用していることを確認する。

対応例:

  • TypeScript: ```ts または ```typescript
  • TSX: ```tsx
  • Diff: ```diff
  • Shell: ```shell または ```bash
  • CSS: ```css など

注意: diff-tsx のような複合指定はShikiでは非対応。差分を表現したい場合は、@shikijs/transformerstransformerNotationDiff を使用するとのこと。この辺はまた別記事としてまとめたい。※追記:別記事にまとめました([Next.js MDX] シンタックスハイライト付き差分表示を実装する(@shikijs/transformers))

まとめ

  • ハイライトはShiki + rehype-pretty-codeでVS Code互換の色を再現しつつ、MDXコードフェンスを自動変換。
  • テーマは vitesse-light / vitesse-dark、行番号なし。CSSは pre[data-theme]--shiki-* を素直に使う。
  • Shiki直呼びでも構成可能だが、装飾や自動変換を含めて楽をしたいなら rehype-pretty-code 併用が現実的。

参考