コンテンツにスキップ

Next.js で Markdown と MDX を使用する方法

Markdown は、テキストをフォーマットするために使用される軽量マークアップ言語です。プレーンテキスト構文を使用して記述し、構造的に有効な HTML に変換できます。ウェブサイトやブログでコンテンツを記述するためによく使用されます。

あなたは書く...

I **love** using [Next.js](https://nextjs.dokyumento.jp/)

出力

<p>I <strong>love</strong> using <a href="https://nextjs.dokyumento.jp/">Next.js</a></p>

MDX は Markdown のスーパーセットであり、Markdown ファイル内で直接 JSX を記述することができます。コンテンツ内に動的なインタラクティブ性や React コンポーネントを埋め込むための強力な方法です。

Next.js は、アプリケーション内のローカル MDX コンテンツと、サーバーで動的に取得されるリモート MDX ファイルの両方をサポートできます。Next.js プラグインは、Markdown と React コンポーネントを HTML に変換する処理を行い、Server Components (App Router のデフォルト) での使用もサポートします。

知っておくと便利:完全な動作例については、Portfolio Starter Kit を参照してください。

依存関係のインストール

@next/mdx パッケージと関連パッケージは、Next.js が Markdown と MDX を処理できるように設定するために使用されます。ローカルファイルからデータを取得するため、/pages または /app ディレクトリに .md または .mdx 拡張子のページを直接作成できます。

Next.js で MDX をレンダリングするには、これらのパッケージをインストールしてください

ターミナル
npm install @next/mdx @mdx-js/loader @mdx-js/react @types/mdx

next.config.mjs の設定

MDX を使用するように設定するには、プロジェクトのルートにある next.config.mjs ファイルを更新してください。

next.config.mjs
import createMDX from '@next/mdx'
 
/** @type {import('next').NextConfig} */
const nextConfig = {
  // Configure `pageExtensions` to include markdown and MDX files
  pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'],
  // Optionally, add any other Next.js config below
}
 
const withMDX = createMDX({
  // Add markdown plugins here, as desired
})
 
// Merge MDX config with Next.js config
export default withMDX(nextConfig)

これにより、.mdx ファイルをアプリケーション内のページ、ルート、またはインポートとして使用できるようになります。

.md ファイルの処理

デフォルトでは、next/mdx.mdx 拡張子のファイルのみをコンパイルします。webpack で .md ファイルを処理するには、extension オプションを更新してください。

next.config.mjs
const withMDX = createMDX({
  extension: /\.(md|mdx)$/,
})

mdx-components.tsx ファイルの追加

プロジェクトのルートに mdx-components.tsx (または .js) ファイルを作成して、グローバル MDX コンポーネントを定義します。例えば、pages または app と同じレベル、または該当する場合は src 内に配置します。

mdx-components.tsx
import type { MDXComponents } from 'mdx/types'
 
const components: MDXComponents = {}
 
export function useMDXComponents(): MDXComponents {
  return components
}

知っておくと良いこと:

  • App Router で @next/mdx を使用するには mdx-components.tsx必須であり、これがないと動作しません。
  • mdx-components.tsx ファイル規約について詳しくはこちらをご覧ください。
  • カスタムスタイルとコンポーネントの使用方法 についてはこちらをご覧ください。

MDX のレンダリング

Next.js のファイルベースルーティングを使用して MDX をレンダリングしたり、MDX ファイルを他のページにインポートしたりできます。

ファイルベースルーティングの使用

ファイルベースルーティングを使用する場合、他のページと同様に MDX ページを使用できます。

/pages ディレクトリ内に新しい MDX ページを作成する

  my-project
  |── mdx-components.(tsx/js)
  ├── pages
  │   └── mdx-page.(mdx/md)
  └── package.json

これらのファイル内で MDX を使用したり、React コンポーネントを MDX ページ内に直接インポートしたりすることもできます。

import { MyComponent } from 'my-component'
 
# Welcome to my MDX page!
 
This is some **bold** and _italics_ text.
 
This is a list in markdown:
 
- One
- Two
- Three
 
Checkout my React component:
 
<MyComponent />

/mdx-page ルートに移動すると、レンダリングされた MDX ページが表示されるはずです。

インポートの使用

/pages ディレクトリ内に新しいページを作成し、好きな場所に MDX ファイルを作成します。

  .
  ├── markdown/
  │   └── welcome.(mdx/md)
  ├── pages/
  │   └── mdx-page.(tsx/js)
  ├── mdx-components.(tsx/js)
  └── package.json

これらのファイル内で MDX を使用したり、React コンポーネントを MDX ページ内に直接インポートしたりすることもできます。

ページ内で MDX ファイルをインポートして、コンテンツを表示します。

pages/mdx-page.tsx
import Welcome from '@/markdown/welcome.mdx'
 
export default function Page() {
  return <Welcome />
}

/mdx-page ルートに移動すると、レンダリングされた MDX ページが表示されるはずです。

カスタムスタイルとコンポーネントの使用

Markdown は、レンダリングされるとネイティブな HTML 要素にマッピングされます。例えば、次の Markdown を記述すると

## This is a heading
 
This is a list in markdown:
 
- One
- Two
- Three

次の HTML が生成されます

<h2>This is a heading</h2>
 
<p>This is a list in markdown:</p>
 
<ul>
  <li>One</li>
  <li>Two</li>
  <li>Three</li>
</ul>

Markdown をスタイル設定するには、生成された HTML 要素にマッピングされるカスタムコンポーネントを提供できます。スタイルとコンポーネントは、グローバル、ローカル、および共有レイアウトで実装できます。

グローバルスタイルとコンポーネント

mdx-components.tsx にスタイルとコンポーネントを追加すると、アプリケーション内のすべての MDX ファイルに影響します。

mdx-components.tsx
import type { MDXComponents } from 'mdx/types'
import Image, { ImageProps } from 'next/image'
 
// 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 inline styles,
// components from other libraries, and more.
 
const components = {
  // Allows customizing built-in components, e.g. to add styling.
  h1: ({ children }) => (
    <h1 style={{ color: 'red', fontSize: '48px' }}>{children}</h1>
  ),
  img: (props) => (
    <Image
      sizes="100vw"
      style={{ width: '100%', height: 'auto' }}
      {...(props as ImageProps)}
    />
  ),
} satisfies MDXComponents
 
export function useMDXComponents(): MDXComponents {
  return components
}

ローカルスタイルとコンポーネント

インポートされた MDX コンポーネントに渡すことで、特定のページにローカルスタイルとコンポーネントを適用できます。これらは、グローバルスタイルとコンポーネントとマージされ、上書きされます。

pages/mdx-page.tsx
import Welcome from '@/markdown/welcome.mdx'
 
function CustomH1({ children }) {
  return <h1 style={{ color: 'blue', fontSize: '100px' }}>{children}</h1>
}
 
const overrideComponents = {
  h1: CustomH1,
}
 
export default function Page() {
  return <Welcome components={overrideComponents} />
}

共有レイアウト

MDX ページを囲むレイアウトを共有するには、レイアウトコンポーネントを作成します。

components/mdx-layout.tsx
export default function MdxLayout({ children }: { children: React.ReactNode }) {
  // Create any shared layout or styles here
  return <div style={{ color: 'blue' }}>{children}</div>
}

次に、MDX ページにレイアウトコンポーネントをインポートし、MDX コンテンツをレイアウトでラップしてエクスポートします。

import MdxLayout from '../components/mdx-layout'
 
# Welcome to my MDX page!
 
export default function MDXPage({ children }) {
  return <MdxLayout>{children}</MdxLayout>
 
}

Tailwind typography プラグインの使用

アプリケーションのスタイル設定に Tailwind を使用している場合、@tailwindcss/typography プラグイン を使用すると、Tailwind の設定とスタイルを Markdown ファイルで再利用できます。

このプラグインは、Markdown などのソースから取得したコンテンツブロックに、タイポグラフィスタイルを追加するための prose クラスのセットを提供します。

Tailwind typography のインストール共有レイアウト と組み合わせて、必要な prose を追加できます。

MDX ページを囲むレイアウトを共有するには、レイアウトコンポーネントを作成します。

components/mdx-layout.tsx
export default function MdxLayout({ children }: { children: React.ReactNode }) {
  // Create any shared layout or styles here
  return (
    <div className="prose prose-headings:mt-8 prose-headings:font-semibold prose-headings:text-black prose-h1:text-5xl prose-h2:text-4xl prose-h3:text-3xl prose-h4:text-2xl prose-h5:text-xl prose-h6:text-lg dark:prose-headings:text-white">
      {children}
    </div>
  )
}

次に、MDX ページにレイアウトコンポーネントをインポートし、MDX コンテンツをレイアウトでラップしてエクスポートします。

import MdxLayout from '../components/mdx-layout'
 
# Welcome to my MDX page!
 
export default function MDXPage({ children }) {
  return <MdxLayout>{children}</MdxLayout>
 
}

フロントマター

フロントマターは、YAML のようなキー/値のペアで、ページのデータを格納するために使用できます。@next/mdx はデフォルトではフロントマターをサポートしていませんが、MDX コンテンツにフロントマターを追加するための多くのソリューションがあります。例えば:

@next/mdx は、エクスポートを他の JavaScript コンポーネントと同様に使用することを許可します。

メタデータは、MDX ファイルの外部から参照できるようになりました。

pages/blog.tsx
import BlogPost, { metadata } from '@/content/blog-post.mdx'
 
export default function Page() {
  console.log('metadata: ', metadata)
  //=> { author: 'John Doe' }
  return <BlogPost />
}

これは、MDX のコレクションを反復処理してデータ抽出したい場合に一般的なユースケースです。例えば、すべてのブログ投稿からブログインデックスページを作成する場合です。Node's fs moduleglobby のようなパッケージを使用して、投稿ディレクトリを読み取り、メタデータを抽出できます。

知っておくと良いこと:

  • fs, globby などはサーバーサイドでのみ使用できます。
  • 完全な動作例については、Portfolio Starter Kit テンプレートを参照してください。

remark および rehype プラグイン

オプションで、remark および rehype プラグインを提供して MDX コンテンツを変換できます。

例えば、GitHub Flavored Markdown をサポートするために remark-gfm を使用できます。

remark および rehype エコシステムは ESM のみであるため、設定ファイルとして next.config.mjs または next.config.ts を使用する必要があります。

next.config.mjs
import remarkGfm from 'remark-gfm'
import createMDX from '@next/mdx'
 
/** @type {import('next').NextConfig} */
const nextConfig = {
  // Allow .mdx extensions for files
  pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'],
  // Optionally, add any other Next.js config below
}
 
const withMDX = createMDX({
  // Add markdown plugins here, as desired
  options: {
    remarkPlugins: [remarkGfm],
    rehypePlugins: [],
  },
})
 
// Combine MDX and Next.js config
export default withMDX(nextConfig)

Turbopack でのプラグインの使用

@next/mdx の最新バージョンにアップグレードし、文字列を使用してプラグイン名を指定すると、Turbopack でプラグインを使用できます。

next.config.mjs
import createMDX from '@next/mdx'
 
/** @type {import('next').NextConfig} */
const nextConfig = {
  pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'],
}
 
const withMDX = createMDX({
  options: {
    remarkPlugins: [
      // Without options
      'remark-gfm',
      // With options
      ['remark-toc', { heading: 'The Table' }],
    ],
    rehypePlugins: [
      // Without options
      'rehype-slug',
      // With options
      ['rehype-katex', { strict: true, throwOnError: true }],
    ],
  },
})
 
export default withMDX(nextConfig)

知っておくと良いこと:

シリアライズ可能なオプションがない remark および rehype プラグインは、JavaScript 関数を Rust に渡せないため、まだ Turbopack では使用できません。

リモート MDX

MDX ファイルまたはコンテンツがどこか別の場所にある場合は、サーバーで動的に取得できます。これは、CMS、データベース、またはその他の場所で格納されているコンテンツに役立ちます。これのためのコミュニティパッケージは next-mdx-remote-client です。

知っておくと便利:注意して進めてください。MDX は JavaScript にコンパイルされ、サーバーで実行されます。MDX コンテンツは信頼できるソースからのみ取得してください。そうしないと、リモートコード実行 (RCE) につながる可能性があります。

次の例は next-mdx-remote-client を使用しています。

pages/mdx-page-remote.tsx
import {
  serialize,
  type SerializeResult,
} from 'next-mdx-remote-client/serialize'
import { MDXClient } from 'next-mdx-remote-client'
 
type Props = {
  mdxSource: SerializeResult
}
 
export default function RemoteMdxPage({ mdxSource }: Props) {
  if ('error' in mdxSource) {
    // either render error UI or throw `mdxSource.error`
  }
  return <MDXClient {...mdxSource} />
}
 
export async function getStaticProps() {
  // MDX text - can be from a database, CMS, fetch, anywhere...
  const res = await fetch('https:...')
  const mdxText = await res.text()
  const mdxSource = await serialize({ source: mdxText })
  return { props: { mdxSource } }
}

/mdx-page-remote ルートに移動すると、レンダリングされた MDX が表示されるはずです。

深掘り: Markdown はどのように HTML に変換されますか?

React は Markdown をネイティブに理解しません。Markdown のプレーンテキストは、まず HTML に変換される必要があります。これは remarkrehype を使用して達成できます。

remark は Markdown を中心としたツールのエコシステムです。rehype は HTML についても同様です。例えば、次のコードスニペットは Markdown を HTML に変換します。

import { unified } from 'unified'
import remarkParse from 'remark-parse'
import remarkRehype from 'remark-rehype'
import rehypeSanitize from 'rehype-sanitize'
import rehypeStringify from 'rehype-stringify'
 
main()
 
async function main() {
  const file = await unified()
    .use(remarkParse) // Convert into markdown AST
    .use(remarkRehype) // Transform to HTML AST
    .use(rehypeSanitize) // Sanitize HTML input
    .use(rehypeStringify) // Convert AST into serialized HTML
    .process('Hello, Next.js!')
 
  console.log(String(file)) // <p>Hello, Next.js!</p>
}

remark および rehype エコシステムには、シンタックスハイライト見出しのリンク目次の生成 などのプラグインが含まれています。

上記のように @next/mdx を使用する場合、remarkrehype を直接使用する必要はありません。これらは内部で処理されます。ここでは、@next/mdx パッケージが内部で何を行っているかをより深く理解するために説明しています。

Rust ベースの MDX コンパイラの使用 (実験的)

Next.js は Rust で書かれた新しい MDX コンパイラをサポートしています。このコンパイラはまだ実験的であり、本番環境での使用は推奨されていません。新しいコンパイラを使用するには、withMDX に渡す際に next.config.js を設定する必要があります。

next.config.js
module.exports = withMDX({
  experimental: {
    mdxRs: true,
  },
})

mdxRs は、MDX ファイルの変換方法を設定するためのオブジェクトも受け付けます。

next.config.js
module.exports = withMDX({
  experimental: {
    mdxRs: {
      jsxRuntime?: string            // Custom jsx runtime
      jsxImportSource?: string       // Custom jsx import source,
      mdxType?: 'gfm' | 'commonmark' // Configure what kind of mdx syntax will be used to parse & transform
    },
  },
})