Next.js 13にしました
ようやくこのサイトをNext.js 13にできました。そのメモです。
## webpackローダーのESM対応
いきなりNext.jsではないのですが、webpack@5.80 でESMのLoaderが指定できるようになりました。これがいちばんありがたかったポイントです。
例えば *.ts
をTypeScriptでロードするために以下のような設定をしたとしましょう。MyTsLoader は{"type":"module"}
なパッケージとします。
1const webpackConfig = {2 module: { rules: [{ test: /\.ts$/, loader: 'MyTsLoader' }] },3};
webpack@5.79 と webpack@5.80 それぞれでの動作は以下で確認できます:
- webpack@5.79: CodeSandbox:repl-webpack-5-79-mjs-loader
- webpack@5.80: CodeSandbox:repl-webpack-5-80-mjs-loader
webpack@5.79 では以下のエラーになります:
1Error [ERR_REQUIRE_ESM]: require() of ES Module /workspace/ts-loader-mjs/index.mjs not supported.2Instead change the require of /workspace/ts-loader-mjs/index.mjs to a dynamic import() which is available in all CommonJS modules.
そのため webpack@5.79 以前はCJSからESMを呼ぶなど回り道が必要でしたが、
- 2023/4/20に webpack@5.80 でそれが不要になり
- 2023/6/9に vercel/next.js#50992 で取り込まれ
- 2023/6/11に next@13.4.5 でリリースされたことで
Next.js環境でもESMのLoaderが使えるようになりました。
## MDXで書く
https://nextjs.org/docs/pages/building-your-application/configuring/mdx
ESMが楽に使えるようになったのでESMでリリースされている unified 関連のパッケージも楽に使えるようになりました。
### パッケージの追加
@next/mdx @mdx-js/loader をインストールします1。
1npm install @next/mdx @mdx-js/loader
### mdx-components.jsxの追加
何もしないのであればこれでOKです。
1export const useMDXComponents = (components) => ({ ...components });
### next.config.mjsの変更
このサイトの実際の設定は next.config.mjs を参照してください。
- remark-gfm: GFM で書けるようにします
- remark-math: 数式ブロックを認識します
- rehype-highlight: コードブロックをハイライトします
- rehype-katex: 数式ブロックを KaTeX でレンダリングします
- rehype-slug: 見出しにidを付けます
1import mdx from '@next/mdx';2import remarkGfm from 'remark-gfm';3import remarkMath from 'remark-math';4import rehypeHighlight from 'rehype-highlight';5import rehypeKatex from 'rehype-katex';6import rehypeSlug from 'rehype-slug';78const withMDX = mdx({9 options: {10 remarkPlugins: [remarkGfm, remarkMath],11 rehypePlugins: [rehypeHighlight, rehypeSlug, rehypeKatex],12 },13});1415const nextConfig = {16 // page.tsx, page.mdxがレスポンスを返します17 pageExtensions: ['tsx', 'mdx'],18};1920export default withMDX(nextConfig);
### page.mdxを作って表示してみる
次の app/mdx-sample/page.mdx を追加して /mdx-sample でページが表示されればOKです。
1# MDXのサンプル23これは**MDXのサンプル**です。
## 記事やサイトの画像を生成する
satori というツールを利用して画像を生成できるようになったので、これを使っています。
/path/to/page というページの画像を /cover/path/to/page で生成します。
- 例えばこのページの画像はこちら: /cover/2023/next13
- 画像を生成しているコードはこちら: /app/cover/[[...path]]/route.tsx
- フォントは @fontsource/noto-sans-jp のフォルダから public フォルダに事前にコピーしておいたものを fetch します。
- 文字出力の際に提供したフォントにない文字があるとエラーになります。
- adobe-blankを渡しておけばエラーにはならなくなりそうです。
<meta>
に /cover/path/to/page を追加します。MDXでmetadataを設定するには page.tsx と同様に metadata を export します。参考: configuring/mdx#frontmatter
1export const metadata = {2 title: 'Next.js 13にしました',3 openGraph: {4 images: [5 {6 url: '/cover/2023/next13',7 width: 1280,8 height: 640,9 },10 ],11 },12};
しかしこの openGraph の値はパスだけわかれば生成できるし、パスが変わったら修正が必要なのでこのような指定はしたくありません。layout.tsxで設定したいところですが現時点ではうまくできませんでした2。そのため後述のようにASTの時点で追加するようにしました。
## MDXのASTをいじる
以下の機能を追加しました。
- コード、画像、表、数式、を
<figure>
の中に入れる<figcaption>
をつける- それぞれリンクできる3
- コードブロックに行番号を追加
- 行番号にリンクできる
- Shift+クリックやCtrl+クリックで範囲指定できる
- 数式を
\begin{align}
と\end{align}
で囲む - 数式番号にリンクできる
- YouTube動画のタイトルとリンクをキャプションに追加する
export metadata = {...}
の中に openGraph を追加する- MDXの中の
export metadata = {...}
のブロックのnode.type
はmdxjsEsm
node.data.estree
にJavaScriptのASTがあるので metadata を export する ExportNamedDeclaration があればそれをいじるnode.value
にコードが文字列で入っていますがそちらを変更しても metadata は変更されません。- 今考えると
node.value
をいじって4node.data.estree
を更新する方が楽でした。
- MDXの中の
機能追加は以下のような関数を next.config.mjs
の remarkPlugins と rehypePlugins に追加すれば動きます。
1// import type { Root } from 'mdast'; // MarkdownのASTをいじる場合2import type { Root } from 'hast'; // HTMLのASTをいじる場合34export const rehypePlugin = async (tree: Root, file: { path: string }) => {5 // ここでtreeをいじる6 return tree;7};
## Footnotes
-
@mdx-js/react は
*.mdx
を import する必要があれば追加します。 ↩ -
middlewareでヘッダにパスを追加してheaders()でそれを読むなど言及されていますがうまくいきませんでした。 ↩
-
idを
position:absolute
で要素よりも上に表示した要素につけるとリンクから飛んできたときに対象がヘッダーで隠れません。 ↩ -
最初の
metadata = {
の直後にopenGraph:{...},
を入れます。 ↩