@next/mdxをedgeで使う
はじめに
-
@next/mdx
を利用することで、 Next.js で MDX からページを作ることができます。 -
@next/mdx
は、edgeで動作させることができます。 - 本記事では、edge 環境で動作するように
@next/mdx
を設定していきます。
以下が作業したソースになります。
@next/mdx
@next/mdx
はローカルに保存されている MDX ファイルからブラウザで閲覧可能なページに変換できます。
@next/mdx
の制限として、ローカルファイルが対象のためリモートにあるファイルを対象とする場合は、next-mdx-remote
やcontentlayer
などの別の仕組みを利用する必要があります。コンテンツに型付けしてくれる型安全性を担保している contentlayer
が個人的にはおすすめです。
ポイント
エッジで動作させるポイントは至ってシンプルです。MDX ファイルのレイアウトを担当する layout.tsx
を作成し、export const runtime = "edge"
を設定するだけです。
// ここの部分!!!
export const runtime = "edge"
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
...
<article className="prose prose-xl">{children}</article>
</>
);
}
プロジェクトを新規作成
プロジェクトを新規に作成します。
$ pnpm create next-app@latest next-nextmdx-sample --typescript --eslint --import-alias "@/*" --src-dir --use-pnpm --tailwind --app
$ cd next-nextmdx-sample
不要な設定を削除します。
@tailwind base;
@tailwind components;
@tailwind utilities;
export default function Home() {
return (
<main className="">
テストページ
</main>
)
}
import "./globals.css";
export const metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ja">
<body className="">{children}</body>
</html>
);
}
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
],
plugins: [],
};
@next/mdx
を設定
@next/mdx
パッケージをインストールします。
$ pnpm add @next/mdx @mdx-js/loader @mdx-js/react @types/mdx
Next.js で MDX ファイルをサポートするために、mdx-components.tsx
を作成します。
$ touch src/mdx-components.tsx
import type { MDXComponents } from 'mdx/types'
// This file allows you to provide custom React components
// to be used in MDX files. You can import and use any
// React component you want, including components from
// other libraries.
// This file is required to use MDX in `app` directory.
export function useMDXComponents(components: MDXComponents): MDXComponents {
return {
// Allows customizing built-in components, e.g. to add styling.
// h1: ({ children }) => <h1 style={{ fontSize: "100px" }}>{children}</h1>,
...components,
}
}
MDX の表現を拡張する必要なパケージ、remarkとrephypeをインストールします。
$ pnpm add remark-gfm remark-math rehype-katex @mapbox/rehype-prism rehype-slug remark-toc remark-breaks
MDX を利用できるように next.config.js を修正します。今回は、remark-gfmを利用するため、ECMAScript Modules(ESM)に対応している next.config.mjs
に変換します。
$ mv next.config.js next.config.mjs
import nextMDX from "@next/mdx";
import remarkGfm from "remark-gfm";
import remarkMath from "remark-math";
import rehypeKatex from "rehype-katex";
import rehypePrism from "@mapbox/rehype-prism";
import rehypeSlug from "rehype-slug";
import remarkToc from "remark-toc";
import remarkBreaks from "remark-breaks";
const withMDX = nextMDX({
extensions: /\.mdx?$/,
options: {
// If you use remark-gfm, you'll need to use next.config.mjs
// as the package is ESM only
// https://github.com/remarkjs/remark-gfm#install
remarkPlugins: [
remarkGfm,
remarkMath,
[remarkToc, { maxDepth: 3, heading: "目次" }],
remarkBreaks,
],
rehypePlugins: [rehypeKatex, rehypePrism, rehypeSlug],
// If you use `MDXProvider`, uncomment the following line.
// providerImportSource: "@mdx-js/react",
},
});
/** @type {import('next').NextConfig} */
const nextConfig = {
// Configure pageExtensions to include md and mdx
pageExtensions: ["ts", "tsx", "js", "jsx", "md", "mdx"],
// Optionally, add any other Next.js config below
reactStrictMode: true,
};
// Merge MDX config with Next.js config
export default withMDX(nextConfig);
コンテンツを作成
コンテンツを作成します。
それぞれのページごとに異なる要素を持たせています。
$ mkdir -p src/app/contents/article01
$ mkdir -p src/app/contents/article02
$ mkdir -p src/app/contents/article03
$ mkdir -p src/app/contents/article04
$ mkdir -p src/app/contents/article05
$ mkdir -p src/app/contents/article06
$ mkdir -p src/app/contents/article07
$ mkdir -p src/app/contents/article08
$ mkdir -p src/app/contents/article09
$ touch src/app/contents/article01/page.mdx
$ touch src/app/contents/article02/page.mdx
$ touch src/app/contents/article03/page.mdx
$ touch src/app/contents/article04/page.mdx
$ touch src/app/contents/article05/page.mdx
$ touch src/app/contents/article06/page.mdx
$ touch src/app/contents/article07/page.mdx
$ touch src/app/contents/article08/page.mdx
$ touch src/app/contents/article09/page.mdx
マークダウン
# ミニマムなマークダウン
マークダウンのリスト
- リスト 1
- リスト 2
- リスト 3
JSX
# JSX
JSXを使えるよ! TailwindCSSもね!
<div className="font-bold">太文字だよ</div>
<div className="bg-blue-100">背景青だよ</div>
Reactコンポーネント
import Button from '@/components/Button'
# Reactのカスタムコンポーネント
Reactのカスタムコンポーネントも使えるよ!
<Button>Hello</Button>
$ mkdir -p touch src/components
$ touch src/components/Button.tsx
import { FC } from "react";
interface ButtonProps {
children: React.ReactNode;
}
const Button: FC<ButtonProps> = (props) => {
return (
<button className="bg-blue-400 w-[90px] py-1 px-1 rounded-md flex items-center justify-center">
{props.children}
</button>
);
};
export default Button;
GFM
# GFM
### Autolink literals
www.example.com, https://example.com, and contact@example.com.
### Footnote
A note[^1]
[^1]: Big note.
### Strikethrough
~one~ or ~~two~~ tildes.
### Table
| a | b | c | d |
| --- | :-- | --: | :-: |
### Tasklist
- [ ] to do
- [x] done
TeX
# TeX
$\TeX$ も使えるよ
$$
\frac{\pi}{2} =
\left( \int_{0}^{\infty} \frac{\sin x}{\sqrt{x}} dx \right)^2 =
\sum_{k=0}^{\infty} \frac{(2k)!}{2^{2k}(k!)^2} \frac{1}{2k+1} =
\prod_{k=1}^{\infty} \frac{4k^2}{4k^2 - 1}
$$
シンタックスハイライト
# シンタックスハイライト
コードブロックにシンタックスハイライトで色をつけれます!
実際はバッククォートですが、文章に記載することができないため全角のバッククォートで記載しています。
コピペする場合は、半角のバッククォートに変換してください。
```css
p { color: red; }
```
目次
# 目次テスト
## 目次
## こんにちは
lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
## おはよう
lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut
改行
# 改行
改行テスト1
改行テスト2
改行テスト3
改行テスト4
改行テスト5
画像
# 画像の追加
![](/2023-07-04_10_45_13.jpg)
画像は public/sakura.jpg
に配置しています。
$ tree public
public
├── next.svg
├── sakura.jpg
└── vercel.svg
画像は適当に選んでください。必要だったら以下使ってください。
スタイリングを適用
シンタックスハイライト と TeX のスタイルを適用
MDX 用に layout.tsx
を作成 link
タグを読み込ませて、シンタックスハイライトと TeX のスタイルを適用させます。
$ touch src/app/contents/layout.tsx
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/katex@0.15.3/dist/katex.min.css"
integrity="sha384-KiWOvVjnN8qwAZbuQyWDIbfCLFhLXNETzBQjA/92pIowpC0d2O3nppDGQVgwd2nB"
crossOrigin="anonymous"
/>
<link
href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.9.0/themes/prism-tomorrow.min.css"
rel="stylesheet"
/>
<article>{children}</article>
</>
);
}
HTMLタグの装飾
TailwindCSS でスタイルは無効化されているので、HTML タグにあわせたスタイルを追加する必要があります。TailwindCSS にはマークダウンを簡単にスタイリングできる Typography の @tailwindcss/typography があります。こちらを利用しマークダウンをスタイリングします。
$ pnpm add -D @tailwindcss/typography
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/katex@0.15.3/dist/katex.min.css"
integrity="sha384-KiWOvVjnN8qwAZbuQyWDIbfCLFhLXNETzBQjA/92pIowpC0d2O3nppDGQVgwd2nB"
crossOrigin="anonymous"
/>
<link
href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.9.0/themes/prism-tomorrow.min.css"
rel="stylesheet"
/>
- <article>{children}</article>
+ <article className="prose prose-xl">{children}</article>
</>
);
}
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
],
- plugins: [],
+ plugins: [require("@tailwindcss/typography")],
};
Runtime をエッジに変更
layout.tsx
の runtime
をエッジに変更します。
+export const runtime = "edge";
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/katex@0.15.3/dist/katex.min.css"
integrity="sha384-KiWOvVjnN8qwAZbuQyWDIbfCLFhLXNETzBQjA/92pIowpC0d2O3nppDGQVgwd2nB"
crossOrigin="anonymous"
/>
<link
href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.9.0/themes/prism-tomorrow.min.css"
rel="stylesheet"
/>
<article className="prose prose-xl">{children}</article>
</>
);
}
ローカルで動作確認
ビルドします。
$ pnpm build
> next-nextmdx-sample@0.1.0 build /Users/hayato94087/Private/next-nextmdx-sample
> next build
- info Creating an optimized production build
- info Compiled successfully
- info Linting and checking validity of types
- info Collecting page data
- info Generating static pages (4/4)
- info Finalizing page optimization
Route (app) Size First Load JS
┌ ○ / 166 B 77.7 kB
├ ℇ /contents/article01 166 B 77.7 kB
├ ℇ /contents/article02 166 B 77.7 kB
├ ℇ /contents/article03 166 B 77.7 kB
├ ℇ /contents/article04 166 B 77.7 kB
├ ℇ /contents/article05 166 B 77.7 kB
├ ℇ /contents/article06 166 B 77.7 kB
├ ℇ /contents/article07 166 B 77.7 kB
├ ℇ /contents/article08 166 B 77.7 kB
├ ℇ /contents/article09 166 B 77.7 kB
└ ○ /favicon.ico 0 B 0 B
+ First Load JS shared by all 77.5 kB
├ chunks/14478101-08a82aad1ad550e2.js 50.5 kB
├ chunks/215-dfbbe15efda3708a.js 25.2 kB
├ chunks/main-app-e0442f49f83198ac.js 215 B
└ chunks/webpack-ee13fe2d6bcb1ddb.js 1.64 kB
Route (pages) Size First Load JS
─ ○ /404 182 B 75.5 kB
+ First Load JS shared by all 75.3 kB
├ chunks/framework-5cb727a20916d7ea.js 45 kB
├ chunks/main-3dda75eaceefd0ef.js 28.4 kB
├ chunks/pages/_app-a187a5672205bf4d.js 195 B
└ chunks/webpack-ee13fe2d6bcb1ddb.js 1.64 kB
ℇ (Streaming) server-side renders with streaming (uses React 18 SSR streaming or Server Components)
○ (Static) automatically rendered as static HTML (uses no initial props)
以下のログから分かる通り、Edge としてビルドされています。
Route (app) Size First Load JS
┌ ○ / 166 B 77.7 kB
├ ℇ /contents/article01 166 B 77.7 kB
├ ℇ /contents/article02 166 B 77.7 kB
├ ℇ /contents/article03 166 B 77.7 kB
├ ℇ /contents/article04 166 B 77.7 kB
├ ℇ /contents/article05 166 B 77.7 kB
├ ℇ /contents/article06 166 B 77.7 kB
├ ℇ /contents/article07 166 B 77.7 kB
├ ℇ /contents/article08 166 B 77.7 kB
├ ℇ /contents/article09 166 B 77.7 kB
ℇ (Streaming) server-side renders with streaming (uses React 18 SSR streaming or Server Components)
○ (Static) automatically rendered as static HTML (uses no initial props)
サーバを起動します。
$ pnpm start
> next-nextmdx-sample@0.1.0 start /Users/hayato94087/Private/next-nextmdx-sample
> next start
- ready started server on 0.0.0.0:3000, url: http://localhost:3000
以下の URL でアクセスします。
Vercelで動作確認
プロジェクトをデプロイします。
動作確認を行います。
まとめ
最後に、@next/mdx
はローカルに保存されている MDX ファイルからブラウザで閲覧可能なページに変換できます。が、@next/mdx
の制限として、ローカルファイルが対象のためリモートにあるファイルを対象とする場合は、next-mdx-remote
やcontentlayer
などの別の仕組みを利用する必要があります。コンテンツに型付けしてくれる型安全性を担保している contentlayer
が個人的にはおすすめです。
以下が作業したソースになります。
Discussion