跳到主要內容

Markdown 和 MDX

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

您編寫...

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

輸出

<p>I <strong>love</strong> using <a href="https://nextjs.dev.org.tw/">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 = {
  // 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)

這允許 .md.mdx 檔案在您的應用程式中充當頁面、路由或 import 語句。

新增 mdx-components.tsx 檔案

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

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

要知道的好事:

渲染 MDX

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

使用基於檔案的路由

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

在 App Router 應用程式中,包括能夠使用 metadata

/app 目錄中建立新的 MDX 頁面

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

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

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 頁面。

使用 import 語句

/app 目錄中建立新頁面,並在您想要的任何位置建立 MDX 檔案

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

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

在頁面內 import MDX 檔案以顯示內容

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

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

使用動態 import 語句

您可以 import 動態 MDX 元件,而不是使用檔案系統路由來處理 MDX 檔案。

例如,您可以有一個動態路由區段,從單獨的目錄載入 MDX 元件

Route segments for dynamic MDX components

generateStaticParams 可以用於預先渲染提供的路由。通過將 dynamicParams 標記為 false,訪問未在 generateStaticParams 中定義的路由將會 404。

app/blog/[slug]/page.tsx
export default async function Page({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const slug = (await params).slug
  const { default: Post } = await import(`@/content/${slug}.mdx`)
 
  return <Post />
}
 
export function generateStaticParams() {
  return [{ slug: 'welcome' }, { slug: 'about' }]
}
 
export const dynamicParams = false

要知道的好事:確保在您的 import 語句中指定 .mdx 檔案副檔名。雖然不需要使用 模組路徑別名 (例如,@/content),但它確實簡化了您的 import 路徑。

使用自訂樣式和元件

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.
 
export function useMDXComponents(components: MDXComponents): MDXComponents {
  return {
    // 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)}
      />
    ),
    ...components,
  }
}

本地樣式和元件

您可以通過將本地樣式和元件傳遞到 import 的 MDX 元件中,將其應用於特定頁面。這些樣式和元件將會與全域樣式和元件合併並覆蓋它們。

app/mdx-page/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 頁面共用版面配置,您可以將內建版面配置支援與 App Router 搭配使用。

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

使用 Tailwind typography 外掛程式

如果您正在使用 Tailwind 來設定應用程式的樣式,則使用 @tailwindcss/typography 外掛程式 將允許您在 Markdown 檔案中重複使用 Tailwind 配置和樣式。

此外掛程式新增了一組 prose 類別,可用於將排版樣式新增至來自來源(如 Markdown)的內容區塊。

安裝 Tailwind typography 並與共用版面配置搭配使用,以新增您想要的 prose

app/mdx-page/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>
  )
}

Frontmatter

Frontmatter 是一種類似 YAML 的鍵/值配對,可用於儲存有關頁面的資料。@next/mdx 預設支援 frontmatter,儘管有很多解決方案可將 frontmatter 新增到您的 MDX 內容,例如

@next/mdx 確實允許您像使用任何其他 JavaScript 元件一樣使用 exports

現在可以在 MDX 檔案外部參考 Metadata

app/blog/page.tsx
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 之類的套件來讀取文章目錄並提取 metadata。

要知道的好事:

  • 使用 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 = {
  // 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 使用外掛程式

要搭配 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,因此尚無法將沒有可序列化選項的 remark 和 rehype 外掛程式與 Turbopack 搭配使用

遠端 MDX

如果您的 MDX 檔案或內容位於其他位置,則可以在伺服器上動態抓取它。這對於儲存在 CMS、資料庫或任何其他位置的內容非常有用。此用途的熱門社群套件是 next-mdx-remote

要知道的好事:請謹慎使用。MDX 編譯為 JavaScript 並在伺服器上執行。您應該僅從受信任的來源抓取 MDX 內容,否則可能會導致遠端程式碼執行 (RCE)。

以下範例使用 next-mdx-remote

app/mdx-page-remote/page.tsx
import { MDXRemote } from 'next-mdx-remote/rsc'
 
export default async function RemoteMdxPage() {
  // MDX text - can be from a database, CMS, fetch, anywhere...
  const res = await fetch('https://...')
  const markdown = await res.text()
  return <MDXRemote source={markdown} />
}

導航到 /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>
}

remarkrehype 生態系統包含用於 語法突顯連結標題產生目錄 等等的外掛程式。

當使用如上所示的 @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
    },
  },
})