はじめに
Nextjsでは@next/mdxを用いることで簡単にマークダウンを扱えます。
ドキュメントによれば、依存ライブラリのインストールとnext.config.jsに@next/mdxの設定を注入するだけで導入できます。
npm install @next/mdx @mdx-js/loader @mdx-js/react @types/mdx
/** @type {import('next').NextConfig} */
const nextConfig = {};
const withMDX = require('@next/mdx')();
module.exports = withMDX(nextConfig);
さらに、@next/mdxではマークダウンから変換するReactコンポーネントを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コンポーネントに変換します。
const H2 = (props) {
return <h2 className="mt-10 text-3xl font-bold" {...props} />;
};
余談ですが、この例のようにtailwindcssを用いる場合はtailwind.config.jsのcontentにmdx-components.tsxを含めることを忘れないようにしてください。忘れていた場合、他の場所で使っているクラスしかスタイルが反映されないので注意してください。
// デフォルトだと./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でシンタックスハイライトを行う方法を紹介します。
この記事で作成するアプリケーションはこちらのリポジトリを参照してください。
JSXへ変換するタイミングでシンタックスハイライトを行う
まず、最初に説明したmdx-components.tsxを用いて実装してみます。
mdx-components.tsxでシンタックスハイライトを実装するにはcodeの変換先のReactコンポーネントを用意する必要があります。
Reactでシンタックスハイライトを実装するときはhighlight.jsやprismjsなどのライブラリを用いたり、それらをReact用にカスタマイズされたものを使うことが多いと思います。
今回はhighlight.jsを使って実装します。
'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に設定して
code: ({ children }) => <SyntaxHighlight>{children}</SyntaxHighlight>,
下のようなコードを埋め込んだマークダウンを表示させてみます。
export const Counter: FC = () => {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
};

一瞬だけ素の状態が見えてからそれらがハイライトされました。これは最初にHTMLの一部として素のcodeがサーバーから送られてきて、ハイドレーション時にシンタックスハイライトしていることが原因です。
そのためサーバー側であらかじめ変換を施すReactコンポーネントを作成して解決したかったのですが、Reactコンポーネントで使われる多くのシンタックスハイライトの手法ではクライアント側で行わせる処理を用いたものが多く画面のチラつきを受け入れるしかありませんでした(これに対する良いアプローチがあれば教えていただけると幸いです)。
次に紹介する方法ではこのチラつきをなくすためのアプローチとして、htmlへ変換する時にシンタックスハイライトを行います。
HTMLへ変換するタイミングでシンタックスハイライトを行う
大雑把な説明となりますが@next/mdxは、マークダウンをASTにパースする処理をremarkで、それをHTMLのASTに変換する処理をrehypeで行い、さらにそれをmdx-componentsに定義した通り(なければ@mdx-js/reactそのままの定義)に変換してマークダウンをReactコンポーネントとして扱えるようにしています。これらの変換はmdx-js/loaderに書かれているので詳細を知りたい場合はそちらを参照してください(変換処理自体はmdx-js/mdxのcore.js周りに書かれています)。
現在@next/mdxで実験的機能であるmdxRsではRustを使ったコンパイラを利用するため処理系が上記とは異なります。そのため、これから説明する方法は実験的機能を使っていない前提となります。
このような変換過程があるので@next/mdxではnext.config.jsでrehypeやremarkに関連するプラグインを導入するオプションを設定できます。そのため、rehypeのプラグインであるrehype-pretty-codeを通じてHTMLのASTに変換する時点でシンタックスハイライトを行ってみます。
これらのプラグインを設定するために必要なパッケージをインストールしてnext.config.jsを修正します。
npm i rehype-pretty-code shiki
shikiはhighlight.jsと同じようにシンタックスハイライトを行うライブラリで、rehype-pretty-codeはこれを利用して装飾します。
/** @type {import('next').NextConfig} */
const nextConfig = {};
const withMDX = require('@next/mdx')({
options: {
rehypePlugins: [
[
require('rehype-pretty-code'),
],
],
},
});
module.exports = withMDX(nextConfig);
これらの設定によって以下のように画面へ表示されます。
これでも十分ですが、少し味気ないです。rehype-pretty-codeではシンタックスハイライトのスタイリングをshikiの提供するテーマを選択できます。
github-lightにしたいときは以下のようにします。
/** @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で設定がライトモードの時のようなコードブロックになります。
この方法であればビルド時にシンタックスハイライトされたhtmlを生成できるので、先ほどのようなブラウザへ表示された時に生じるチラつきがなくなります。
おわりに
Nextjsで@next/mdxを利用してマークダウンをHTMLへ変換する時にシンタックスハイライトする方法を紹介しました。
紹介した内容を参考にすればかなり簡単にマークダウンベースのブログサイト作れます。
カスタマイズ性も高いので、是非この記事を参考にブログを作ってみてはいかがでしょうか。
今回はシンタックスハイライトでしたが、rehype-autolink-headingsのように他にもremarkやrehypeで変換する際に行える便利なプラグインがあるのでmdx-components.tsxだけじゃ性能に満足できないときは利用してみてはいかがでしょうか。

