Skip to content

Latest commit

 

History

History
630 lines (465 loc) · 29.8 KB

README-ja.md

File metadata and controls

630 lines (465 loc) · 29.8 KB

next-mdx-remote


インストール

npm install next-mdx-remote

Turbopackと一緒に使用する場合、この問題が解決されるまで、next.config.jsに以下を追加する必要があります:

const nextConfig = {
+  transpilePackages: ['next-mdx-remote'],
}

import { serialize } from 'next-mdx-remote/serialize'
import { MDXRemote } from 'next-mdx-remote'

import Test from '../components/test'

const components = { Test }

export default function TestPage({ source }) {
  return (
    <div className="wrapper">
      <MDXRemote {...source} components={components} />
    </div>
  )
}

export async function getStaticProps() {
  // MDXテキスト - ローカルファイル、データベース、どこからでも取得可能
  const source = 'Some **mdx** text, with a component <Test />'
  const mdxSource = await serialize(source)
  return { props: { source: mdxSource } }
}

これら2つが同じファイルにあるのは奇妙に見えるかもしれませんが、これはNext.jsの素晴らしい点の1つです。getStaticPropsTestPageは同じファイルに現れていますが、2つの異なる場所で実行されます。最終的に、ブラウザバンドルにはgetStaticPropsやサーバーでのみ使用される関数は一切含まれないので、serializeはブラウザバンドルから完全に削除されます。

重要: next-mdx-remoteのコードを別の「ユーティリティ」ファイルに入れることには非常に注意してください。そうすると、Next.jsのコード分割機能に問題が発生する可能性が高いです。サーバーサイドでのみ使用されるものとクライアントバンドルに残すべきものを明確に区別できる必要があります。next-mdx-remoteのコードを外部ユーティリティファイルに入れて何かが壊れた場合は、それを削除し、上記の簡単な例から始めてから問題を報告してください。

追加の例

フロントマターの解析

マークダウンは一般的にフロントマターと一緒に使用され、通常これはマークダウンの処理方法にカスタム処理を追加することを意味します。これに対処するため、next-mdx-remoteにはフロントマターのオプション解析機能が付属しており、serializeparseFrontmatter: trueを渡すことで有効にできます。

以下がその例です:

import { serialize } from 'next-mdx-remote/serialize'
import { MDXRemote } from 'next-mdx-remote'

import Test from '../components/test'

const components = { Test }

export default function TestPage({ mdxSource }) {
  return (
    <div className="wrapper">
      <h1>{mdxSource.frontmatter.title}</h1>
      <MDXRemote {...mdxSource} components={components} />
    </div>
  )
}

export async function getStaticProps() {
  // MDXテキスト - ローカルファイル、データベース、どこからでも取得可能
  const source = `---
title: Test
---

Some **mdx** text, with a component <Test name={frontmatter.title}/>
  `

  const mdxSource = await serialize(source, { parseFrontmatter: true })
  return { props: { mdxSource } }
}

フロントマターの解析にはvfile-matterが使用されます。

`scope`を使用してコンポーネントにカスタムデータを渡す

<MDXRemote />scopeプロップを受け取り、これによりMDX内で使用可能なすべての値が利用できるようになります。

scope引数の各キー/値ペアはJavaScript変数として公開されます。例えば、{ foo: 'bar' }のようなスコープがあった場合、const foo = 'bar'として解釈されます。

これは特に、scope引数のキー名が有効なJavaScript変数名であることを確認する必要があることを意味します。例えば、{ 'my-variable-name': 'bar' }を渡すと_エラー_が発生します。キー名が有効なJavaScript変数名ではないためです。

また、scope変数は_コンポーネントの引数_として消費される必要があり、テキストの途中でレンダリングすることはできないことに注意することが重要です。これは以下の例で示されています。

import { serialize } from 'next-mdx-remote/serialize'
import { MDXRemote } from 'next-mdx-remote'

import Test from '../components/test'

const components = { Test }
const data = { product: 'next' }

export default function TestPage({ source }) {
  return (
    <div className="wrapper">
      <MDXRemote {...source} components={components} scope={data} />
    </div>
  )
}

export async function getStaticProps() {
  // MDXテキスト - ローカルファイル、データベース、どこからでも取得可能
  const source =
    'Some **mdx** text, with a component using a scope variable <Test product={product} />'
  const mdxSource = await serialize(source)
  return { props: { source: mdxSource } }
}
代わりに`serialize`関数に`scope`を渡す

serializeにカスタムデータを渡すこともでき、その値を通過させて結果から利用可能にします。sourceの結果を<MDXRemote />に展開することで、データが利用可能になります。

serializeに渡される任意のスコープ値はシリアライズ可能である必要があり、関数やコンポーネントを渡すことはできないことに注意してください。さらに、scope引数で名前付けられた任意のキーは有効なJavaScript変数名である必要があります。シリアライズできないカスタムスコープを渡す必要がある場合は、レンダリングされる場所で<MDXRemote />に直接scopeを渡すことができます。この方法の例はこのセクションの上にあります。

import { serialize } from 'next-mdx-remote/serialize'
import { MDXRemote } from 'next-mdx-remote'

import Test from '../components/test'

const components = { Test }
const data = { product: 'next' }

export default function TestPage({ source }) {
  return (
    <div className="wrapper">
      <MDXRemote {...source} components={components} />
    </div>
  )
}

export async function getStaticProps() {
  // MDXテキスト - ローカルファイル、データベース、どこからでも取得可能
  const source =
    'Some **mdx** text, with a component <Test product={product} />'
  const mdxSource = await serialize(source, { scope: data })
  return { props: { source: mdxSource } }
}
MDXProviderからのカスタムコンポーネント

アプリケーションでレンダリングされる任意の<MDXRemote />にコンポーネントを利用可能にしたい場合は、@mdx-js/react<MDXProvider />を使用できます。

// pages/_app.jsx
import { MDXProvider } from '@mdx-js/react'

import Test from '../components/test'

const components = { Test }

export default function MyApp({ Component, pageProps }) {
  return (
    <MDXProvider components={components}>
      <Component {...pageProps} />
    </MDXProvider>
  )
}
// pages/test.jsx
import { serialize } from 'next-mdx-remote/serialize'
import { MDXRemote } from 'next-mdx-remote'

export default function TestPage({ source }) {
  return (
    <div className="wrapper">
      <MDXRemote {...source} />
    </div>
  )
}

export async function getStaticProps() {
  // MDXテキスト - ローカルファイル、データベース、どこからでも取得可能
  const source = 'Some **mdx** text, with a component <Test />'
  const mdxSource = await serialize(source)
  return { props: { source: mdxSource } }
}
ドットを含むコンポーネント名(例:motion.div

framer-motionのようなドット(.)を含むコンポーネント名は、他のカスタムコンポーネントと同じ方法でレンダリングできます。コンポーネントオブジェクトにmotionを渡すだけです。

import { motion } from 'framer-motion'

import { MDXProvider } from '@mdx-js/react'
import { serialize } from 'next-mdx-remote/serialize'
import { MDXRemote } from 'next-mdx-remote'

export default function TestPage({ source }) {
  return (
    <div className="wrapper">
      <MDXRemote {...source} components={{ motion }} />
    </div>
  )
}

export async function getStaticProps() {
  // MDXテキスト - ローカルファイル、データベース、どこからでも取得可能
  const source = `Some **mdx** text, with a component:

<motion.div animate={{ x: 100 }} />`
  const mdxSource = await serialize(source)
  return { props: { source: mdxSource } }
}
遅延ハイドレーション

遅延ハイドレーションは、クライアント側でのコンポーネントのハイドレーションを遅延させます。これはアプリケーションの初期ロードを改善するための最適化テクニックですが、MDXコンテンツ内の動的コンテンツの対話性に予期せぬ遅延をもたらす可能性があります。

注意:これはレンダリングされたMDXの周りに追加のラッパーdivを追加します。これはレンダリング中のハイドレーションの不一致を避けるために必要です。

import { serialize } from 'next-mdx-remote/serialize'
import { MDXRemote } from 'next-mdx-remote'

import Test from '../components/test'

const components = { Test }

export default function TestPage({ source }) {
  return (
    <div className="wrapper">
      <MDXRemote {...source} components={components} lazy />
    </div>
  )
}

export async function getStaticProps() {
  // MDXテキスト - ローカルファイル、データベース、どこからでも取得可能
  const source = 'Some **mdx** text, with a component <Test />'
  const mdxSource = await serialize(source)
  return { props: { source: mdxSource } }
}

API

このライブラリは、serialize関数と<MDXRemote />コンポーネントを公開しています。これらの2つは意図的に独自のファイルに分離されています。serializeサーバーサイド で実行されることを意図しており、サーバー/ビルド時に実行されるgetStaticProps内で使用されます。一方、<MDXRemote />はクライアントサイド、つまりブラウザで実行されることを意図しています。

  • serialize(source: string, { mdxOptions?: object, scope?: object, parseFrontmatter?: boolean })

    serialize はMDXの文字列を消費します。オプションで、MDXに直接渡されるオプションとMDXスコープに含めることができるスコープオブジェクトを渡すこともできます。この関数は、<MDXRemote />に直接渡すことを意図したオブジェクトを返します。

    serialize(
      // 文字列としての生のMDXコンテンツ
      '# hello, world',
      // オプションのパラメータ
      {
        // カスタムMDXコンポーネントの引数で利用可能
        scope: {},
        // MDXの利用可能なオプション、詳細はMDXのドキュメントを参照してください。
        // https://mdxjs.com/packages/mdx/#compilefile-options
        mdxOptions: {
          remarkPlugins: [],
          rehypePlugins: [],
          format: 'mdx',
        },
        // MDXソースからフロントマターを解析するかどうかを示します
        parseFrontmatter: false,
      }
    )

    利用可能なmdxOptionsについてはhttps://mdxjs.com/packages/mdx/#compilefile-optionsを参照してください。

  • <MDXRemote compiledSource={string} components?={object} scope?={object} lazy?={boolean} />

    <MDXRemote />serializeの出力とオプションのcomponents引数を消費します。その結果はコンポーネントに直接レンダリングできます。コンテンツのハイドレーションを遅延させ、静的マークアップを即座に提供するには、lazyプロップを渡します。

    <MDXRemote {...source} components={components} />

デフォルトコンポーネントの置き換え

レンダリングは内部でMDXProviderを使用します。これは、HTMLタグをカスタムコンポーネントで置き換えられることを意味します。これらのコンポーネントはMDXJSのコンポーネントテーブルにリストされています。

使用例としては、好みのスタイリングライブラリでコンテンツをレンダリングすることです。

import { Typography } from "@material-ui/core";

const components = { Test, h2: (props) => <Typography variant="h2" {...props} /> }
...

お好みであれば、コンポーネントを<MDXRemote />に直接渡す代わりに、アプリケーション全体を<MDXProvider />でラップすることもできます。上記のを参照してください。

注意:コンポーネント名に "/"が含まれるため、th/tdは機能しません。

背景と理論

Next.jsアプリでMDXファイルをロードするための良いデフォルトの方法は実際にありません。以前、MDXファイルをレイアウトにレンダリングし、そのフロントマターをインポートしてインデックスページを作成できるようにするためにnext-mdx-enhancedを作成しました。

next-mdx-enhancedからのこのワークフローは問題ありませんでしたが、next-mdx-remoteで解決したいくつかの制限がありました:

  • ファイルコンテンツはローカルでなければなりません。 MDXファイルを別のレポジトリやデータベースなどに保存することはできません。十分に大規模な運用では、コンテンツを作成する人々とコンテンツのプレゼンテーションに取り組む人々の間に分割が生じることになります。同じレポジトリでこれら2つの懸念事項を重複させると、全員にとってより困難なワークフローになります。
  • ファイルシステムベースのルーティングに縛られます。 ページはその場所に応じてURLで生成されます。または、exportPathMapを使用してリマップすることもできますが、これは作成者に混乱を招きます。いずれにしても、ページを移動すると何かが壊れます。ページのURLまたはexportPathMapの設定のいずれかです。
  • パフォーマンスの問題に直面することになります。 WebpackはJavaScriptバンドラーであり、数百/数千ページのテキストコンテンツをロードすることを強制すると、メモリ要件が爆発的に増加します。Webpackは各ページを大量のメタデータを持つ個別のオブジェクトとして保存します。数百ページを持つ我々の実装の1つでは、サイトをコンパイルするのに8GB以上のメモリが必要でした。ビルドには25分以上かかりました。
  • 関係データを構造化する方法が制限されます。 フロントマターがJavaScriptオブジェクトにパースされてメモリに保持されるという全データ構造では、コンテンツを動的で関連するカテゴリに整理するのが困難です。

そこで、next-mdx-remoteはパターン全体を変更し、MDXコンテンツをインポートを通じてではなく、getStaticPropsgetServerPropsを通じてロードします。つまり、他のデータをロードするのと同じ方法です。このライブラリは、パフォーマンスの高い方法でMDXコンテンツをシリアライズおよびハイドレートするためのツールを提供します。これにより、上記のすべての制限が解消され、しかも大幅に低コストで実現されます。next-mdx-enhancedは多くのカスタムロジックといくつかの煩わしい制限を持つ非常に重いライブラリです。非公式のテストでは、ビルド時間が50%以上短縮されることが示されています。

このプロジェクトが最初に作成されて以来、Kent C. Doddsが類似のプロジェクトmdx-bundlerを作成しました。このライブラリは、MDXファイル内のインポートとエクスポートをサポートし(各インポートされたファイルの内容を手動で読み取って渡す限り)、フロントマターを自動的に処理します。すべてのファイルが異なるコンポーネントをインポートして使用する場合、mdx-bundlerを使用することで便利に使える場合があります。現在、next-mdx-remoteはコンポーネントをインポートしてすべてのページで利用可能にすることしかできないためです。ただし、この機能にはコストがかかることに注意することが重要です。基本的なマークダウンコンテンツの場合、mdx-bundlerの出力はnext-mdx-remoteの出力よりも少なくとも400%大きくなります。

これでブログを構築するにはどうすればいいですか?

データによると、すべての開発者ツールの使用例の99%は、不必要に複雑な個人ブログを構築することです。冗談です。しかし、真剣に、個人や小規模ビジネス用のブログを構築しようとしている場合は、通常のHTMLとCSSを使用することを検討してください。シンプルなブログを作成するために重いフルスタックJavaScriptフレームワークを使用する必要は絶対にありません。数年後に更新を行うために戻ってきたとき、すべての依存関係に10回の破壊的なリリースがなかったことに感謝するでしょう。

しかし、本当に主張するなら、公式のNext.js実装例をチェックしてください。💖

注意事項

環境ターゲット

next-mdx-remoteによって生成されるコードは、実際にMDXをレンダリングするために使用され、モジュールサポートを持つブラウザをターゲットにしています。古いブラウザをサポートする必要がある場合は、serializeからのcompiledSource出力をトランスパイルすることを検討してください。

import / export

importおよびexport文は、MDXファイルの内部で使用することはできません。MDXファイルでコンポーネントを使用する必要がある場合は、<MDXRemote />にプロップとして提供する必要があります。

これは理解できるはずです。なぜなら、機能するためにはインポートがファイルパスに相対的である必要があり、このライブラリは設定されたファイルパスからのローカルコンテンツのみをロードするのではなく、どこからでもコンテンツをロードできるようにするためです。エクスポートに関しては、MDXコンテンツはモジュールではなくデータとして扱われるため、next-mdx-remoteに渡されたMDXからエクスポートされる可能性のある値にアクセスする方法はありません。

セキュリティ

このライブラリは、クライアント側でJavaScriptの文字列を評価し、それによってMDXコンテンツをリモート処理します。文字列をJavaScriptに評価することは、注意して行わないと危険を伴う可能性があります。XSS攻撃を可能にする可能性があるためです。ドキュメントで指示されているように、serialize関数によって生成されたmdxSource入力のみを<MDXRemote />に渡していることを確認することが重要です。ユーザー入力を<MDXRemote />に直接渡さないでください。

ウェブサイトにevalnew Function()を介したコード評価を許可しないCSPがある場合、next-mdx-remoteを利用するためにその制限を緩和する必要があります。これはunsafe-evalを使用して行うことができます。

TypeScript

このプロジェクトにはTypeScript用のネイティブタイプが含まれています。serialize<MDXRemote />の両方が通常予想される通りのタイプを持ち、ライブラリはgetStaticPropsの結果をタイプ付けするために使用できるタイプもエクスポートします。

  • MDXRemoteSerializeResult<TScope = Record<string, unknown>>serializeの戻り値を表します。TScopeジェネリックタイプを渡して、渡すスコープデータのタイプを表すことができます。

以下は、TypeScriptでの単純な実装の例です。TypeScriptのすべての設定でタイプを正確にこの方法で実装する必要はないかもしれません。この例は、必要に応じてタイプをどこに適用できるかを示すデモンストレーションに過ぎません。

import type { GetStaticProps } from 'next'
import { serialize } from 'next-mdx-remote/serialize'
import { MDXRemote, type MDXRemoteSerializeResult } from 'next-mdx-remote'
import ExampleComponent from './example'

const components = { ExampleComponent }

interface Props {
  mdxSource: MDXRemoteSerializeResult
}

export default function ExamplePage({ mdxSource }: Props) {
  return (
    <div>
      <MDXRemote {...mdxSource} components={components} />
    </div>
  )
}

export const getStaticProps: GetStaticProps<{
  mdxSource: MDXRemoteSerializeResult
}> = async () => {
  const mdxSource = await serialize('some *mdx* content: <ExampleComponent />')
  return { props: { mdxSource } }
}

React Server Components (RSC) & Next.js app ディレクトリのサポート

サーバーコンポーネント、特にNext.jsのappディレクトリ内でのnext-mdx-remoteの使用は、next-mdx-remote/rscからのインポートによってサポートされています。以前は、シリアライズとレンダリングのステップが分離されていましたが、今後はRSCがこの分離を不要にします。

注目すべきいくつかの違い:

  • <MDXRemote />は現在、next-mdx-remote/serializeからのシリアライズされた出力を受け入れる代わりに、sourceプロップを受け入れます
  • カスタムコンポーネントは、RSCがReact Contextをサポートしていないため、@mdx-js/reactMDXProviderコンテキストを使用して提供することはできなくなりました
  • parseFrontmatter: trueを渡す際にMDX外部でフロントマターにアクセスするには、next-mdx-remote/rscから公開されているcompileMdxメソッドを使用します
  • lazyプロップはサポートされなくなりました。レンダリングがサーバーで行われるためです
  • <MDXRemote />は現在非同期コンポーネントであるため、サーバーでレンダリングする必要があります。クライアントコンポーネントはMDXマークアップの一部としてレンダリングできます

RSCの詳細については、Next.jsのドキュメントをチェックしてください。

appディレクトリを使用するNext.js 13+アプリケーションでの使用を想定しています。

基本

import { MDXRemote } from 'next-mdx-remote/rsc'

// app/page.js
export default function Home() {
  return (
    <MDXRemote
      source={`# Hello World

      This is from Server Components!
      `}
    />
  )
}

ローディング状態

import { MDXRemote } from 'next-mdx-remote/rsc'

// app/page.js
export default function Home() {
  return (
    // 理想的には、このローディングスピナーはレイアウトシフトがないことを保証します。
    // これはそのようなローディングスピナーを提供する方法の例です。
    // Next.jsでは、これに`loading.js`を使用することもできます。
    <Suspense fallback={<>Loading...</>}>
      <MDXRemote
        source={`# Hello World

        This is from Server Components!
        `}
      />
    </Suspense>
  )
}

カスタムコンポーネント

// components/mdx-remote.js
import { MDXRemote } from 'next-mdx-remote/rsc'

const components = {
  h1: (props) => (
    <h1 {...props} className="large-text">
      {props.children}
    </h1>
  ),
}

export function CustomMDX(props) {
  return (
    <MDXRemote
      {...props}
      components={{ ...components, ...(props.components || {}) }}
    />
  )
}
// app/page.js
import { CustomMDX } from '../components/mdx-remote'

export default function Home() {
  return (
    <CustomMDX
      // h1は`large-text`クラス名でレンダリングされるようになりました
      source={`# Hello World
      This is from Server Components!
    `}
    />
  )
}

MDX外部でフロントマターにアクセスする

// app/page.js
import { compileMDX } from 'next-mdx-remote/rsc'

export default async function Home() {
  // オプションでフロントマターオブジェクトのタイプを提供します
  const { content, frontmatter } = await compileMDX<{ title: string }>({
    source: `---
title: RSC Frontmatter Example
---
# Hello World
This is from Server Components!
`,
    options: { parseFrontmatter: true },
  })
  return (
    <>
      <h1>{frontmatter.title}</h1>
      {content}
    </>
  )
}

代替案

next-mdx-remoteはサポートする機能について意見を持っています。next-mdx-remoteが提供しない追加機能が必要な場合は、以下のいくつかの代替案を検討してください:

next-mdx-remoteが必要ない可能性があります

React Server Componentsを使用していて、カスタムコンポーネントを持つ基本的なMDXを使用しようとしているだけなら、コアMDXライブラリ以外に何も必要ありません。

import { compile, run } from '@mdx-js/mdx'
import * as runtime from 'react/jsx-runtime'
import ClientComponent from './components/client'

// MDXはファイルやデータベースなど、どこからでも取得できます。
const mdxSource = `# Hello, world!
<ClientComponent />
`

export default async function Page() {
  // MDXソースコードを関数本体にコンパイルします
  const code = String(
    await compile(mdxSource, { outputFormat: 'function-body' })
  )
  // その後、サーバーでコードを実行してサーバーコンポーネントを生成するか、
  // 最終的なレンダリングのために文字列をクライアントコンポーネントに渡すことができます。

  // ランタイムでコンパイルされたコードを実行し、デフォルトエクスポートを取得します
  const { default: MDXContent } = await run(code, {
    ...runtime,
    baseUrl: import.meta.url,
  })

  // MDXコンテンツをレンダリングし、ClientComponentをコンポーネントとして提供します
  return <MDXContent components={{ ClientComponent }} />
}

コンパイルされた文字列をデータベースやクライアントコンポーネントに渡す予定がない場合は、evaluateを使用してこのアプローチを簡略化することもできます。evaluateは1回の呼び出しでコードをコンパイルして実行します。

import { evaluate } from '@mdx-js/mdx'
import * as runtime from 'react/jsx-runtime'
import ClientComponent from './components/client'

// MDXはファイルやデータベースなど、どこからでも取得できます。
const mdxSource = `
export const title = "MDX Export Demo";

# Hello, world!
<ClientComponent />

export function MDXDefinedComponent() {
  return <p>MDX-defined component</p>;
}
`

export default async function Page() {
  // コンパイルされたコードを実行します
  const {
    default: MDXContent,
    MDXDefinedComponent,
    ...rest
  } = await evaluate(mdxSource, runtime)

  console.log(rest) // { title: 'MDX Export Demo' } をログ出力

  // MDXコンテンツをレンダリングし、ClientComponentをコンポーネントとして提供し、
  // エクスポートされたMDXDefinedComponentをレンダリングします。
  return (
    <>
      <MDXContent components={{ ClientComponent }} />
      <MDXDefinedComponent />
    </>
  )
}

ライセンス

Mozilla Public License Version 2.0