🐷

@next/mdxをedgeで使う

2023/07/04に公開

はじめに

  • @next/mdxを利用することで、 Next.js で MDX からページを作ることができます。
  • @next/mdx は、edgeで動作させることができます。
  • 本記事では、edge 環境で動作するように @next/mdx を設定していきます。

以下が作業したソースになります。

https://github.com/hayato94087/next-nextmdx-sample

@next/mdx

@next/mdxはローカルに保存されている MDX ファイルからブラウザで閲覧可能なページに変換できます。


@next/mdx の制限として、ローカルファイルが対象のためリモートにあるファイルを対象とする場合は、next-mdx-remotecontentlayerなどの別の仕組みを利用する必要があります。コンテンツに型付けしてくれる型安全性を担保している contentlayer が個人的にはおすすめです。

ポイント

エッジで動作させるポイントは至ってシンプルです。MDX ファイルのレイアウトを担当する layout.tsx を作成し、export const runtime = "edge" を設定するだけです。

src/app/contents/layout.tsx(省略版)
// ここの部分!!!
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

不要な設定を削除します。

src/app/globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;
src/app/page.tsx
export default function Home() {
  return (
    <main className="">
      テストページ
    </main>
  )
}
src/app/layout.tsx
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>
  );
}
tailwind.config.js
/** @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
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 の表現を拡張する必要なパケージ、remarkrephypeをインストールします。

$ 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
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

マークダウン

src/app/contents/article01/page.mdx
# ミニマムなマークダウン

マークダウンのリスト

- リスト 1
- リスト 2
- リスト 3

JSX

src/app/contents/article02/page.mdx
# JSX

JSXを使えるよ! TailwindCSSもね!

<div className="font-bold">太文字だよ</div>
<div className="bg-blue-100">背景青だよ</div>

Reactコンポーネント

src/app/contents/article03/page.mdx
import Button from '@/components/Button'

# Reactのカスタムコンポーネント

Reactのカスタムコンポーネントも使えるよ!

<Button>Hello</Button>
$ mkdir -p touch src/components
$ touch src/components/Button.tsx
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

src/app/contents/article04/page.mdx
# 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

src/app/contents/article05/page.mdx
# 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}
$$

シンタックスハイライト

src/app/contents/article06/page.mdx
# シンタックスハイライト

コードブロックにシンタックスハイライトで色をつけれます!

実際はバッククォートですが、文章に記載することができないため全角のバッククォートで記載しています。
コピペする場合は、半角のバッククォートに変換してください。

```css
p { color: red; }
```

目次

src/app/contents/article07/page.mdx
# 目次テスト

## 目次

## こんにちは

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

改行

src/app/contents/article08/page.mdx
# 改行

改行テスト1
改行テスト2

改行テスト3


改行テスト4



改行テスト5

画像

src/app/contents/article09/page.mdx
# 画像の追加

![](/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
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
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>
+    <article className="prose prose-xl">{children}</article>
   </>
 );
}
tailwind.config.cjs
/** @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.tsxruntime をエッジに変更します。

src/app/contents/layout.tsx
+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を利用することで、 Next.js で MDX からページを作ることができました。
  • @next/mdx は、edgeにデプロイできました。


最後に、@next/mdxはローカルに保存されている MDX ファイルからブラウザで閲覧可能なページに変換できます。が、@next/mdx の制限として、ローカルファイルが対象のためリモートにあるファイルを対象とする場合は、next-mdx-remotecontentlayerなどの別の仕組みを利用する必要があります。コンテンツに型付けしてくれる型安全性を担保している contentlayer が個人的にはおすすめです。

以下が作業したソースになります。

https://github.com/hayato94087/next-nextmdx-sample

Discussion