介紹/指南/MDX

如何在 Next.js 中使用 markdown 和 MDX

Markdown 是一種輕量級標記語言,用於格式化文字。它允許您使用純文字語法編寫並將其轉換為結構有效的 HTML。它常用於在網站和部落格上撰寫內容。

您可以這樣寫...

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

輸出:

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

MDX 是 Markdown 的超集,允許您直接在 Markdown 檔案中編寫 JSX。這是一種強大的方式,可以在內容中新增動態互動性和嵌入 React 元件。

Next.js 可以支援應用程式內的本地 MDX 內容,以及從伺服器動態獲取的遠端 MDX 檔案。Next.js 外掛處理將 Markdown 和 React 元件轉換為 HTML,包括支援在伺服器元件(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

更新專案根目錄下的 next.config.mjs 檔案以配置使用 MDX:

next.config.mjs
import createMDX from '@next/mdx'

/** @type {import('next').NextConfig} */
const nextConfig = {
  // 配置 `pageExtensions` 以包含 Markdown 和 MDX 檔案
  pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'],
  // 可選地,在此新增其他 Next.js 配置
}

const withMDX = createMDX({
  // 在此新增所需的 Markdown 外掛
})

// 將 MDX 配置與 Next.js 配置合併
export default withMDX(nextConfig)

這允許 .mdx 檔案在您的應用程式中作為頁面、路由或導入使用。

處理 .md 檔案

預設情況下,next/mdx 僅編譯 .mdx 副檔名的檔案。要使用 webpack 處理 .md 檔案,請更新 extension 選項:

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

須知Turbopack 目前不支援 extension 選項,因此不支援 .md 檔案。

新增 mdx-components.tsx 檔案

在專案根目錄建立 mdx-components.tsx(或 .js)檔案以定義全域 MDX 元件。例如,與 pagesapp 同級,或在 src 目錄中(如果適用)。

import type { MDXComponents } from 'mdx/types'

export function useMDXComponents(components: MDXComponents): MDXComponents {
  return {
    ...components,
  }
}

須知

渲染 MDX

您可以使用 Next.js 的檔案路由或將 MDX 檔案導入其他頁面來渲染 MDX。

使用檔案路由

使用檔案路由時,您可以像使用任何其他頁面一樣使用 MDX 頁面。

/pages 目錄中建立一個新的 MDX 頁面:

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

您可以在這些檔案中使用 MDX,甚至可以直接在 MDX 頁面中導入 React 元件:

import { MyComponent } from 'my-component'

# 歡迎來到我的 MDX 頁面!

這是一些 **粗體**_斜體_ 文字。

這是 Markdown 中的列表:

-
-
-

看看我的 React 元件:

<MyComponent />

導航到 /mdx-page 路由應顯示您渲染的 MDX 頁面。

使用導入

/pages 目錄中建立一個新頁面,並在任何您想要的地方建立一個 MDX 檔案:

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

您可以在這些檔案中使用 MDX,甚至可以直接在 MDX 頁面中導入 React 元件:

import { MyComponent } from 'my-component'

# 歡迎來到我的 MDX 頁面!

這是一些 **粗體**_斜體_ 文字。

這是 Markdown 中的列表:

-
-
-

看看我的 React 元件:

<MyComponent />

在頁面中導入 MDX 檔案以顯示內容:

import Welcome from '@/markdown/welcome.mdx'

export default function Page() {
  return <Welcome />
}

導航到 /mdx-page 路由應顯示您渲染的 MDX 頁面。

使用自訂樣式和元件

Markdown 在渲染時會映射到原生 HTML 元素。例如,編寫以下 Markdown:

## 這是一個標題

這是 Markdown 中的列表:

-
-
-

會生成以下 HTML:

<h2>這是一個標題</h2>

<p>這是 Markdown 中的列表:</p>

<ul>
  <li>一</li>
  <li>二</li>
  <li>三</li>
</ul>

要為您的 Markdown 添加樣式,您可以提供映射到生成的 HTML 元素的自訂元件。樣式和元件可以全域、局部實現,並與共享佈局一起使用。

全域樣式和元件

mdx-components.tsx 中新增樣式和元件將影響應用程式中的 所有 MDX 檔案。

import type { MDXComponents } from 'mdx/types'
import Image, { ImageProps } from 'next/image'

// 此檔案允許您提供自訂 React 元件
// 以在 MDX 檔案中使用。您可以導入和使用任何
// React 元件,包括內聯樣式、
// 來自其他函式庫的元件等。

export function useMDXComponents(components: MDXComponents): MDXComponents {
  return {
    // 允許自訂內建元件,例如添加樣式。
    h1: ({ children }) => (
      <h1 style={{ color: 'red', fontSize: '48px' }}>{children}</h1>
    ),
    img: (props) => (
      <Image
        sizes="100vw"
        style={{ width: '100%', height: 'auto' }}
        {...(props as ImageProps)}
      />
    ),
    ...components,
  }
}

局部樣式和元件

您可以通過將它們傳遞到導入的 MDX 元件中來為特定頁面應用局部樣式和元件。這些將與 全域樣式和元件 合併並覆蓋它們。

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 頁面周圍共享佈局,請建立一個佈局元件:

export default function MdxLayout({ children }: { children: React.ReactNode }) {
  // 在此建立任何共享佈局或樣式
  return <div style={{ color: 'blue' }}>{children}</div>
}

然後,將佈局元件導入 MDX 頁面,將 MDX 內容包裹在佈局中,並導出它:

import MdxLayout from '../components/mdx-layout'

# 歡迎來到我的 MDX 頁面!

export default function MDXPage({ children }) {
  return <MdxLayout>{children}</MdxLayout>

}

使用 Tailwind 排版插件

如果您使用 Tailwind 來設計應用程式樣式,使用 @tailwindcss/typography 插件 可以讓您在 markdown 檔案中重複使用 Tailwind 配置和樣式。

該插件添加了一組 prose 類別,可用於為來自 markdown 等來源的內容區塊添加排版樣式。

安裝 Tailwind 排版插件 並與 共享佈局 一起使用,以添加您想要的 prose 樣式。

要在 MDX 頁面之間共享佈局,請創建一個佈局元件:

export default function MdxLayout({ children }: { children: React.ReactNode }) {
  // 在此創建任何共享佈局或樣式
  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'

# 歡迎來到我的 MDX 頁面!

export default function MDXPage({ children }) {
  return <MdxLayout>{children}</MdxLayout>

}

前言資料 (Frontmatter)

前言資料 (Frontmatter) 是一種類似 YAML 的鍵值對,可用於儲存頁面的相關資料。@next/mdx 預設 不支援 前言資料,但有許多解決方案可以為您的 MDX 內容添加前言資料,例如:

@next/mdx 允許 您像其他 JavaScript 元件一樣使用導出:

export const metadata = {
  author: 'John Doe',
}

# 部落格文章

現在可以在 MDX 檔案外部引用元資料:

import BlogPost, { metadata } from '@/content/blog-post.mdx'

export default function Page() {
  console.log('metadata: ', metadata)
  //=> { author: 'John Doe' }
  return <BlogPost />
}

一個常見的使用情境是當您想要遍歷 MDX 集合並提取資料時。例如,從所有部落格文章中創建一個部落格索引頁面。您可以使用 Node 的 fs 模組globby 等套件來讀取文章目錄並提取元資料。

須知

  • 使用 fsglobby 等僅能在伺服器端使用。
  • 查看 Portfolio Starter Kit 模板以獲取完整的工作範例。

remark 和 rehype 插件

您可以選擇性地提供 remark 和 rehype 插件來轉換 MDX 內容。

例如,您可以使用 remark-gfm 來支援 GitHub Flavored Markdown。

由於 remark 和 rehype 生態系統僅支援 ESM,您需要使用 next.config.mjsnext.config.ts 作為配置檔案。

next.config.mjs
import remarkGfm from 'remark-gfm'
import createMDX from '@next/mdx'

/** @type {import('next').NextConfig} */
const nextConfig = {
  // 允許 .mdx 副檔名的檔案
  pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'],
  // 可選地,在此添加其他 Next.js 配置
}

const withMDX = createMDX({
  // 在此添加所需的 markdown 插件
  options: {
    remarkPlugins: [remarkGfm],
    rehypePlugins: [],
  },
})

// 合併 MDX 和 Next.js 配置
export default withMDX(nextConfig)

在 Turbopack 中使用插件

要在 Turbopack 中使用插件,請升級到最新版本的 @next/mdx 並使用字串指定插件名稱:

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: [],
    rehypePlugins: [['rehype-katex', { strict: true, throwOnError: true }]],
  },
})

export default withMDX(nextConfig)

須知

由於 無法將 JavaScript 函數傳遞給 Rust,目前無法在 Turbopack 中使用沒有可序列化選項的 remark 和 rehype 插件。

遠端 MDX

如果您的 MDX 檔案或內容位於 其他地方,您可以在伺服器上動態獲取它。這對於儲存在 CMS、資料庫或其他地方的內容非常有用。一個適用於此用途的社群套件是 next-mdx-remote-client

須知:請謹慎操作。MDX 會編譯為 JavaScript 並在伺服器上執行。您應該只從受信任的來源獲取 MDX 內容,否則可能導致遠端代碼執行 (RCE)。

以下範例使用 next-mdx-remote-client

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) {
    // 可以渲染錯誤 UI 或拋出 `mdxSource.error`
  }
  return <MDXClient {...mdxSource} />
}

export async function getStaticProps() {
  // MDX 文字 - 可以來自資料庫、CMS、fetch 等任何地方...
  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) // 轉換為 markdown AST
    .use(remarkRehype) // 轉換為 HTML AST
    .use(rehypeSanitize) // 清理 HTML 輸入
    .use(rehypeStringify) // 將 AST 轉換為序列化的 HTML
    .process('Hello, Next.js!')

  console.log(String(file)) // <p>Hello, Next.js!</p>
}

remarkrehype 生態系統包含用於 語法高亮連結標題生成目錄 等的插件。

當如上所述使用 @next/mdx 時,您 不需要 直接使用 remarkrehype,因為它已經為您處理好了。我們在此描述它是為了更深入地了解 @next/mdx 套件底層的工作方式。

使用基於 Rust 的 MDX 編譯器 (實驗性)

Next.js 支援一個用 Rust 編寫的新 MDX 編譯器。此編譯器仍處於實驗階段,不建議用於生產環境。要使用新編譯器,您需要在將 next.config.js 傳遞給 withMDX 時進行配置:

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

mdxRs 也接受一個物件來配置如何轉換 mdx 檔案。

next.config.js
module.exports = withMDX({
  experimental: {
    mdxRs: {
      jsxRuntime?: string            // 自訂 jsx 運行時
      jsxImportSource?: string       // 自訂 jsx 導入來源,
      mdxType?: 'gfm' | 'commonmark' // 配置將使用哪種 mdx 語法來解析和轉換
    },
  },
})

有用的連結