はじめに
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
だけじゃ性能に満足できないときは利用してみてはいかがでしょうか。