zudo-doc
GitHub リポジトリ

検索したい単語を入力

いつでも検索バーを開ける

カスタムコンポーネント

作成 2026年7月13日更新 2026年10月11日Takeshi Takatsudo

表示用 MDX コンポーネント、オーバーライド、実験的なインタラクティブアイランドを追加する。

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

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

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

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

<MyBadge>New</MyBadge>

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

グローバル登録: mdxExtras

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

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>
  );
}
src/chrome-bindings.tsx
import { defineChromeBindings } from "@takazudo/zudo-doc/chrome-bindings";
import { MyBadge } from "./components/my-badge";

export const chromeBindings = defineChromeBindings({
  mdxExtras: { MyBadge },
});
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を参照してください。

<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を参照してください。

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を渡します。コンポーネント関数やシグナルをこの転送境界へ渡せません。

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>
  );
}
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 /> })}</>;
}
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)を含むルートの残りはそのまま維持します。デフォルトとロケール版のドキュメントルートのインポート部分は次の形になります。

pages/docs/[[...slug]].tsx (import section)
import { chromeBindings } from "../../src/chrome-bindings";
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ハンドラーを併用しないでください。

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" />;
}
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と寿命をその子に所属させます。

関連ページ

Revision History

Takeshi Takatsudo作成: 2026-07-13T23:42:56+09:00更新: 2026-10-11T20:18:53+09:00

AI Assistant

Ask a question about the documentation.

Preview theme

Loading theme previews…