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-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 経由

参考コード(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 と併用する理由

実際の導入ステップ

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 が認識する言語名を使用していることを確認する。

対応例:

注意: diff-tsx のような複合指定は Shiki では非対応。差分を表現したい場合は、@shikijs/transformerstransformerNotationDiff を使用するとのこと。この辺はまた別記事としてまとめたい。

まとめ

参考