Nextjsで@next/mdxを使ったマークダウンの変換でシンタックスハイライトを実装する

## はじめに
Nextjsでは[`@next/mdx`](https://github.com/vercel/next.js/tree/canary/packages/next-mdx)を用いることで簡単にマークダウンを扱えます。
[ドキュメント](https://nextjs.org/docs/app/building-your-application/configuring/mdx)によれば、依存ライブラリのインストールと`next.config.js`に`@next/mdx`の設定を注入するだけで導入できます。
```sh
npm install @next/mdx @mdx-js/loader @mdx-js/react @types/mdx
```
```js:next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {};
 
const withMDX = require('@next/mdx')();
module.exports = withMDX(nextConfig);
```


さらに、`@next/mdx`ではマークダウンから変換するReactコンポーネントを`mdx-components.tsx`ファイルを用いて置き換えられます。
```tsx:mdx-components.tsx
import type { MDXComponents } from 'mdx/types';

export function useMDXComponents(components: MDXComponents): MDXComponents {
  return {
    h1: (props) => {
      return <h2 className="mt-10 text-3xl font-bold" {...props} />;
    },
    ...components,
  };
};
```
例ではマークダウンとして`#`で装飾され、htmlとして`h1`に変換されるものを下のようなReactコンポーネントに変換します。
```tsx
const H2 = (props) {
  return <h2 className="mt-10 text-3xl font-bold" {...props} />;
};
```
:::note info 
余談ですが、この例のように`tailwindcss`を用いる場合は`tailwind.config.js`の`content`に`mdx-components.tsx`を含めることを忘れないようにしてください。忘れていた場合、他の場所で使っているクラスしかスタイルが反映されないので注意してください。
```js:tailwind.config.js
// デフォルトだと./src/mdx-components.tsxは読み込まれない設定
content: [
  './src/pages/**/*.{js,ts,jsx,tsx,mdx}',
  './src/components/**/*.{js,ts,jsx,tsx,mdx}',
  './src/app/**/*.{js,ts,jsx,tsx,mdx}',
]
// src配下を全部読み込ませる
content: [
  './src/**/*.{js,ts,jsx,tsx,mdx}',
]
// 追加する
content: [
  './src/pages/**/*.{js,ts,jsx,tsx,mdx}',
  './src/components/**/*.{js,ts,jsx,tsx,mdx}',
  './src/app/**/*.{js,ts,jsx,tsx,mdx}',
  './src/mdx-components.tsx',
]
```
:::

このようにシンプルで便利な機能も備わっているので多くの実装はこのまま拡張することで十分ですが、シンタックスハイライトの実装はこのままでは綺麗にできませんでした。
この記事では`@next/mdx`でシンタックスハイライトを行う方法を紹介します。

この記事で作成する[アプリケーション](https://example-mdx.vercel.app/)はこちらのリポジトリを参照してください。

https://github.com/KokiSakano/example-mdx

## JSXへ変換するタイミングでシンタックスハイライトを行う
まず、最初に説明した`mdx-components.tsx`を用いて実装してみます。
`mdx-components.tsx`でシンタックスハイライトを実装するには`code`の変換先のReactコンポーネントを用意する必要があります。
Reactでシンタックスハイライトを実装するときは[highlight.js](https://highlightjs.org/)や[prismjs](https://prismjs.com/)などのライブラリを用いたり、それらをReact用にカスタマイズされたものを使うことが多いと思います。
今回は`highlight.js`を使って実装します。
```tsx
'use client';

import { useRef, useLayoutEffect, ReactNode } from 'react';
import hljs from 'highlight.js';
import 'highlight.js/styles/nord.css';

export const SyntaxHighlight = (
  { children }: { children: ReactNode },
) => {
  const codeRef = useRef<HTMLElement>(null);

  useLayoutEffect(() => {
    if (codeRef.current) {
      hljs.highlightBlock(codeRef.current);
    }
  }, []);

  return (
    <code ref={codeRef}>
      {children}
    </code>
  );
};
```
`useRef`と`useEffect`もしくは`useLayoutEffect`を使って`code`を装飾するのでクライアントコンポーネントとして実装しました。さらに、`highlight.js`のstyleを反映するためにcssを直接このファイルから読み込むようにしました。
これを`mdx-components.tsx`に設定して
```tsx:mdx-components.tsx
code: ({ children }) => <SyntaxHighlight>{children}</SyntaxHighlight>,
```
下のようなコードを埋め込んだマークダウンを表示させてみます。
```tsx
export const Counter: FC = () => {
    const [count, setCount] = useState(0);
    return <button onClick={() => setCount(count + 1)}>{count}</button>;
};
```
![画面収録_2023-06-23_18_26_01_AdobeExpress.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/591069/9ab56065-9fd2-1b25-23ff-e31c6f7bbd92.gif)
一瞬だけ素の状態が見えてからそれらがハイライトされました。これは最初にHTMLの一部として素の`code`がサーバーから送られてきて、ハイドレーション時にシンタックスハイライトしていることが原因です。
そのためサーバー側であらかじめ変換を施すReactコンポーネントを作成して解決したかったのですが、Reactコンポーネントで使われる多くのシンタックスハイライトの手法ではクライアント側で行わせる処理を用いたものが多く画面のチラつきを受け入れるしかありませんでした(これに対する良いアプローチがあれば教えていただけると幸いです)。
次に紹介する方法ではこのチラつきをなくすためのアプローチとして、htmlへ変換する時にシンタックスハイライトを行います。

## HTMLへ変換するタイミングでシンタックスハイライトを行う
大雑把な説明となりますが`@next/mdx`は、マークダウンをASTにパースする処理を[`remark`](https://remark.js.org/)で、それをHTMLのASTに変換する処理を[`rehype`](https://unifiedjs.com/)で行い、さらにそれを`mdx-components`に定義した通り(なければ[`@mdx-js/react`](https://github.com/mdx-js/mdx/blob/main/packages/react/lib/index.js)そのままの定義)に変換してマークダウンをReactコンポーネントとして扱えるようにしています。これらの変換は[`mdx-js/loader`](https://mdxjs.com/packages/loader/)に書かれているので詳細を知りたい場合はそちらを参照してください(変換処理自体は[`mdx-js/mdx`](https://mdxjs.com/packages/mdx/)の`core.js`周りに書かれています)。

:::note warn
現在`@next/mdx`で実験的機能である`mdxRs`ではRustを使ったコンパイラを利用するため処理系が上記とは異なります。そのため、これから説明する方法は実験的機能を使っていない前提となります。
:::

このような変換過程があるので`@next/mdx`では`next.config.js`で`rehype`や`remark`に関連するプラグインを導入するオプションを設定できます。そのため、`rehype`のプラグインである[rehype-pretty-code](https://rehype-pretty-code.netlify.app)を通じてHTMLのASTに変換する時点でシンタックスハイライトを行ってみます。

これらのプラグインを設定するために必要なパッケージをインストールして`next.config.js`を修正します。
```sh
npm i rehype-pretty-code shiki
```
[`shiki`](https://shiki.matsu.io/)は`highlight.js`と同じようにシンタックスハイライトを行うライブラリで、`rehype-pretty-code`はこれを利用して装飾します。
```js:next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {};
 
const withMDX = require('@next/mdx')({
  options: {
    rehypePlugins: [
      [
        require('rehype-pretty-code'),
      ],
    ],
  },
});
module.exports = withMDX(nextConfig);

```
これらの設定によって以下のように画面へ表示されます。

![スクリーンショット 2023-06-26 15.48.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/591069/2c3f84e1-37eb-842d-a830-8d52e4f66182.png)

これでも十分ですが、少し味気ないです。`rehype-pretty-code`ではシンタックスハイライトのスタイリングを`shiki`の[提供するテーマ](https://github.com/shikijs/shiki/blob/main/docs/themes.md#all-themes)を選択できます。
`github-light`にしたいときは以下のようにします。
```js:next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {};

const withMDX = require('@next/mdx')({
  options: {
    remarkPlugins: [],
    rehypePlugins: [
      [
        require('rehype-pretty-code'),
        /** @type {Partial<import("rehype-pretty-code").Options>} */
        ({
          theme: "github-light",
          getHighlighter: require('shiki').getHighlighter,
        }),
      ],
    ],
  },
});
module.exports = withMDX(nextConfig);
```
これによってgithubで設定がライトモードの時のようなコードブロックになります。

![スクリーンショット 2023-06-26 14.39.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/591069/b294087a-3548-fe1a-33b4-b460d0fd6594.png)

この方法であればビルド時にシンタックスハイライトされたhtmlを生成できるので、先ほどのようなブラウザへ表示された時に生じるチラつきがなくなります。

## おわりに
Nextjsで`@next/mdx`を利用してマークダウンをHTMLへ変換する時にシンタックスハイライトする方法を紹介しました。
紹介した内容を参考にすればかなり簡単にマークダウンベースのブログサイト作れます。
カスタマイズ性も高いので、是非この記事を参考にブログを作ってみてはいかがでしょうか。

今回はシンタックスハイライトでしたが、[`rehype-autolink-headings`](https://github.com/rehypejs/rehype-autolink-headings)のように他にも`remark`や`rehype`で変換する際に行える便利なプラグインがあるので`mdx-components.tsx`だけじゃ性能に満足できないときは利用してみてはいかがでしょうか。