# Fumadocs (Framework Mode, Next.js): AI と LLM
URL: /ja/docs/next/integrations/llms
Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/[framework]/integrations/llms.ja.mdx

Fumadocs に AI 機能を組み込みます。

## LLM向けドキュメント [#docs-for-llm]

LLMやAIエージェント向けに、ドキュメントをMarkdown形式で提供します。

```package-install
npx @fumadocs/cli feature llms
```

これにより、docs コレクションで処理済みの Markdown が有効になり、`lib/source.ts` に `docsLlms` が追加されて、以下の[ファイル](#files)が配信されます。手動でセットアップする場合に備えて、生成されるファイルを以下に記載しています。

### `docsLlms` [#docsllms]

`llms()` は、ドキュメントを LLM 向けにレンダリングします。`renderPage` は、生のファイル内容ではなく処理済みのドキュメントをもとに、1 つのページを Markdown に変換します。

```ts title="lib/source.ts"
import { llms } from 'fumadocs-core/source';

export const docsLlms = llms(source, {
  renderPage: async (page) => `# ${page.data.title} (${page.url})

${await page.data.getText('processed')}`,
});

```

| メソッド           | 出力                                |
| -------------- | --------------------------------- |
| `index(lang?)` | ページツリーから構築される `llms.txt` インデックス   |
| `page(page)`   | `renderPage` でレンダリングした 1 つのページ    |
| `full(lang?)`  | `renderPage` でレンダリングした全ページを結合したもの |
| `files()`      | [エージェント向けファイル](#files)のパス         |
| `serve(path)`  | 指定したパスにあるファイルのレスポンス               |

<Callout title="renderPage は必須です">
コンテンツソースが Markdown をどのように公開しているかを Fumadocs 側では判断できないため、`page()`、`full()`、`files()`、`serve()` は
`renderPage` を渡した場合にのみ利用できます。
</Callout>

`renderPage` はページの処理済み Markdown を読み取るため、`includeProcessedMarkdown` を有効にしてください。

```ts title="source.config.ts"
import { defineDocs } from 'fumadocs-mdx/config';

export const docs = defineDocs({
  dir: 'content/docs',
  docs: { // [!code ++:5]
    postprocess: {
      includeProcessedMarkdown: true,
    },
  },
});

```

MDX コンポーネントは JSX 構文のまま出力されます。これらを [Markdown 形式](/docs/headless/utils/markdown#asmarkdown)にレンダリングするには、[MDX コンポーネント](/docs/headless/mdx/remark-llms#jsx-elements)をページの props として渡してください。

[`@fumadocs/local-md`](/docs/next/integrations/content/local-md) のようなランタイムのコンテンツソースは、リクエスト時に解決されます。その `getSource` を渡すと、CLI が同じファイルを生成します。

```ts
export const docsLlms = llms(getSource, {
  renderPage: (page) => `# ${page.data.title} (${page.url})

${page.data.content}`,
});
```

`getSource` を使用する場合、`index()` と `indexNode()` は文字列ではなく Promise を返します。

### ファイル [#files]

CLIは、AIエージェント向けに次のファイルを配信します。

* `/llms.txt`: ドキュメントのインデックスです。各ページのMarkdownへのリンクが含まれます。
* `/llms-full.txt`: 全ページのコンテンツです。
* `<page>.md`: 各ページのMarkdownです (例: `/docs/ui.md`) 。`/` のページは `/index.md` になります。
* `<section>/llms-full.txt`: [セクション](#sections)のコンテンツです (例: `/docs/ui/llms-full.txt`) 。

下書きなどのページをこれらのファイルから除外するには、`filter` を指定します。除外したページも `llms.txt` には引き続き掲載され、HTMLページへのリンクになります。

```ts title="lib/source.ts"
export const docsLlms = llms(source, {
  filter: (page) => page.slugs[0] !== 'drafts',
  renderPage: async (page) => `# ${page.data.title}\n\n${await page.data.getText('processed')}`,
});
```

これらのファイルでは、`renderUrl` はリンク先のファイルのパス (ページの `.md` ファイルなど) を受け取ります。

ページの HTML を読み取るエージェントは、`<head>` 内のリンクからそのページの Markdown を見つけます：

```html
<link rel="alternate" type="text/markdown" href="/docs/ui.md" />
```

### セクション [#sections]

セクションとは、独自の `llms-full.txt` を持つページツリー内のフォルダーです。これにより、エージェントは大規模なドキュメントを部分ごとに読み込めます。デフォルトでは、すべてのルートフォルダーがセクションになります。`llms.txt` では、各フォルダーの行がそのセクションの `llms-full.txt` へのリンクになります。

セクションとするフォルダーを指定するには、`section` を渡します。この関数はフォルダーを受け取り、そのフォルダーをセクションとするかどうかを返します。

```ts title="lib/source.ts"
export const docsLlms = llms(source, {
  // index ページを持つすべてのフォルダー
  section: (folder) => folder.index !== undefined,
  renderPage: async (page) => `# ${page.data.title}\n\n${await page.data.getText('processed')}`,
});
```

大規模なサイトでは、セクションを折りたたむと `llms.txt` を短く保てます。折りたたんだセクションは、`llms.txt` ではそのセクション専用の `<section>/llms.txt` への1つのリンクとして記載され、セクション内のページはそのファイルに一覧表示されます。

```ts title="lib/source.ts"
export const docsLlms = llms(source, {
  section: (folder) => (folder.root ? { collapse: true } : false),
  renderPage: async (page) => `# ${page.data.title}\n\n${await page.data.getText('processed')}`,
});
```

セクションのファイルは、そのフォルダーのインデックスページの URL 配下で配信されます。インデックスページがないフォルダーでは、代わりに配下のページの URL ディレクトリが使われます。フォルダーグループのように 2 つのフォルダーが同じ URL を共有する場合は、`asPath` を渡すと、一方を別のパスで配信できます (例：`{ asPath: '/docs/guides' }`) 。

### ルート [#routes]

`files()` はファイルのパスを列挙し、`serve(path)` は指定したパスのファイルをレスポンスとして返します。そのため、各ルートはリクエストパスを `serve()` に渡すだけで済みます。

`/llms.txt` と `/llms-full.txt` はルートハンドラーが配信します。その他のファイルは `app/docs/[[...slug]]` と同じ階層に置かれるため、Next.js の設定でそれらのパスを `/llms.mdx/[...path]` ルートへリライトします：

```ts tab="app/llms.txt/route.ts"
import { docsLlms } from '@/lib/source';

export const revalidate = false;

export async function GET() {
  return (await docsLlms.serve('/llms.txt'))!;
}

```

```ts tab="app/llms-full.txt/route.ts"
import { docsLlms } from '@/lib/source';

export const revalidate = false;

export async function GET() {
  return (await docsLlms.serve('/llms-full.txt'))!;
}

```

```ts tab="app/llms.mdx/[...path]/route.ts"
import { docsLlms } from '@/lib/source';

export const revalidate = false;
// other paths are 404s, never cached responses
export const dynamicParams = false;

export async function GET(_req: Request, { params }: RouteContext<'/llms.mdx/[...path]'>) {
  const { path } = await params;
  return (await docsLlms.serve(`/${path.join('/')}`)) ?? new Response(null, { status: 404 });
}

export async function generateStaticParams() {
  const params: { path: string[] }[] = [];
  for (const file of await docsLlms.files()) {
    // `/llms.txt` and `/llms-full.txt` have their own routes
    if (!/^\/llms(-full)?\.txt$/.test(file)) params.push({ path: decodeURI(file).slice(1).split('/') });
  }
  return params;
}

```

```ts tab="next.config.ts"
import type { NextConfig } from 'next';

const config: NextConfig = {
  reactStrictMode: true,
  async rewrites() { // [!code ++:12]
    return [
      {
        source: '/docs/:path*.md',
        destination: '/llms.mdx/docs/:path*.md',
      },
      {
        source: '/docs/:path*.txt',
        destination: '/llms.mdx/docs/:path*.txt',
      },
    ];
  },
};

export default config;

```

`serve()` はファイルが存在しないパスに対して `undefined` を返すため、ミドルウェアはリクエストを後続の処理に渡せます。`hideLocale: 'always'` を指定した場合は URL にロケールが含まれないため、言語を `serve()` の第2引数、および `files()` の第1引数として渡してください。

#### 静的出力 [#static-output]

`output: 'export'` を使用する場合、Next.js では rewrites を利用できません。`/llms.mdx/[...path]` ルートは `/llms.mdx/docs/ui.md` のようなプレフィックス付きのパスでファイルを配信し、`renderUrl` によって `llms.txt` 内のリンクがそのパスを指すようにします。

```ts title="lib/source.ts"
import { llms } from 'fumadocs-core/source';

export const docsLlms = llms(source, {
  renderPage: async (page) => `# ${page.data.title} (${page.url})

${await page.data.getText('processed')}`,
  // link to the files under `/llms.mdx`
  renderUrl: (item) => (/^\/.*\.(md|txt)$/.test(item.url) ? `/llms.mdx${item.url}` : item.url),
});

```

#### `Accept` [#accept]

AIエージェントは、`Accept` ヘッダーを使って Markdown を要求できます。

リクエストが Markdown を優先する場合は、ページの Markdown を返すようにします。

```ts title="proxy.ts"
import { NextRequest, NextResponse } from 'next/server';
import { isMarkdownPreferred, rewritePath } from 'fumadocs-core/negotiation';

const { rewrite: rewriteDocs } = rewritePath('/docs{/*path}', '/llms.mdx/docs{/*path}.md');

export default function proxy(request: NextRequest) {
  const { pathname } = request.nextUrl;
  // `/docs/ui.md` のようなファイルは Next.js の設定の rewrites で配信されます
  const result =
    isMarkdownPreferred(request) && !/\.(md|txt)$/.test(pathname) && rewriteDocs(pathname);

  if (result) {
    return NextResponse.rewrite(new URL(result, request.nextUrl), {
      headers: { Vary: 'Accept' }, // [!code highlight]
    });
  }

  return NextResponse.next();
}
```

同じ URL に2つの表現が存在することになるため、キャッシュの衝突を防ぐには、レスポンスに `Vary: Accept` を含める必要があります。

<Callout>
Next.js は App Router のページレスポンスから `Vary` を削除するため、ドキュメントページの HTML レスポンスにはこのヘッダーを
付与できません。共有キャッシュからドキュメントを配信している場合は、CDN 側で設定してください。
</Callout>

### ページアクション [#page-actions]

ページアクションを使うと、読者はページの Markdown をコピーしたり、AI アプリでページを開いたりできます。ページアクションは、ページの [`.md` ファイル](#files)を読み込みます。

![AI ページアクション](/docs/ai-page-actions.png)

docs ページ内でレンダリングしてください。

```tsx
import { MarkdownCopyButton, ViewOptionsPopover } from 'fumadocs-ui/layouts/docs/page';

const markdownUrl = `${page.url}.md`;

<div className="flex flex-row gap-2 items-center border-b pt-2 pb-6">
  <MarkdownCopyButton markdownUrl={markdownUrl} />
  <ViewOptionsPopover
    markdownUrl={markdownUrl}
    githubUrl={`https://github.com/${owner}/${repo}/blob/main/content/docs/${encodeURI(page.path)}`}
  />
</div>;
```

静的エクスポートを使用する場合は、URL が `` `/llms.mdx${page.url}.md` `` になります。

## MCP サーバー [#mcp-server]

[MCP](https://modelcontextprotocol.io) サーバーを通じてドキュメントをAIエージェントに公開できます。ページの一覧表示・検索・読み取りを行うツールも提供されます。

```package-install
npx @fumadocs/cli feature mcp
```

上記の [LLM ルート](#docs-for-llm) に加えて、Streamable HTTP トランスポートを使用する `/api/mcp` ルートを作成します。

ツールは [`fumadocs-core/mcp`](/docs/headless/utils/mcp) によって提供されます。

```ts title="app/api/mcp/route.ts"
import { createMcpHandler, McpServer } from '@modelcontextprotocol/server';
import { registerSearchTool, registerSourceTools } from 'fumadocs-core/mcp';
import { createFromSource } from 'fumadocs-core/search/server';
import { docsLlms, source } from '@/lib/source';

const handler = createMcpHandler(() => {
  const mcp = new McpServer(
    {
      name: 'docs',
      version: '1.0.0',
    },
    // the tools never change, a stream to watch them would hold a serverless function open
    { capabilities: { tools: { listChanged: false } } },
  );

  registerSourceTools(mcp, source, docsLlms);
  registerSearchTool(mcp, createFromSource(source));

  return mcp;
});

export async function GET(request: Request) {
  return handler.fetch(request);
}

export async function POST(request: Request) {
  return handler.fetch(request);
}

export async function DELETE(request: Request) {
  return handler.fetch(request);
}

```

エージェントを接続するには、次のようにします。

```json
{
  "mcpServers": {
    "docs": { "url": "https://your-site.com/api/mcp" }
  }
}
```

## WebMCP [#webmcp]

<Callout type="warn" title="実験的機能">
[WebMCP](https://webmachinelearning.github.io/webmcp/) は策定初期段階の Web 標準で、Chrome 149 以降で
`#enable-webmcp-testing` フラグを有効にするか、オリジントライアルを通じて利用できます。API は今後
変更される可能性があります。
</Callout>

前述の MCP サーバーがサイトに接続してくるエージェント向けであるのに対し、WebMCP は読者が閲覧しているページ上で、ブラウザに組み込まれた AIエージェント
にツールを公開します。

```package-install
npx @fumadocs/cli feature webmcp
```

[LLM ルート](#docs-for-llm)をベースにした `WebMCP` コンポーネントが作成されます。これを `<RootProvider />` の内側でレンダリングしてください。

* `search_docs` は検索 API ルートにクエリを送信します。`fetchClient` は[他のクライアント](/docs/next/search)に置き換えることもできます。
* `read_page` は、`/docs/page.md` からページの Markdown を取得します。

## AIに質問 [#ask-ai]

![AI Search](/docs/ai-search.png)

Fumadocs CLI を使用して、AI チャットダイアログをインストールできます。

<Tabs items={["AI SDK","LLMGateway","Inkeep AI"]}>
<Tab>
```npm
npx @fumadocs/cli add ai/openrouter
```

Vercel AI SDK を使用して [OpenRouter](https://openrouter.ai) 向けに自動で設定され、AI 用の `/search` ツールも含まれます。

`/api/chat` ルートを更新すれば、他のモデルも使用できます。

> Fumadocs は AI モデルを提供していません。モデルはご自身で用意してください。
>
> AI モデルには、上記で生成した `llms-full.txt` ファイルを利用できます。また、サードパーティのソリューションと組み合わせれば、より多様な情報ソースを活用することもできます。
</Tab>

<Tab>
```npm
npx @fumadocs/cli add ai/llmgateway
```

Vercel AI SDK を使用して [LLMGateway](https://llmgateway.io) 向けに自動で設定され、AI 用の `/search` ツールも含まれます。

LLMGateway は、300 以上の言語モデルへのリクエストをプロキシするオープンソースの API ゲートウェイです。そのため、モデル ID を変更するだけで、同じルートハンドラーのままプロバイダーを切り替えられます。

LLMGateway の API キーを環境変数に追加します。

```dotenv
LLM_GATEWAY_API_KEY="..."
```

デフォルトでは、ルートは `anthropic/claude-3.5-sonnet` を使用します。LLMGateway がサポートする他のモデルに切り替えるには、`LLM_GATEWAY_MODEL` 環境変数を設定してください。コードを変更する必要はありません。

```dotenv
# https://llmgateway.io/models に掲載されている任意のモデル ID
LLM_GATEWAY_MODEL="openai/gpt-4o"
```
</Tab>

<Tab>
```npm
npx @fumadocs/cli add ai/inkeep
```

Vercel AI SDK を使用して [Inkeep AI](https://inkeep.com) 向けに自動で設定されます。

Inkeep の API キーを環境変数に追加します。

```dotenv
INKEEP_API_KEY="..."
```
</Tab>
</Tabs>

Docs レイアウトは、`aiChat` オプションをもとにチャットをレンダリングします。このオプションはクライアントの状態を受け取るため、レイアウト用のクライアントコンポーネントを作成し、元のレイアウトの代わりに使用してください。

```tsx tab="components/ai/layout.tsx"
'use client';
// Notebook、Glass、Spacious レイアウトも使用できます
import { DocsLayout as Layout, type DocsLayoutProps } from 'fumadocs-ui/layouts/docs';
// インストールしたコンポーネント
import { AIChat, AIChatPanel, useAIChat } from './search';

export function DocsLayout(props: DocsLayoutProps) {
  return (
    <AIChat>
      <ChatLayout {...props} />
    </AIChat>
  );
}

function ChatLayout(props: DocsLayoutProps) {
  const { open, setOpen } = useAIChat();

  return (
    <Layout
      {...props}
      aiChat={{ open, onOpenChange: setOpen, panel: <AIChatPanel />, trigger: true }}
    />
  );
}
```

```tsx tab="app/docs/layout.tsx"
import { DocsLayout } from 'fumadocs-ui/layouts/docs'; // [!code --]
import { DocsLayout } from '@/components/ai/layout'; // [!code ++]
```

レイアウトのオプションはクライアントコンポーネントに渡されるため、シリアライズ可能な値にする必要があります。