## コンポーネントを追加する2つの方法

単一のMDXファイルでインポートするか、再利用するコンポーネントを`mdxExtras`で登録します。どちらもzfbのzudo-reactのDescriptionを使用し、表示専用のコンポーネントはクライアントJavaScriptなしでサーバーHTMLを生成します。



## ファイルごとのインポート

```mdx
import { MyBadge } from "@/components/my-badge";

<MyBadge>New</MyBadge>
```

通常のMDXインポートはサーバーで描画します。それだけでは状態やイベントハンドラーは有効になりません。

## グローバル登録: mdxExtras

型付きのコンポーネントを作成し、ホストのbindingsモジュールに登録します。

```tsx title="src/components/my-badge.tsx"
/** @jsxRuntime automatic */
/** @jsxImportSource @takazudo/zfb/zudo-react */
import type { Child } from "@takazudo/zfb/zudo-react";

export function MyBadge({ children }: { children?: Child }) {
  return (
    <span class="rounded-full bg-surface px-hsp-sm py-vsp-2xs text-caption text-fg">
      {children}
    </span>
  );
}
```

```tsx title="src/chrome-bindings.tsx"
import { defineChromeBindings } from "@takazudo/zudo-doc/chrome-bindings";
import { MyBadge } from "./components/my-badge";

export const chromeBindings = defineChromeBindings({
  mdxExtras: { MyBadge },
});
```

```ts title="zfb.config.ts"
import { defineConfig } from "@takazudo/zfb/config";
import { zudoDoc } from "@takazudo/zudo-doc/config";

export default defineConfig(
  zudoDoc({
    siteName: "My Docs",
    chromeBindingsModule: "./src/chrome-bindings.tsx",
  }),
);
```

生成ルートは`virtual:zudo-doc-chrome-bindings`を使用します。省略したスロットにはパッケージのデフォルトが残り、`mdxExtras`は同名の組み込みコンポーネントを上書きします。各エントリーは`(props: { html: string })`のような必須propsを含め、独自の型付きpropsをそのまま保持できます。propsはMDXの呼び出し側で渡されるため、アダプターは不要です。MDXでの使用時に渡さないpropsを必須にしないでください。[Host Chrome Bindings](https://zudo-doc.takazudomodular.com/ja/docs/reference/host-chrome-bindings)を参照してください。

```mdx
<MyBadge>New</MyBadge>
```



## 組み込みコンポーネントの上書き

extrasマップは組み込みMDXマップの後に適用されます。admonition（`Note`、`Tip`、`Info`、`Warning`、`Danger`、`Important`、`Caution`）、ヘルパー（`Details`、`HtmlPreview`、`CodeGroup`、`Tabs`、`TabItem`、`Island`、`Asset`、`AssetCode`）、`p`や`table`などのHTMLオーバーライドを置き換えられます。既存の記法とpropsの契約を維持します。全体の上書きが不要なら新しい名前のコンポーネントを選びます。マニフェストベースのアセットは[Asset Viewer](https://zudo-doc.takazudomodular.com/ja/docs/guides/asset-viewer)を参照してください。



## JSXと厳密なprops

共有tsconfigは`jsxImportSource: "@takazudo/zfb/zudo-react"`を使用します。`Child`、`Component`、`Description`はこの公開エントリーから、`JSX`名前空間は`@takazudo/zfb/zudo-react/jsx-runtime`からインポートします。`className`ではなく`class`を使います。ネイティブpropsにはDOMの綴りと明示的な単位が必要です。styleオブジェクトの長さは単位のない数値ではなく`"12px"`などの文字列にします。ネイティブ`<ol start={3}>`と`<pre>`の先頭改行は現在のネイティブ契約に従います。

## インタラクティブアイランド（実験的）

モジュールは`"use client"`でクライアント実行を宣言します。サーバー境界は固定した1つのクライアントターゲットを静的にインポートし、シリアライズ可能な`props`を渡します。コンポーネント関数やシグナルをこの転送境界へ渡せません。

```tsx title="src/components/counter.tsx"
/** @jsxRuntime automatic */
/** @jsxImportSource @takazudo/zfb/zudo-react */
"use client";

import { computed, getScope, signal } from "@takazudo/zfb/zudo-react";

export default function Counter() {
  const scope = getScope();
  const count = signal(0);
  const label = computed(() => `Count: ${count.value}`);

  scope.onActivate(() => {
    // Browser-only setup belongs here. Return cleanup when needed.
  });

  return (
    <button
      type="button"
      class="rounded-full bg-surface px-hsp-sm py-vsp-2xs text-caption text-fg"
      on:click={() => { count.value += 1; }}
    >
      {label}
    </button>
  );
}
```

```tsx title="src/components/counter-island.tsx"
/** @jsxRuntime automatic */
/** @jsxImportSource @takazudo/zfb/zudo-react */
import { Island } from "@takazudo/zfb";
import Counter from "./counter";

export function CounterIsland() {
  return <>{Island({ when: "visible", children: <Counter /> })}</>;
}
```

```tsx title="src/chrome-bindings.tsx"
import { defineChromeBindings } from "@takazudo/zudo-doc/chrome-bindings";
import { CounterIsland } from "./components/counter-island";

export const chromeBindings = defineChromeBindings({
  mdxExtras: { CounterIsland },
});
```

MDXでは`<CounterIsland />`を使います。生成されたvirtual bindingsだけではカスタムアイランドのスキャナー到達性を保証できません。対象の各ドキュメントルートからホスト境界へ静的に到達できるようにし、locale/versionルートの相対インポートを調整します。`displayName`を設定したり、描画済みの子要素をアイランドのターゲットとして渡したりしないでください。ルートグラフを変更したら、実際のビルドでマーカーとブラウザーでの有効化を確認します。

このカスタムアイランドの場合だけ、対象の各ルートスタブにある`virtual:zudo-doc-chrome-bindings`のインポートを直接の静的インポートへ置き換えます。`createChrome(routeCtx, chromeBindings)`を含むルートの残りはそのまま維持します。デフォルトとロケール版のドキュメントルートのインポート部分は次の形になります。

```tsx title="pages/docs/[[...slug]].tsx (import section)"
import { chromeBindings } from "../../src/chrome-bindings";
```

```tsx title="pages/[locale]/docs/[[...slug]].tsx (import section)"
import { chromeBindings } from "../../../src/chrome-bindings";
```

バージョン付きスタブにも直接の静的インポートが必要です。ディレクトリの深さに応じて相対パスを調整します（`pages/v/[version]/docs/`では`../../../../src/chrome-bindings`、localeとversionの組み合わせではさらに1階層増えます）。パッケージ注入ルートでは、同じ静的境界へ到達できるプロジェクト所有のルートを用意します。これはカスタムクライアントアイランドを検出するための高度なルートカスタマイズです。通常の表示専用`mdxExtras`では生成されたvirtualインポートを維持します。

zudo-reactはスコープごとにコンポーネントのセットアップを1回実行します。反応する出力にはシグナルやcomputedシグナルを直接JSXへ渡します。`{count.value}`ではセットアップ時の値だけを読みます。`scope.onActivate`はブラウザーでの初期化を所有し、cleanupを返せます。`scope.effect`はリアクティブな副作用を所有します。セットアップヘルパーにはReact hooksを模倣せず、`scope`とシグナルを明示的に渡します。fetchには`scope.abortSignal`を渡し、importやリクエストのawait後もDOM操作の前に中断状態を確認します。

## フォームとイベント

modelアダプターはtext input、checkbox input、radio input、textarea、single-selectの5種類です。text/textarea/selectは書き込み可能な文字列シグナルの`modelValue`、checkboxはbooleanシグナルの`modelChecked`、radioは文字列またはnullのシグナルの`modelValue`と明示的なvalueを使います。対応しないinputやmultiple selectにはネイティブハンドラーを記述します。modelと競合するvalue/checkedハンドラーを併用しないでください。

```tsx title="src/components/name-field.tsx"
/** @jsxRuntime automatic */
/** @jsxImportSource @takazudo/zfb/zudo-react */
"use client";

import { signal } from "@takazudo/zfb/zudo-react";

export function NameField() {
  const name = signal("");
  return <input type="text" modelValue={name} aria-label="Name" />;
}
```

```tsx
function handleInput(event: Event) {
  const target = event.currentTarget;
  if (!(target instanceof HTMLInputElement)) return;
  console.log(target.value);
}
```

`on:click`、`on:input`、`on:event-name:capture`でイベントハンドラーを指定します。ハンドラーはネイティブの`Event`を受け取ります。要素固有のフィールドを読む前に`currentTarget`をローカルで絞り込みます。

## 構造の更新と信頼済みHTML

boolean分岐には`Show`、変化するリストにはキー付き`For`を使います。条件がtrueのままの`Show`は子スコープを保持します。`For`のキー変更は古いスコープを破棄して新しいスコープを作ります。通常の要素のJSX `key`には同じリセット動作はありません。tableの子はネイティブのsection/row、sectionの子はrow、rowの子はcellという構造を維持します。これらの制約された親の直下へ`Show`/`For`のマーカー構造を置かず、完全なtableの外側に構造境界を置きます。

`rawHtml`は使用箇所ごとに信頼境界を確認してから使います。サニタイズは行いません。script/styleのraw bodyは静的文字列のみで、リアクティブな置き換えや子要素との混在はできません。ユーザーのテキストにはエスケープされる通常の子要素を優先します。`@takazudo/zudo-doc/use-modal-dialog`の`modalDialog(scope, options)`はスコープ内のネイティブdialogを所有します。条件付きdialogでは`Show`内の子コンポーネントでヘルパーを呼び、refと寿命をその子に所属させます。



## 関連ページ

- [Customizing zudo-doc](https://zudo-doc.takazudomodular.com/ja/docs/reference/customizing)
- [Writing Documentation](https://zudo-doc.takazudomodular.com/ja/docs/getting-started/writing-docs)
- [Component First Strategy](https://zudo-doc.takazudomodular.com/ja/docs/reference/component-first)