ホストクロームバインディング
パッケージ所有ルートの主要クローム、名前付きヘッダー項目、コンテンツへホスト側の実装を注入する。
プロジェクトで packageOwnedRoutes: true を設定すると、@takazudo/zudo-doc がドキュメントルートを自前で注入し、プロジェクト側はほぼ空の pages/ ディレクトリだけを持つことになります。これは便利な反面、通常であればプロジェクト固有のレンダリングを差し込むホストファイルが失われます。ホストクロームバインディングはそれを取り戻すための継ぎ目です。型付けされた小さなスロット群(ChromeHostBindings)を通じて、コンポーネントをエジェクトすることなく、パッケージ所有のクロームにホスト独自のコンテンツやコーラブルを注入できます。
このページでは、6つの主要クローム置換、名前付きヘッダー右コンポーネント、docContentHeaderExtras と homeExtras のコンテンツ継ぎ目、そしてすべてのバインディングを注入ルートと自己完結型ルートへ届ける settings.chromeBindingsModule を扱います。
Note
これらの継ぎ目が主に意味を持つのは packageOwnedRoutes: true のときです。自前の pages/*.tsx スタブを引き続き持つプロジェクトは、レンダリング先のホストファイルをすでに持っており、createChrome(context, hostBindings) を直接呼び出せます。パッケージ所有ルートがどのように列挙されるかはルーティング規約を参照してください。
ChromeHostBindings 型
ここで説明する継ぎ目はすべて ChromeHostBindings インターフェースのフィールドで、@takazudo/ からエクスポートされています。
import type { ChromeHostBindings } from "@takazudo/zudo-doc/factory-context";すべてのフィールドは任意です。省略したスロットは、継ぎ目導入前の挙動をバイト単位で再現するパッケージデフォルトにフォールバックするため、部分的なバインディングオブジェクトでも常に安全です。
ChromeHostBindings は、クローム自身の呼び出し地点が内部で必要とする広い構造的な形状で各スロットを型付けします。実際の狭い型を持つコンポーネントやコーラブルをそのまま直接代入する — あるいは代入をコンパイル可能にするために as/as unknown as キャストに頼る — と、本当に重要なチェック、すなわち「渡した値がクロームから渡されるプロパティを実際に受け取れるかどうか」が消えてしまいます。代わりに @takazudo/ からエクスポートされる defineChromeBindings でバインディングを組み立ててください。その入力型 ChromeBindingsInput は、各スロットの実際の呼び出し地点が渡す正確なプロパティや引数を宣言しているため、クロームが決して渡さないプロパティをコンポーネントが要求している場合はコンパイルエラーになります(#2674 のドリフト検出チェック)。広い ChromeHostBindings 形状は、あなたが気にする必要のない内部の単一のワイドニングステップによって生成されます。
配信チャネル: chromeBindingsModule
packageOwnedRoutes: true ではクロームがパッケージ内部で組み立てられるため、バインディングを渡すためのホスト側の呼び出し地点が存在しません。プラグインがルートへ渡すルートコンテキストはシリアライズ可能なデータのみ(settings、translations、タグ語彙)を運ぶため、関数やコンポーネントを運ぶことはできません。このチャネルはその制約を回避します。コーラブルそのものをシリアライズするのではなく、それらをエクスポートするモジュールへのパスをシリアライズするのです。
zudoDoc() 設定の chromeBindingsModule に、defineChromeBindings で組み立てた名前付き chromeBindings エクスポートを持つモジュールを指すプロジェクトルート相対パスを設定します。
import { defineConfig } from "zfb/config";
import { zudoDoc } from "@takazudo/zudo-doc/config";
export default defineConfig(
zudoDoc({
// packageOwnedRoutes defaults to true
chromeBindingsModule: "./src/chrome-bindings.tsx",
}),
);import { defineChromeBindings } from "@takazudo/zudo-doc/chrome-bindings";
export const chromeBindings = defineChromeBindings({
// ...seams go here (see below)
});ビルド時にルートプラグインが chromeBindings を再エクスポートする仮想モジュールを登録し、注入されたクロームシムがそれをクロームファクトリへスプレッドします。文字列パスはシリアライズ可能なので、「ルートコンテキストはデータのみ」というルールは依然として保たれます — 異なるのはローダーのソースだけです。
scaffold とカスタムルートの配線
新しい scaffold のドキュメントルートスタブは virtual:zudo-doc-chrome-bindings をインポートし、その値を createChrome へ渡します。そのため、同じモジュールが自己完結型ルートと注入ルートの両方へ届きます。ルートをゼロから作成する場合も、createChrome(routeCtx, chromeBindings) の第2引数として渡してください。mdxExtras のグローバル登録パターンにルート側の完全な例があります。
名前付きヘッダー右コンポーネント
headerRightItems はシリアライズ可能なままです。カスタムコンポーネント項目には文字列名だけを置き、呼び出し可能なレンダラーは chromeBindings.headerRightComponents で同じ名前へ登録します。完全な設定例は次のとおりです。
import { defineConfig } from "zfb/config";
import { zudoDoc } from "@takazudo/zudo-doc/config";
export default defineConfig(
zudoDoc({
siteName: "Acme Docs",
chromeBindingsModule: "./src/chrome-bindings.tsx",
headerRightItems: [
{ type: "link", href: "/status", label: "Status" },
{ type: "component", component: "release-badge" },
{ type: "component", component: "search" },
],
}),
);import { defineChromeBindings } from "@takazudo/zudo-doc/chrome-bindings";
export const chromeBindings = defineChromeBindings({
headerRightComponents: {
"release-badge": ({ item, index, lang }) => (
<a
href={`/${lang ?? "en"}/releases`}
data-component={item.component}
data-position={index}
>
v4
</a>
),
},
});レンダラーはシリアライズ済みの item、0始まりの index、ロケール、GitHub 値、組み込み子スロット、カラーモード/ロケールのゲートを受け取ります。組み込み名(theme-toggle、language-switcher、version-switcher、github-link、search)は予約済みです。未知の名前や予約名の上書きは、黙って消えるのではなく、設定位置と修正方法を含むエラーになります。
ルートコンテキストを通るのは項目名だけで、headerRightComponents 自体はホストのコーラブルモジュールに残ります。ほかの bindings モジュール内コンポーネントと同様、静的なアイランド登録経路も持たない限り SSR 表示用として扱ってください。
セッション固有の island props を保持する
同一ロケール内でページを移動すると、persist されたヘッダー内の island が持つシリアライズ済み props は、デフォルトでは遷移先ページの値に更新されます。ページから導出する props にはこの動作を使ってください。一方、静的に登録したホスト island がセッション固有の props を持つ場合 — たとえば、初期カウントを次ページの SSR スナップショットで置き換えたくないバッジ — は、island のルート要素か persist されたヘッダー内の祖先要素に data-zd-props-preserve を付けます。
<div data-zd-props-preserve>
<SessionBadge initialCount={unreadCount} />
</div>オプトアウトの判定に使われるのは遷移前から存在する live DOM です。遷移先ページだけに属性を付けても、現在の island は保持されません。保持対象の island では data-props の更新も remount フラグの付与も行われません。persist されたヘッダー自体に data-zd-props-preserve を付けると、配下の全 island が一括でオプトアウトされます。そのため、セッション固有の props を所有する範囲だけを囲むようにしてください。
既存スロットの「取り残し」も解消する
docContentHeaderExtras と homeExtras は新しい継ぎ目ですが、ChromeHostBindings にはこのチャネルが存在する以前から、注入ルートへ届く手段がなかったスロットがいくつかありました — それらはスタブのデフォルトのまま黙って据え置かれていました。chromeBindingsModule はそれらをまとめて解消します。
| スロット | 省略時のデフォルト |
|---|---|
Header | パッケージの HeaderWithDefaults。HeaderSlotProps を受け取る |
Footer | パッケージの FooterWithDefaults。FooterSlotProps を受け取る |
Sidebar | パッケージの SidebarWithDefaults。SidebarSlotProps を受け取る |
Toc | パッケージのデスクトップ Toc。TocSlotProps を受け取る |
Breadcrumb | パッケージの Breadcrumb。BreadcrumbSlotProps を受け取る |
DocPager | パッケージの DocPager。DocPagerSlotProps を受け取る |
SearchWidget | サイトの base を内部で束縛したパッケージ検索ウィジェット |
headerRightComponents | {} — パッケージ組み込みのヘッダー右名だけを解決する |
docHistoryMeta | {} — Created/Updated ブロックなし |
sidebarsConfig | {} — 自動生成ツリーのみ |
frontmatterRenderers | {} — カスタムフロントマタープレビューレンダラーなし |
buildFrontmatterPreviewEntries | () => [] — プレビューテーブルが描画されない |
loadTagsForLocale | () => [] — フッターのタグエントリーなし |
tagVocabulary | [] — フッターのタグ語彙なし |
BodyEndIslands | settings から導出されるパッケージのアイランド群 |
DocHistory | 何も描画しない no-op スタブ |
DesignTokenPanelBootstrap | パッケージの固定ターゲットのサーバー境界。ホスト配線なしで designTokenPanel: true のときにマウントされる。このスロットは、パッケージのパネルがオンの間に、どのビルダーをマウントするかを差し替えるだけです — ここに何を渡しても designTokenPanel: false のままではマウントされません。パッケージのパネルをオフにしたまま自前のパネルをマウントしたい場合は、自前のアイランド(たとえば下の BodyEndIslands スロット経由で配線)から runDesignTokenPanelBootstrapOnce を呼び出し、bundleZdtp: true を設定してください。設定しないと、本物のローダーは例外を投げるスタブのままです。自前のパネルをマウントするを参照してください。 |
mdxExtras | パッケージの SSR コンポーネント(Details、HtmlPreview、Island)と no-op の PresetGenerator |
docContentHeaderExtras | 未指定 — 何も描画されない |
homeExtras | 未指定 — 何も描画されない |
つまり、docContentHeaderExtras のために作成するモジュールは、たとえば以前はホスト所有ルートでしか機能しなかったカスタムフロントマターレンダラーを登録する場所でもあります。
sidebarNodes は createHeaderWithDefaults の prop であり、Header バインディングのスロットではない
上表の Header 行はコンポーネント全体を置き換えるものです。置き換えたコンポーネントは HeaderSlotProps を受け取り、モバイルドロワーのツリーを完全に自前で所有します。sidebarNodes はこれとは別の、より狭い継ぎ目です — HeaderWithDefaultsProps のオプション prop で、ヘッダーの残りを置き換えずに、パッケージのヘッダーファクトリーが SidebarToggle に渡すツリーだけを上書きします。この prop は createHeaderWithDefaults(ctx) と createChrome(...).HeaderWithDefaults で利用できます。HeaderSlotProps には含まれないため、chromeBindingsModule で設定した Header の置き換えコンポーネントはこの prop を受け取りません。完全な契約は API.md の createHeaderWithDefaults(ctx) を参照してください。
生成されたドキュメントルートとパッケージ所有ドキュメントルートでは、通常のオブジェクト優先順位に1つだけ意図的な例外があります。docHistory が有効な場合、静的にインポートされたパッケージ DocHistory を設定済みオブジェクトの後から重ねます。これによりクライアントアイランド登録をスキャナーから到達可能に保ち、ほかのすべての binding は維持されます。仮想ホストモジュール内だけで宣言した DocHistory は、これらのルートでは置き換えられません。
Warning
SSRの表示専用契約のみ。バインディングモジュールの内部で定義したクライアントアイランドは、注入ルートでハイドレートされる保証はありません — 仮想再エクスポートは zfb の静的インポートスキャナーが到達可能なグラフの外側に位置するためです。バインディングモジュールはサーバーレンダリングされるコンテンツとコーラブルに使ってください。注入ルート上でハイドレートするアイランドが必要な場合は、依然として静的にインポートされる登録経路が必要です。
Info
ファイル欠落時も空文字列指定時も、挙動は明示的。chromeBindingsModule が設定されているのに解決先のファイルが存在しない場合、ビルドはプラグインのセットアップ時に、解決された絶対パスを示すエラーで失敗します — 黙って空にフォールバックすることはありません。chromeBindingsModule に空文字列や空白のみの文字列が設定されている場合も、ビルドはプラグインのセットアップ時に、設定名を示すエラーで失敗します。設定自体が省略されている場合(未設定の場合)、チャネルは export const chromeBindings = {} を出力し、設定を省略した場合とバイト単位で同一の挙動になります。
docContentHeaderExtras — コンテンツヘッダーへの注入
docContentHeaderExtras は、ドキュメントのコンテンツヘッダー内、ページの <h1> とメタ情報ブロックの間に追加コンテンツを描画します。これはページエントリを受け取ってレンダリング可能な出力を返すレンダラーであり、プロパティから再導出するのではなく、現在のページのフロントマターを自然にキーとして扱えます。
docContentHeaderExtras?: (args: {
entry: DocPageEntry;
slug: string;
locale: string;
isFallback?: boolean;
version?: string;
}) => unknown;よくある用途は、カスタムフロントマターフィールドから導出するティアバッジです。フロントマターで tier: core または tier: opt-in を宣言するページに対して次のように書けます。
import { defineChromeBindings } from "@takazudo/zudo-doc/chrome-bindings";
export const chromeBindings = defineChromeBindings({
docContentHeaderExtras: ({ entry }) => {
const tier = entry.data.tier;
if (tier !== "core" && tier !== "opt-in") return null;
const label = tier === "core" ? "Core" : "Opt-in";
const tone = tier === "core" ? "bg-accent text-bg" : "bg-surface text-fg";
return (
<span
class={`inline-block px-hsp-sm py-vsp-2xs text-caption rounded-full ${tone}`}
>
{label}
</span>
);
},
});デフォルト: 未設定 → 何も描画されず、ヘッダー出力は継ぎ目導入前のヘッダーとバイト単位で同一になります。
バージョン付きページ
このレンダラーは4つのドキュメントルートすべてで entry ドキュメントページに対して呼び出され、バージョン付きページも含みます。バージョン付きページでは version 引数を受け取ります。バージョン付きページではメタ情報ブロックとタグは非表示になります — そこで描画するかどうか、どう描画するかはレンダラー自身が判断します。バージョン付きルートのモデルについてはバージョニングを参照してください。
homeExtras と HomePageView — ホームヒーローへの注入
homeExtras は、ホームヒーローのリンク行の末尾に、直前の要素(プライマリリンクや GitHub リンク)と / 区切りで、追加コンテンツをインラインで描画します — 例えば overview/GitHub のリンクと並べてブランドやソーシャルのリンクを置く用途です。
段落、リスト、長い紹介文には home.introMarkdown を使います。サイトの識別情報の下に独立したコンパクトな文章領域を表示し、homeExtras と extras は引き続きインラインのナビゲーション追加に使います。
homeExtras?: (args: { locale: string }) => unknown;import { defineChromeBindings } from "@takazudo/zudo-doc/chrome-bindings";
export const chromeBindings = defineChromeBindings({
homeExtras: ({ locale }) => (
<a
href="https://example.com/blog"
class="text-fg underline hover:text-accent"
>
Read the blog
</a>
),
});優先順位: extras プロパティが勝つ
共有のホーム本体は HomePageView が組み立て、パッケージルートとホストページのどちらもこれを呼び出します。HomePageView は extras プロパティ — ホストページが直接渡す描画済みの値 — を受け取ります。両方が存在する場合はプロパティが勝ちます。
// resolved inside HomePageView as:
extras ?? hostBindings.homeExtras?.({ locale })この非対称性は意図的なものです。ホストページは手元に JSX(値)を持っている一方、注入/バインディング経路はレンダリング時に locale 文字列しか持たず、そこから自前でコンテンツを導出しなければなりません(レンダラー)。
/ ルートのトポロジー
/ にあるデフォルトホームは、ルートプラグインによって決して注入されません — zfb が / パターンを拒否するためです(アップストリーム Takazudo/zudo-front-builder#1227)。scaffold の pages/ は @takazudo/ を再エクスポートする1行のファイルで、extras プロパティは渡しません。使用する注入済み / ルートが存在しないため、/ では常にホストファイルが優先されます。
変更していない
/ホームはパッケージルートの再エクスポートを使い、extrasプロパティ経由でhomeExtrasを受け取りません。注入された
/ホームは[locale] chromeBindingsModuleを通じてhostBindings.homeExtrasを読み取ります。
ルートホームをカスタマイズするには、再エクスポートを HomePageView を呼び出して extras を渡す自前のページに置き換えます。i18n サイトでは、その値とロケールホーム用の homeExtras が返す値を同じ内容にしてください。
Design Token Panel チャネル(designTokenPanelConfigModule)
Design Token Panel には独自のホストモジュールチャネルがあり、chromeBindingsModule とまったく同じ仕組みで動作します — 同じメカニクス、同じ「ファイルが無ければ大きな声で失敗する」挙動 — ただしクロームバインディングの代わりにパネルの設定ビルダーを運びます。
パネルは動作にホスト設定ファイルを必要としません。designTokenPanel: true にすると、注入された DesignTokenPanelBootstrap アイランドは、出荷済みのトークンマニフェストとバンドルされた Default Light / Default Dark スキームから導出されたパッケージデフォルトのビルダー(@takazudo/)を使います。これがゼロコンフィグの経路です。
パネルを完全にカスタマイズするには、designTokenPanelConfigModule に、名前付きの buildDesignTokenPanelConfig(mode) をエクスポートするホストモジュールを指すプロジェクトルート相対パスを設定します:
import type { PanelConfig } from "@takazudo/zdtp";
type PanelMode = "light" | "dark";
export function buildDesignTokenPanelConfig(mode: PanelMode): PanelConfig {
// return a mode-scoped panel config (spacing/font/size/color tiers)
// ...
}ルートプラグインは3番目の仮想モジュール(virtual:zudo-doc-design-token-panel-config)を登録し、あなたのビルダーを re-export します。chromeBindingsModule と同様、設定を通じて移動するのはパスだけで、ビルダー自体はモジュールグラフ経由でインポートされます。
Note
designTokenPanelConfigModule は chromeBindingsModule と同様、ZudoDocConfig の第一級フィールドです — インラインの zudoDoc({ … }) キーとしてそのまま渡せます。パネルの有効化(designTokenPanel: true)は従来どおりインラインで動作します。
chromeBindingsModule との違いで知っておくべき点があります:
デフォルトは空オブジェクトではなくパッケージビルダーです。 設定が無い場合、ローダーはパッケージデフォルト(完全に動作するパネル)を re-export します — フォールバックすべき意味のある「空の」ビルダーは存在しません。
スキャナー到達可能性がこのコントラクトの一部です。 各サーバー境界はクライアントのターゲットを静的にインポートします。パッケージデフォルトとルート専用の設定済み境界は、それぞれ別の固定ターゲットを持ち、設定済みクライアントは設定仮想モジュール経由でビルダーをインポートします。ホストの差し替えも、バインディングマップに生のクライアントターゲットを渡すのではなく、以下の境界合成規則に従います。
この設定がある間、自己完結型の
pages/スタブはパネルをマウントしません。 この設定はパッケージ注入ルートでレンダリングされるページにしか届かず、パネルの設定はブラウザセッションごとに一度だけ行われます — そのため、パッケージデフォルトのブートストラップをマウントするスタブページがハードロードの入口になると、セッション全体の設定をそのページが決めてしまっていました。設定が存在する場合(かつ明示的なchromeBindings.DesignTokenPanelBootstrapが無い場合)、スタブでレンダリングされるページはパネルアイランド(およびヘッダーのパネルボタン)を一切マウントしません。注入ルートはどの入口ページからでも、あなたの設定済みパネルを受け取ります。スタブページにもパネルを出したい場合は、クライアントのブートストラップを以下のサーバー境界経由で渡してください — このバインディングはどこでも優先されます。
同じガード規則が適用されます: 明示的な空文字列、存在しないファイル、ディレクトリパスは、プラグインのセットアップ時に解決済みパスを明示して大きな声で失敗します。storagePrefix: "zudo-doc-tweak" はパッケージデフォルトで維持されるため、既存のユーザー保存は引き継がれます。
DocHistory の境界
DocHistory スロットもサーバー境界を受け取ります。パッケージのクライアントを使う場合は、@takazudo/ から DocHistoryBoundary をインポートし、DocHistory として登録してください。この境界は任意の ssrFallback 子要素をサーバー側で処理し、固定クライアントターゲットを when="idle" の Island と Fragment で包みます。カスタム境界でも ssrFallback を取り除いてから、残りのシリアライズ可能な props をクライアントへ渡してください。フォールバックの JSX をクライアントデータとして転送してはいけません。領域を描画するかどうかは、既存のドキュメント履歴の設定と除外ゲートが引き続き制御します。
パネル境界の差し替え
v6 の chromeBindings.DesignTokenPanelBootstrap は、生の "use client" コンポーネントではなく、props を受け取らないサーバー境界を受け取ります。クライアントのブートストラップを静的にインポートし、固定ターゲットの Island を Fragment で包んで返します。このサーバーファイルはクライアントモジュールと分けてください。クライアントのブートストラップでは、自前のパネルをマウントするの例のように、getScope().onActivate 内で runDesignTokenPanelBootstrapOnce を起動します。
/** @jsxRuntime automatic */
import { Island } from "@takazudo/zfb";
import { HostPanelBootstrap } from "./host-panel-bootstrap";
export function HostPanelBoundary() {
return <>{Island({ when: "load", children: <HostPanelBootstrap /> })}</>;
}import { defineChromeBindings } from "@takazudo/zudo-doc/chrome-bindings";
import { HostPanelBoundary } from "./panel-boundary";
export const chromeBindings = defineChromeBindings({
DesignTokenPanelBootstrap: HostPanelBoundary,
});この差し替えスロットでは designTokenPanel: true を維持します。設定のゲートとハイドレーション前のトグル shim は引き続きパッケージが管理し、機能を無効にすると境界も shim も出力しません。明示的なホスト境界は、designTokenPanelConfigModule がパッケージデフォルトを抑制する場合も優先され、注入ルートの設定済みデフォルトよりも優先されます。パッケージ機能を無効にして独立したホストパネルをマウントする場合は、リンク先の独立した BodyEndIslands 経路を使ってください。
createBodyEndIslands を直接使う場合も、任意の依存コンポーネント DesignTokenPanelBootstrap と ThemePackSwitcher は同じ合成規則に従います。渡すコンポーネント側が固定ターゲットの Fragment で包んだ境界を所有し、ファクトリーは機能のゲート内で描画するだけです。依存コンポーネントを省略した場合、そのアイランドは従来どおり何も描画しません。クライアント関数をアイランドの props として渡すことはありません。
関連ページ
zudo-docのカスタマイズ — 最小→拡張のはしご。
chromeBindingsModuleはその6段目フロントマタープレビュー — このチャネルが取り残しを解消する
frontmatterRenderersスロット設定 —
zudoDoc()の完全なフィールドリファレンスDesign Token Panel — このチャネルが設定するパネル UI