# zfb > The Rust engine under your content-site framework — router, renderer, content pipeline. Author in TypeScript/JSX, runs as a single binary. --- # zfb を選ぶ > Source: https://takazudomodular.com/pj/zudo-front-builder/ja/docs/concepts/choosing-zfb zfb があなたのプロジェクトに適したツールかどうか — zfb が得意でないことについての 率直な見解も含みます。[アーキテクチャ概要](/architecture/build-engine)、 [getting-started ガイド](/getting-started)、そして [README](https://github.com/Takazudo/zudo-front-builder#limits) のスケールに関する 注記と相互リンクしています。 zfb は焦点を絞ったツールです。特定の問題をうまく解決し、それ以外の問題は意図的に他の ツールに委ねます。このページではそれを整理し、時間を投資する前に判断できるようにします。 ## zfb がよく合うとき 以下の多くがあなたのプロジェクトに当てはまるなら、zfb は強くマッチします。 **コンテンツ中心の、静的生成サイト。** 真実の源はリポジトリ内の Markdown または MDX ファイルです。それらのファイルを静的 HTML に変えるビルドステップが欲しく、そのビルドが 高速で決定的であってほしい。zfb のアーキテクチャ全体はこのケースを軸に形作られています。 **ページ単位の依存グラフ。** 数百のページがあり、編集のたびにフルリビルドはしたくない。 zfb は各ページがどのソースファイルに依存しているかを追跡し、何かが変わったときに影響を 受けるページだけをリビルドします。依存グラフ(`crates/zfb-graph`)は後付けではなく、 エンジンの第一級の構成要素です。 **workerd の形をした出力バンドル。** ターゲットが Cloudflare Workers または Pages。 zfb のビルド時 JS ホストは V8 ベース(組み込み V8 経由)で、workerd と同じエンジン ファミリーです。コンテンツスナップショットとアイランドバンドルはその環境向けのサイズに 収まっています。Workers にデプロイするなら、実際にフィットするバンドルの形が得られます。 **Cloudflare Pages に優しいデプロイ。** `zfb build` の静的出力は、Pages デプロイに そのまま投入できます。管理すべきサーバーランタイムも、設定すべきサーバーレス関数も、 アダプタ層もありません。ビルドされた `dist/` がデプロイの成果物そのものです。 **軽量なインタラクティブアイランド。** ページはほとんどが静的なドキュメントで、少数の インタラクティブなコンポーネント(検索ボックス、カウンター、モーダルのトリガーなど)が あるだけ。zfb のアイランドモデル(`"use client"`)は、各ページが使うアイランドの JS を ちょうどそれだけ配信し、それ以外は何も配信しません。アイランドのないページが配信する クライアント JS は 0 バイトです。 ## 比較 | | **zfb** | **Astro** | **Next.js** | |---|---|---|---| | CLI 言語 | Rust | Node.js | Node.js | | TSX コンパイル | SWC(組み込み) | Vite/Rollup | SWC/Webpack | | バンドラ | esbuild(アイランド) | Vite | Webpack/Turbopack | | ビルド時 JS ホスト | 組み込み V8 | Node.js | Node.js | | クライアントフレームワーク | 任意の TSX(`"use client"`) | 任意(アダプタ) | React | 最も意味のある違いは **ビルド時 JS ホスト** です。zfb は組み込み V8 を直接使います。 Cloudflare の workerd と同じエンジンファミリーです。Astro と Next.js はどちらもフルの Node.js プロセスでビルドします。Workers をターゲットにしたデプロイでは、zfb のホストは ターゲットランタイムに近く、グローバルの可用性やモジュール解決まわりでの驚きが少なくなります。 ## 別のものを選ぶとき zfb が誤った選択になるケースはいくつかあります。コミットする前に自分自身に正直になりましょう。 **サイトのほとんど、またはすべてのページが動的。** zfb はデフォルトで SSG です。すべての ページはビルド時に静的 HTML へ計算されます。リクエスト時の振る舞いが必要なルートは `prerender = false` でオプトアウトでき、アダプタによって配信されます([SSR and Cloudflare bindings](/guides/ssr-and-cloudflare-bindings) を参照)。これは、それ以外は静的なサイト上の わずかな動的ルートをカバーします。もし *すべて* のページが動的(ユーザーごと、リクエスト ごと)なら、zfb は誤ったツールであり、フルの SSR フレームワークが正しい選択です。 **幅広いマルチフレームワークサポートが必要。** Astro のアダプタエコシステムは、同じサイトの 中で React・Vue・Svelte・Solid などを切り替えられます。zfb のクライアント側は TSX (React または Preact)です。チームが複数のフレームワークのコンポーネントを抱えていて、 それらをすべて 1 つのサイトに収めたいなら、zfb は適切なホストではありません。 **ページが 100,000 以上ある。** zfb はインメモリのコンテンツスナップショットを構築し、 レンダーパスのあいだ V8 ホストに埋め込みます。数百から数千の MDX ファイルなら、デフォルトの workerd メモリに余裕で収まります。非常に大規模なコーパス(数万のエントリ、または数メガバイトの 本文を持つエントリ)では、V8 の RSS がスナップショットのサイズに比例して線形に増えます。 その規模では、スナップショットの設計を見直す必要があり(下記の [スケールのスイートスポット](#スケールのスイートスポット) セクションを参照)、zfb は今日その問題を解決しません。 **実績があり、広く採用され、大きなエコシステムを持つツールが欲しい。** zfb は焦点を絞った、意見の強いエンジンです。大きなコミュニティ、豊富なプラグインエコシステム、あるいは今日すぐに使える実戦投入済みの本番スケールが必要なら、[Astro](https://astro.build) が正しい答えです。 ## スケールのスイートスポット このエンジンは **数百から数千の MDX ページ** 向けに設計されています。それがカバーするのは: - 典型的なドキュメントサイト(50〜500 ページ) - 数年分のアーカイブを持つ開発者ブログ(100〜2000 記事) - `zudo-doc` 規模のコンテンツセット そのサイズではコンテンツスナップショットは小さく、コールドスタートのビルド時間は秒単位で 測られ、依存グラフは総ページ数に関係なくインクリメンタルリビルドを高速に保ちます。 **おおよそ 10,000 ページを超えると**、インメモリのスナップショットアプローチがボトルネックに なります。その規模での正しい答えは、より大きなモノリスを作ることではありません。コンテンツを 複数の zfb プロジェクトにシャーディングし(セクションごと、ロケールごと、コンテンツタイプ ごとに 1 つ)、CDN 層でそれらを合成することです。その合成のストーリーはロードマップにありますが、 今日は実装されていません。 ビルドの実際のスナップショットのフットプリントを調べるには: ```sh ZFB_DEBUG_SNAPSHOT=1 pnpm exec zfb build ``` これは stderr に 1 行だけ出力します。 ``` content snapshot: 187 entries / 412 KB ``` ここでの `KB` は決定的な JSON シリアライズのバイトサイズであり、V8 ヒープコストの信頼できる 代理指標です。コンテンツが増えるにつれてこの数値を見守ってください。数百メガバイトを超えて 登っていくなら、スイートスポットの端に近づいています。メモリモデルの完全な説明は [README scaling note](https://github.com/Takazudo/zudo-front-builder#limits) にあります。 ## 次に行く先 - [Getting started](/getting-started) — 最初のサイトをスキャフォールドします。 - [Design philosophy](/concepts/design-philosophy) — 「意図的に狭く」というスタンスと、この一連のトレードオフを説明する「プラグインよりレシピ」というフレーミング。 - [Architecture: Build engine](/architecture/build-engine) — Rust クレート、JS ホスト、スナップショットがどう組み合わさるのか。 - [Engine vs Framework](/concepts/engine-vs-framework) — zfb がコミットする 6 つのプリミティブと、エンジンの外側にあるもの。 --- # 設計哲学 > Source: https://takazudomodular.com/pj/zudo-front-builder/ja/docs/concepts/design-philosophy zfb のあらゆる決定を貫く 2 つの設計原則。表面積を広げるのではなく意図的に狭いままでいる という選択と、なぜ — LLM があらゆるワークフローに組み込まれた時代において — 小さな普遍的 コアの外側ではほぼすべてにおいてプラグインよりレシピが勝つのか。 zfb の表面積は意図的に小さく保たれています。その選択にはコストがあり、そしてメリットがあります。 このページは両方を明示し、そのうえで、LLM がループに入った変化がなぜレシピ経路を以前よりさらに 魅力的にするのかを説明します。 ## 意図的に狭く zfb は esbuild の SSG + SSR 版です。コミットしたことだけを正確に行い、そこで止まる、速くて 焦点を絞ったエンジンです。esbuild があらゆる変換のためのプラグインエコシステムを備えた完全な アプリケーションバンドラになることを意図的に避けているのと同じように、zfb はあらゆるサイト 構築パターンをカバーするために分厚い API 表面を育てることを意図的に避けています。 [6 つのエンジンプリミティブ](/concepts/engine-vs-framework) が zfb v1 の公開コントラクトの すべてです。7 つ目のプリミティブを追加することは — たとえそれが有用であっても — 設計し、 ドキュメント化し、メンテナンスし、将来のリリースを通じてバージョン管理すべき新しい API が 増えることを意味します。新しい表面上のエッジケースについて「これは可能か?」という問いに 答えることを意味します。そして、zfb の上に構築するフレームワーク作者が、さらにもう 1 つの軸に ついて考えなければならなくなることを意味します。 **狭いままでいることのコスト** は本物です。グルーは利用者が抱えます。zfb がサイドバー生成も i18n ルーティングも組み込みの検索フックも出荷しないとき、フレームワーク作者 — または利用者 本人 — がそのコードを書きます。事前生成のエコシステムであれば、それはレシピを探し、読み込み、 プロジェクトの形に適応させることを意味したでしょう。その適応コストは確かに存在しました。 **メリット** は不確実性税の不在です。表面が狭くて安定していれば、「zfb はこれをサポートして いるか?」という問いには素早く答えが出ます。6 つのプリミティブを見ればいい。機能がそれらに マッピングできれば、動きます。できなければ、それはエンジンの中ではなく、zfb の上に乗る フレームワークに属します。監査すべき分厚い API もなく、次のマイナーバージョンで壊れるかも しれないプラグインポイントもなく、あらゆる下流のユースケースに追従し続けるエンジンチームへの 依存もありません。 [Astro](https://astro.build) と [Next.js](https://nextjs.org) は異なるトレードを します。幅広いマルチフレームワークサポート、豊富なプラグインエコシステム、多くのパターン向けの ファーストパーティアダプタを提供します。それは、可能な限り広い層を狙う汎用フレームワークに とっては正しい判断です。zfb の賭けは逆です。より狭く、より速いエンジンで、より小さく安定した コントラクトを持つことが、Cloudflare のエッジをターゲットにしたコンテンツ中心の静的生成 サイトという特定のケースにとって、より良い基盤になる、という賭けです。 エンジン昇格テストがその境界を正確に捉えます。`"if two frameworks built on zfb would do this differently, it's not engine."`(zfb の上に構築された 2 つのフレームワークがこれを 異なるやり方で行うなら、それはエンジンではない)。もし、下流の 2 つのフレームワークが異なる 選択をする正当な理由が少しでもあるなら、その機能は zfb の表面積に属しません。フレームワークに 属します。 ## AI 時代のプラグインよりレシピ LLM があらゆるワークフローの一部になる前は、レシピとプラグインの選択には本物の非対称性が ありました。 **LLM 以前のレシピのコスト。** レシピを見つけることは、検索、Stack Overflow の回答や ブログ記事のスキャン、そのレシピが現在のプロジェクトのバージョンと構造に合うかどうかの判断、 そして手作業での適応を意味しました。それぞれのステップが時間と注意を要しました。同じ問題を 解決するうまくパッケージ化されたプラグインは、手を伸ばすのが純粋に速かったのです。 **LLM がループに入る変化。** LLM が使えると、適応コストは崩れ落ちます。理解して適応するのに 30 分かかっていたレシピが、今やプロンプト 1 つで済みます。さらに重要なのは、レシピが今や *透明* であることです。それは利用者のプロジェクトの中で動き、利用者のインポートを使い、 利用者のコードベースと共に歳を重ね、依存先の内部をのぞき込まずにデバッグできます。対照的に プラグインは、実装をバージョン固定されたパッケージの背後に隠し、プラグイン作者がエンジンに 歩調を合わせ続けることを要求し、LLM が考慮しなければならない間接層を 1 つ増やします。 これはプラグインが時代遅れだという意味ではありません。何かをプラグインに昇格させるための基準が 上がったという意味です。正しい問いはもはや「ここでレシピを見つけて適応させるのは煩わしいか?」 ではありません — それは LLM が処理します。正しい問いは「これは zfb 利用者の 100% が、 逸脱する正当な理由なく、同じやり方で使うものか?」です。 **zfb が今日出荷している狭いプラグイン API** — [Plugins](/concepts/plugins) に文書化された 4 つのライフサイクルフック、仮想モジュール、インポートエイリアス、開発専用の注入ルート — は、 まさにその狭い例外です。`setup` フックの仮想モジュールとエイリアスの登録、`preBuild` / `postBuild` のファイル生成フック、そして `devMiddleware` ハンドラは、ビルドパイプラインと 構造的に切り離せないパターンをカバーします。エンジン自体をフォークしなければ利用者がレシピ として実装できないものたちです。表面はそれ以外では意図的に閉じられています — `addRemarkPlugin` も `addModuleTransform` も `onModuleLoad` もありません — なぜなら、それらのパターンは 安定したエンジン API ではなくレシピに属するからです。 プラグイン昇格テストがその閾値を述べます。`"don't extract until the same recipe has been written by hand in three different zfb consumer projects."`(同じレシピが 3 つの異なる zfb 利用プロジェクトで手書きされるまで抽出するな)。1 つのプロジェクトの利便性はレシピです。 3 つの独立したプロジェクトが同じ解に収束することは、普遍的なニーズの証拠です。その時点で、 そしてその時点でのみ、プラグイン API への抽出はメンテナンスコストに見合います。 ## 次に行く先 - [Engine vs Framework](/concepts/engine-vs-framework) — zfb がコミットする 6 つのプリミティブと、エンジンとフレームワークの関心事の境界線。 - [Plugins](/concepts/plugins) — 狭い例外: 4 つのライフサイクルフックと、それらがカバーするもの・しないもの。 - [Choosing zfb](/concepts/choosing-zfb) — 「意図的に狭く」というトレードオフがプロジェクトにとって正しい判断になるのはいつか、ならないのはいつか。 --- # アーキテクチャ概要 > Source: https://takazudomodular.com/pj/zudo-front-builder/ja/docs/concepts/architecture-overview 2 バンドルモデル、ビルド時パイプライン全体の図、なぜワーカーバンドルが Cloudflare Workers モジュールの形をしているのか、そして esbuild と JS ランタイムを 差し替え可能に保つレイヤリングの原則。ステップごとのビルドパイプラインについては [Build pipeline](/concepts/build-pipeline) を参照してください。 zfb は 2 種類の JavaScript 出力を生成し、それらを 2 つの異なる宛先に送り、そこへ 到達するために 2 つの異なる実行環境を使います。まずこの分割を頭の中で整理しておくと、 残りのアーキテクチャがすっと腑に落ちます。 ## 2 つのバンドル、2 つの宛先 **ワーカーバンドル — ビルド時専用、ユーザーには決して配信されません。** `zfb build` を実行すると、最初に起こるのは esbuild があなたの TSX ページ・レイアウト・ コンポーネントを単一のワーカーバンドルにコンパイルすることです。このバンドルは Cloudflare Workers モジュールの形をしています。`fetch` ハンドラだけをエクスポートし、 それ以外には何もエクスポートしません。 ```ts fetch(request: Request, env: Env, ctx: ExecutionContext): Response { ... } }; ``` Rust オーケストレーターはこのバンドルを組み込み V8 アイソレートにロードし、合成 HTTP リクエスト(ページ URL ごとに 1 リクエスト)で駆動します。各合成リクエストは HTML の `Response` を生成します。オーケストレーターはそれらのレスポンスを `dist/` にプレーンな `.html` ファイルとして書き出します。ビルドが終わるとアイソレートは破棄されます。ビルド時、 ワーカーバンドルがあなたのマシンを離れることは決してありません。これは Rust オーケストレーターが静的出力を生成するために駆動する *ツール* です。(同じバンドルの形は、 `prerender = false` のルート向けに変更なしで Cloudflare Workers にもデプロイされます。 その本番経路についてはこのページの後半と [SSR guide](/guides/ssr-and-cloudflare-bindings) で扱います。) **アイランドバンドル — ブラウザにオンデマンドで配信されます。** `"use client"` でマークされたコンポーネントは別物です。esbuild はそれぞれを個別の ESM モジュールとしてバンドルし、それらのバンドルは HTML ファイルと並んで `dist/` に配置されます。 ブラウザはそれらをオンデマンドでダウンロードし、対応する DOM ノードをハイドレートします。 アイランドバンドルはエンドユーザーに到達する *唯一* の JavaScript です。 この区別が重要なのは、2 つのバンドルがまったく異なるライフタイム、異なる実行環境、異なる 最適化目標を持つからです。両者を混同することが、zfb の動作についての混乱のほとんどの原因です。 `"use client"` でオプトインする方法については [Islands](/concepts/islands) を参照してください。 ## 何がどこで動くのか ```mermaid flowchart TD src["TSX / MDX source files\n(pages/, components/, content/)"] subgraph build["zfb build — your machine"] esbuild_worker["esbuild\n(worker bundle)"] v8["embedded V8\n(synthetic HTTP requests → HTML)"] esbuild_islands["esbuild\n(island bundles)"] dist_html["dist/\nHTML files"] dist_js["dist/\nisland JS files"] end deploy["Static host\n(any CDN / Cloudflare Pages)"] browser["Browser\n(island bundles hydrate on-demand)"] src --> esbuild_worker esbuild_worker --> v8 v8 --> dist_html src --> esbuild_islands esbuild_islands --> dist_js dist_html --> deploy dist_js --> deploy deploy --> browser ``` `zfb build` のボックス内のすべては、ビルド時にあなたのマシン上で動き、ビルドが終わると 終了します。静的デプロイでは、このボックス内のどれもリクエスト時にサーバーで動くことは ありません。 ビルドオーケストレーターがこれらのステップをどう調整するのかという、より踏み込んだ話は [Build engine](/architecture/build-engine) を参照してください。 ## Hono / Cloudflare Workers への移植性という賭け ワーカーバンドルの形 — `export default { fetch }` — は偶然ではありません。これは Cloudflare Workers デプロイが期待するのと同じコントラクトです。つまり、Rust オーケストレーターがビルド時に駆動するバンドルは、変更なしで Cloudflare Workers に デプロイでき、静的プリレンダリングをオプトアウトしたルートに対しては workerd が本番で それを実行します。 ルーティングのコア(`@takazudo/zfb-runtime` の `createPageRouter`)は Hono のアダプタ パターンに従います。ルーターは 1 つ、エントリアダプタは複数。ビルド時アダプタは合成の `Request` オブジェクトを与え、`Response` 文字列を収集します。Cloudflare Workers アダプタはそれをワーカーの `fetch` ハンドラとして登録します。ルーター自身は、どのアダプタが 自分を駆動しているのかを知りません。 これが移植性の賭けです。バンドルの形を安定したコントラクトとして固定することで、zfb は それを実行する JS エンジンから切り離されたままでいられます。ビルド時ホストは組み込み V8 アイソレート、本番ホストは workerd。コントラクト — `export default { fetch }` — は どちらの文脈でも同じです。 `packages/zfb-adapter-cloudflare` は Cloudflare Workers 向けのデプロイアダプタです。 `zfb build --target=cloudflare` が生成したバンドルを受け取り、`wrangler deploy` が 期待する薄いシェルで包みます。2 ファイル構成のワーカー出力と、ランタイムでリクエストが どうディスパッチされるのかを深掘りするには [SSR on a Worker (adapter mode)](/concepts/ssr-on-a-worker) を参照してください。 この設計の完全な根拠は [JS Runtime](/architecture/js-runtime) にあります。組み込み V8 ホストの設計と、なぜバンドルコントラクトがエンジン非依存なのか。SSG ファーストで Hono/Workers のバンドル形を選んだのは、ビルド時と本番の実行環境を互換に保つためであり、 当初のインプロセス `deno_core` アプローチは、パフォーマンスと分離の要件が明確になった 時点で組み込み V8 アイソレートに置き換えられました。 ## レイヤリングのまとめ 3 つのレイヤー、それぞれに明確な役割があります。 | レイヤー | 何を所有するか | 何を所有しないか | |-------|-------------|----------------------| | **あなたのソース** | ページ、コンポーネント、コンテンツ、スタイル | ビルドの仕組み | | **zfb** | ビルドコントラクト — ルートテーブル、ページ props の形、アイランドプロトコル、`dist/` のレイアウト | どのバンドラ、どの JS ランタイムか | | **ツール** | esbuild(バンドル)、組み込み V8(ビルド時評価) | あなたのコードのセマンティクス | zfb はコントラクトを所有します。どんな入力を受け付けるのか、出力がどう見えるのか、ビルドを 通してどんな不変条件が成り立つのか。esbuild と組み込み V8 ランタイムは実装の詳細であり、 ユーザーコードに触れることなく `RenderHost` トレイトの背後で差し替えられます。 バンドラは安くて速いノコギリです。zfb は大工です。 コントラクトの下流: - [Islands](/concepts/islands) — `"use client"` がどのようにコンポーネントをアイランドバンドルにオプトインさせるのか - [Build engine](/architecture/build-engine) — Rust クレートがどのようにパイプラインを調整するのか - [Incremental rebuild](/concepts/incremental-rebuild) — 依存グラフがどのように dev リビルドを高速に保つのか --- # エンジンとフレームワーク > Source: https://takazudomodular.com/pj/zudo-front-builder/ja/docs/concepts/engine-vs-framework このページは英語版のスタブです。完全な翻訳は別の PR で行われます。 英語版: [Engine vs Framework](/concepts/engine-vs-framework) ## 概要 zfb はエンジンであり、フレームワークではありません。プリミティブを提供する低層であり、その上にフレームワーク的な体験を乗せて構築できる、という分離方針について解説します。 英語の最新ドキュメントは [Engine vs Framework](/concepts/engine-vs-framework) を参照してください。 --- # はじめに > Source: https://takazudomodular.com/pj/zudo-front-builder/ja/docs/getting-started はじめにのセクションへようこそ。以下からトピックを選択してください。 --- # フロントマター > Source: https://takazudomodular.com/pj/zudo-front-builder/ja/docs/concepts/frontmatter このページは英語版のスタブです。完全な翻訳は別の PR で行われます。 英語版: [Frontmatter](/concepts/frontmatter) ## 概要 MD/MDX ファイルの YAML フロントマターと、TSX ページの `export const` リテラルを、統一された JSON 表現として扱う zfb のフロントマター契約について解説します。 英語の最新ドキュメントは [Frontmatter](/concepts/frontmatter) を参照してください。 --- # defineConfig > Source: https://takazudomodular.com/pj/zudo-front-builder/ja/docs/api/define-config ## シグネチャ ```ts defineConfig(config: ZfbConfig): ZfbConfig ``` `defineConfig` は `zfb/config` からエクスポートされます。このヘルパーは identity 型で、受け取った引数をそのまま返します。唯一の役割は、エディタに `ZfbConfig` 形状に対する IntelliSense と型チェックを提供することです。実際のスキーマは設定ロード時に Rust の serde によって検証されるため、設定を TypeScript で書いても JSON で書いても同じルールが適用されます。 ## 設定の形状 すべてのキーは camelCase で、`crates/zfb/src/config.rs` を反映しています。 - `outDir?: string` — 出力ディレクトリ。デフォルト: `"dist"`。 - `publicDir?: string` — 静的アセットのディレクトリ。そのままコピーされます。デフォルト: `"public"`。 - `host?: string` — dev / preview サーバーがバインドするホスト。 - `port?: number` — dev / preview サーバーのポート。 - `framework?: "preact" | "react"` — JSX フレームワークのランタイム。デフォルト: `"preact"`。 - `collections?: CollectionDef[]` — コンテンツコレクション。各エントリは `name`(`getCollection` 呼び出しで使う識別子)、`path`(プロジェクトルートからの相対ディレクトリ)、任意の `schema`(設定ロード時に検証され、フロントマターは `zfb check` とビルドによってスキーマと照合されます)を持ちます。 - `tailwind?: { enabled?: boolean }` — Tailwind オプション。 - `plugins?: PluginConfig[]` — ユーザー提供のプラグイン。各エントリは `name`(npm の specifier または `./` 相対パス)と任意の `options`(プラグインのフックに渡される任意の JSON オブジェクト)を持ちます。フックのコントラクト(`setup`、`preBuild`、`postBuild`、`devMiddleware`)の全体、および仮想モジュール・インポートエイリアス・開発専用の注入ルートについては [Plugins](/concepts/plugins) を参照してください。 - `adapter?: string` — デプロイ先アダプタのパッケージ名。純粋な静的ビルドの場合は省略します。`"@takazudo/zfb-adapter-cloudflare"` のようなパッケージは SSR バンドルをデプロイ可能なエントリ(例: Cloudflare Pages 向けの `dist/_worker.js`)にラップします。 - `site?: string` — 正規のオリジン URL(例: `"https://example.com"`)。設定すると `globalThis.__zfb.site` が公開され、レイアウトで正規の `` タグ、OpenGraph メタ、サイトマップの絶対 href、hreflang の代替言語指定を構築できます。絶対 HTTP/HTTPS URL である必要があります。サーバーサイドでの正規 URL 構築が不要なビルドでは省略してください。`base` とは別物です(後述)。 - `base?: string` — アセット URL 向けの公開 URL プレフィックス。サイトがサブパス配下にデプロイされる場合(例: `"/pj/my-site/"`)に使います。`site` とは別物です。`base` はアセット URL の前に付与され、`site` はメタデータで使う完全な正規オリジンです。 - `stripMdExt?: boolean` — MDX コンパイル時に内部リンクの href から `.md` / `.mdx` 拡張子を取り除き、末尾に `/` を付与します。デフォルト: `false`。 - `trailingSlash?: boolean` — base パスの書き換え時に、拡張子なしの絶対 href に末尾の `/` を付与します。デフォルト: `false`。 - `resolveMarkdownLinks?: ResolveMarkdownLinksConfig` — マークダウンリンクリゾルバの設定。`[label](./other.mdx)` のリンクをレンダリング後のルート URL に書き換えるには有効にします。 - `extraWatchPaths?: string[]` — プロジェクト内のソースルートに加えて、dev ウォッチャーが追跡する追加の絶対ファイルシステムパス。[プロジェクトルート外のパスを監視する](#プロジェクトルート外のパスを監視する) を参照してください。 ## プロジェクトルート外のパスを監視する `extraWatchPaths` を使うと、プロジェクトツリー外のファイルが変更されたときに `zfb dev` がライブリロードできます。プロジェクトが兄弟リポジトリからコンテンツを読み込む場合、コードとともにコンテンツを同梱する `file:` 依存、共有ファイルシステムのディレクトリなどで便利です。 ```ts extraWatchPaths: [ "/home/me/knowledge-base", "/srv/shared-content", ], }); ``` セマンティクス: - **絶対パスのみ。** 各エントリは絶対パスである必要があります。相対パスは設定ロード時に `extraWatchPaths[N]: ... must be an absolute path` エラーで拒否されます。dev ウォッチャーは各エントリをプロジェクトルートの外でそのまま登録するため、相対パスを解決するためのアンカーを持ちません。 - **正規化。** 各エントリは `zfb dev` 起動時に一度だけ正規化されます(`Path::canonicalize`)。シンボリックリンクは解決され、以降のイベントは正規形でリビルドロジックに到達します。そのため、ウォッチャーが発するパスは、設定した値に対して `realpath` を実行したときに得られる形と一致します。 - **起動時に存在しない場合。** 設定されたパスが `zfb dev` 起動時点で存在しない場合は、警告とともにスキップされます。ウォッチャーはそのパスが後から出現するかをポーリングしません。dev サーバーがすでに動いている状態でディレクトリを作成した場合は、それを認識させるために `zfb dev` を再起動してください。 - **再帰的。** 各エントリは再帰的に監視されます。起動後に作成されたサブディレクトリも、OS レベルの再帰監視によって自動的に拾われます。 - **リビルドの範囲。** これらのパスからのイベントは依存グラフのカバー範囲外にあります(グラフはツリー内のエッジのみを追跡します)。そのため、ツリー内の同等の編集よりも保守的に広範なリビルドをトリガーします。これは意図的なトレードオフで、ルート外のソースについては精度よりも正しさを優先しています。 **セキュリティ上の注意。** オプトイン専用です。`$HOME` や `/` のような無制限のディレクトリを指定し**ない**でください。Linux では再帰ウォッチャーがすべてのサブディレクトリを登録するため、大きなツリーでは inotify の `max_user_watches` 上限(多くのディストリビューションでデフォルト約 8192)にすぐ到達する可能性があります。広大なソースを監視する必要がある場合は、実際に編集するファイルを含む最も狭いサブツリーを監視してください。 これは dev モードの機能です。プロダクションビルド(`zfb build`)はファイルシステムを一度スナップショットし、ウォッチャーのイベントに依存しないため、`extraWatchPaths` は出力される成果物に影響しません。 ## 例 ```ts // zfb.config.ts — 推奨される形式 outDir: "dist", framework: "preact", collections: [ { name: "blog", path: "content/blog", }, ], tailwind: { enabled: true }, }); ``` ローダーは `zfb.config.ts`(推奨)と `zfb.config.json`(レガシーのフォールバック)を受け付けます。`zfb.config.json` のみが存在する場合は `serde_json` 経由で読み込まれます。`./...`、`../...`、または絶対パスとして宣言されたプラグインパスは設定ファイルからの相対で解決されます(`"@takazudo/some-plugin"` のような npm specifier はどちらの形式でも動作します)。 ```json // zfb.config.json — レガシー形式。現在もサポート { "outDir": "dist", "framework": "preact", "collections": [ { "name": "blog", "path": "content/blog" } ], "tailwind": { "enabled": true } } ``` ## バリデーション ローダーは以下のルールを強制し、JSON のパース失敗についてはファイルパスと `line:column` を添えてエラーを報告します。 - コレクション名は一意である必要があります。 - `path` は絶対パスにできません。 - `path` はプロジェクトルートから抜け出す `..` セグメントを含めることができません。 --- # ビルドエンジン > Source: https://takazudomodular.com/pj/zudo-front-builder/ja/docs/architecture/build-engine このページは zfb のビルドエンジンの *構造* についての解説です。ファイルを保存したときに何が起こるかの手順を追いたい場合は [Build pipeline](/concepts/build-pipeline) を読んでください。このページではなぜこの構造になっているのかを説明します。 関連: [Architecture overview](/concepts/architecture-overview) · [Islands](/concepts/islands) · [Incremental rebuild](/concepts/incremental-rebuild) ## クレートの分割 zfb は Rust のワークスペースです。各クレートはビルドの 1 つの領域を担当し、クレート間を流れるデータは小さく明示的です。 - **`zfb-router`** は `pages/` を走査してファイルをルートに変換します。ファイル名から URL への変換規約だけを担当し、それ以外は何もしません。 - **`zfb-graph`** は依存グラフを保持します。各ページは自分がどのソース(コンポーネント、レイアウト、コンテンツ、スタイル)に依存しているかを把握し、各ソースは自分にどのページが依存しているかを把握します。これがページ単位のリビルドを可能にするインデックスです。 - **`zfb-watcher`** は `notify` クレートをラップし、ノイズの多いネイティブイベントを論理的な保存 1 回につき 1 つの `Change` に正規化して、チャンネルに送出します。 - **`zfb-build`** はオーケストレーターです。`Change` の値を消費して分類し、どのページをリビルドする必要があるかをグラフに問い合わせ、アセットパイプラインを実行します。 - **`zfb-render`** は SWC を通して TSX をコンパイルし、その結果の JS を `RenderHost` に渡して HTML を書き出します。 - **`zfb-css`** は CSS モジュールとグローバルスタイルを処理します。 - **`zfb-islands`** は `client:*` ディレクティブを走査し、アイランドごとの JS をバンドルして、ハイドレーションのエントリを送出します。 - **`zfb-content`** は Markdown / MDX のコンテンツコレクションをパースし、unified のプラグインチェーンを実行します。 - **`zfb-server`** は dev 専用の HTTP サーバーです。ページキャッシュ、静的ファイルのルート、ライブリロードの SSE ストリーム、そして `prerender = false` ページ向けのリクエスト時 SSR ディスパッチャーを備えます。 クレートは下方向に依存します。オーケストレーターは render・css・islands・content を知っています。レンダラーはオーケストレーターを知りません。サーバーは読み取るだけです。 ## バンドル単位ではなく、ページ単位のリビルド バンドラーベースのツール(Vite、esbuild)はモジュールとチャンクで考えます。変更があるとモジュールが無効化され、バンドラーは import グラフをたどって何を再出力するかを決めます。zfb は **ページ** で考えます。 ファイルが変更されると、`zfb-watcher` が `Change` を送出し、オーケストレーターは `zfb-graph` にこのパスに依存しているのはどのページかを問い合わせ、そのページだけが再レンダリングされます。3 つのページから使われているリーフコンポーネントは 3 つのページリビルドを生み出します。サイト全体のリビルドでもなければ、バンドラーのグラフ走査でもありません。 得られるのは粒度です。2,000 ページのサイトでも、共有ヘッダーが変更されたときに影響を受けるページをミリ秒単位でリビルドします。コストはグラフが正直でなければならないことです。`zfb-graph` は成功したレンダリングごとに更新され、次のクエリが最新の状態を参照できるようにします。 ## エンジンが使うツール(とそれらが差し替え可能な理由) zfb はオーケストレーターです。依存グラフ、ページ単位のリビルドのコントラクト、クレート間を流れるデータを所有しています。zfb が呼び出す外部ツールは実装の詳細です。それぞれが Rust のトレイトの背後に位置するため、パイプラインの残りに触れることなくいずれも差し替えられます。 | ツール | 役割 | トレイト境界 | | --- | --- | --- | | **esbuild** | サーバーサイドのワーカーバンドル(`zfb-build/bundler.rs`)とアイランドごとのクライアントバンドル(`zfb-islands`)をバンドルします。高速で、TSX/JSX、MDX ローダー、ツリーシェイキングを扱えます。 | `ClientBundler`(`zfb-islands` 内) | | **deno_core (V8)** | SSG と dev プレビューサーバー向けにサーバーサイドのワーカーバンドルを実行する、組み込みの V8 アイソレートです。Tauri での配布には Node 依存のない単一バイナリが必要なため選ばれました。理由は [JS runtime](/architecture/js-runtime) を参照してください。 | `RenderHost`(`zfb-render` 内) | | **SWC** | TSX のソースを JavaScript にコンパイルしてから esbuild やレンダーホストに渡します。`zfb-render` の内部に存在します。 | `zfb-render` の内部。そのクレートの transform ステップを置き換えることで差し替え可能 | | **lol_html** | ハイドレーションのエントリポイントの `` タグやアイランドのマウントマーカーを、完全なパースなしにレンダリング済みの HTML に注入するために使うストリーミング HTML リライターです。 | `zfb-build/head_inject.rs` の内部で使用。公開トレイトは不要 — 低レベルのユーティリティです | ### レイヤリングの仕組み zfb はオーケストレーション、依存グラフ、ページ単位のコントラクトを所有します。各ツールはちょうど 1 つのレイヤーに登場します。 - **esbuild** は 2 か所で呼ばれます。`zfb-build`(サーバーワーカーバンドル)と `zfb-islands`(アイランドごとのクライアントバンドル)です。どちらも TypeScript/JSX のソースツリーをバンドルするために使い、いずれも上位のクレートには公開しません。 - **deno_core (V8)** は `RenderHost` の背後にある JS エンジンです。`zfb-render` のレンダラーが `RenderHost::call_default` を呼び、具体的なホスト(`EmbeddedV8RenderHost`)がインプロセスの V8 アイソレートでバンドルを実行します。`zfb-build` のオーケストレーターはエンジンの名前を一切口にせず、レンダリング済みの HTML を受け取るだけです。 - **SWC** はモジュールがレンダーホストに到達する前に `zfb-render` の内部で、または esbuild がソースを見る前に `zfb-build/bundler.rs` の内部で実行されます。いずれにせよオーケストレーターからは見えません。 - **lol_html** はレンダーホストが HTML を返した後に `zfb-build/head_inject.rs` の内部で実行されます。オーケストレーターから見れば、入力は素の文字列で、出力も素の文字列です。 この構造により、各ツールのトレイト境界より上のクレートは、ツールが変わっても影響を受けません。`ClientBundler` のコントラクトについては [Islands](/concepts/islands) を、依存グラフがどのようにページ単位のポリシーへ供給されるかについては [Incremental rebuild](/concepts/incremental-rebuild) を参照してください。 ## V8 モードのゲート: `output` と自動検出 `zfb-render` は、デフォルトで有効な `embed_v8` cargo フィーチャーの背後に組み込み V8 ホストを公開します。ビルドエンジンはすべてのビルドの開始時に、デプロイ成果物が V8 を備えたランタイムを構造の一部として前提とするかどうか(`V8Mode` の判定)を決定し、設定とルートテーブルが食い違う場合には明確なエラーを表示します。 この判定は 2 つの入力で駆動されます。 - `zfb.config.ts` の `Config.output`(`"static"` / `"hybrid"` / `"auto"`、デフォルトは `"auto"`)。 - `prerender = false` をエクスポートするルートとして検出された集合。adapter なしでは SSR を許さない事前条件がすでに使っているのと同じデータです。`prerender_map` を 1 回走査し、判定箇所は 1 か所です。 | `output` | SSR ルートの有無 | 結果 | | ---------- | ---------------- | ------------- | | `"static"` | なし | `V8Mode::Off` | | `"static"` | あり | **エラー** | | `"hybrid"` | 任意 | `V8Mode::On` | | `"auto"` | なし | `V8Mode::Off` | | `"auto"` | あり | `V8Mode::On` | このエラーはバンドラーが走る前に発火し、`output` の設定と最初に問題となったルートの両方を示し、それ以上ある場合は残りの数をカウントします。そのため、static を宣言したプロジェクトがコピー & ペーストの結果として SSR ルートをうっかり拾い上げることはありません。ビルドはルートのデプロイ形態を黙って切り替えるのではなく、その矛盾を拒否します。 2 つの手動オーバーライド(`"static"`、`"hybrid"`)とデフォルト(`"auto"`)は、3 つの異なる意図をカバーします。 - `"auto"` — ルートテーブルに判断を任せます。すでに合致しているプロジェクトにとって最良のデフォルトです。 - `"static"` — 意図を先に宣言します。「誰かがページに `prerender = false` を追加する」という失敗モードを静かにではなく大きく顕在化させたい SSG 専用サイトに有用です。 - `"hybrid"` — 逆方向に意図を宣言します。あとから SSR ルートを追加する予定で、それまでビルドのトポロジーを安定させておきたいプロジェクトに有用です。 `V8Mode::Off` が今日実際に行うことは、正直に述べておくべき部分です。出荷される `zfb` バイナリ上では観測的なものにとどまります。ビルドマシンの `zfb` は SSG ページをレンダリングするために常に V8 を起動します — それがパイプラインの動作の仕組みであり、`embed_v8 = off` はすでに `zfb build` でハードエラーになります。このモードは、将来の出荷経路(Tauri サイドカー、スタンドアロン SSR サーバー、`cargo install` によるデプロイ)が同じ判定を再導出せずに読み取れるよう配線されています。今日の負荷を担う、ユーザーから見える役割は `"static"` の事前条件エラーです。 ## アトミックな書き込み `dist/` 内のすべてのファイルは `atomic_write_string`(`zfb-build` の `atomic.rs`)を通して書き込まれます。同じディレクトリ内の兄弟一時ファイルに書き込み、その後あて先に対して `rename` します。`rename` は同一ディスク上のファイルに対して POSIX でアトミックであり、Windows も `MoveFileExW` の replace-existing セマンティクスを通して同じ保証を持ちます。 具体的には次のとおりです。 - ビルドの途中で `dist/index.html` を開いた読み手は、古いバイト列か新しいバイト列のどちらかを見ます。途中まで書かれた状態や空の状態を見ることは決してありません。 - クラッシュしたビルドは孤立した `*.tmp--` ファイルを残しますが、出力を壊すことは決してありません。この命名は意図的です。クラッシュ後に `ls dist/` すると、処理中だったものが分かります。 - dev サーバーは、オーケストレーターが書き換えている最中の `dist/` を配信できます。調整は不要です。 これは退屈な種類の正しさです。機能ではなく、決して破らない不変条件です。 ## ウォッチャーのデバウンスと変更の集約 `zfb-watcher` はデフォルト 50ms のウィンドウでネイティブイベントをデバウンスします。エディタの保存は雑然としています。vim はスワップファイルに書き込んでからリネームし、vscode は複数のメタデータイベントを発し、`git checkout` は一度に数百のイベントを生み出します。デバウンスは各バーストをパスごとに 1 つの `Change` に折りたたみます。 オーケストレーターは 2 段目の処理を行います。tick が発火したとき、パイプラインを呼び出す前にチャンネルにすでにある `Change` をすべて drain します。速い保存のバーストでも、自然な小休止ごとに 1 回のパイプライン実行になります。オーケストレーターはウォッチャーの上にさらなる時間ベースの集約を行いません。ウォッチャーがすでに正しいことをしているからです。速くタイピングしてもビルドがばたつくことはありません。 ## dev サーバーとの関係 dev サーバー(`zfb-server`)は、オーケストレーターの出力の上に乗る薄い読み手です。`PageCache`(URL パスからレンダリング済み HTML へのマップで、レンダリングごとに更新される)、`ReloadEvent` 値の `tokio::sync::broadcast` チャンネル、そしてキャッシュ・`dist/assets/`・`public/`・`/__zfb/reload` の SSE エンドポイントを配信する `axum` ルーターを所有します。 配線箇所は `zfb-server` の `livereload.rs` にある `outcome_to_events` です。noop でないすべての `BuildOutcome` は `ReloadEvent` に変換されてブロードキャストされます。CSS のみの変更は `css` イベントを発し、ブラウザはクライアントの状態を失うことなくスタイルシートを差し替えます。それ以外はすべて `page` イベントを発し、`location.reload()` をトリガーします。 ### `prerender = false` ルート向けのリクエスト時 SSR dev サーバーは `prerender = false` ルートも、ビルド時の SSG を駆動するのと **同じ** 組み込み V8 ホストを通して配信します。スタンプ済みの静的スナップショットからではありません。dev ルーターのリクエストごとの優先順位は次のとおりです。 1. プラグインの dev ミドルウェア(最長プレフィックス一致が勝つ)、 2. `SsrRouteSet` に一致する URL に対する **リクエスト時 SSR**、 3. メモリ上のページキャッシュ(SSG 出力)、 4. ディスク上の `dist/` へのフォールバック、 5. ディスク上の `public/` へのフォールバック、 6. dev の 404。 SSR レイヤーは `zfb-server` の小さな `SsrDispatcher` トレイトを介して配線されます。bin クレート(`crates/zfb/src/commands/dev.rs`)が `EmbeddedV8SsrAdapter` を介して具体的な実装を提供します。これはレンダラーの `Arc>>` へのハンドルをクローンし、`spawn_blocking` タスク上で `EmbeddedV8Host::dispatch_fetch` を通してディスパッチします。V8 アイソレートは専用の OS スレッドにとどまり、アダプターは 2 つ目のスレッドを生成しません。 これが「dev が prod と一致する」を本物の保証にしています。dev プレビューの `prerender = false` 出力は、同じソースから Cloudflare アダプターが生成するものと **意味的に等価**(同じステータス・ボディ・コンテンツタイプ。タイムスタンプとリクエスト ID は異なる場合があります)です。共有された V8 ホストにより、ドリフトする 2 つ目のレンダラーが存在しません。 サーバーは dev 専用です。本番ではエッジ CDN 向けの静的ファイルに加えて、`prerender = false` ルート向けに Cloudflare アダプターからの `_worker.js` を出力します。dev サーバーを安全にしているのと同じアトミック書き込みの保証が、あらゆる本番デプロイを安全にします。 ### ライブラリとしての組み込み: ホストが供給する HTTP ハンドラ Rust のホスト(Tauri デスクトップアプリ、CLI ツール、コンテナ化されたサービス)は、`Server::builder()` API を介して `zfb-server` をインプロセスで実行できます。このビルダーは `with_ssr_handler(pattern, handler)` も公開しており、URL パターンに対してホストが所有する非同期関数を登録します。 登録されたハンドラは、dev ルーターの優先順位チェーンの **プラグインの dev ミドルウェアとランタイム SSR ディスパッチャーの間** に組み込まれます。 1. プラグインの dev ミドルウェア、 2. **ホストが登録した Rust ハンドラ**(新規 — ライブラリ組み込み時のみ)、 3. `SsrRouteSet` に一致する URL に対するリクエスト時 SSR、 4. メモリ上のページキャッシュ(SSG 出力)、 5. ディスク上の `dist/` へのフォールバック、 6. ディスク上の `public/` へのフォールバック、 7. dev の 404。 ホストハンドラは同一パスのランタイム SSR ページより優先されます。それこそがこの継ぎ目の主眼です。ビルダーの形・ハンドラのシグネチャ・コード上の優先順位コントラクトについては [Embed-as-library guide](/guides/embed-as-library) を参照してください。 --- # ルーティング > Source: https://takazudomodular.com/pj/zudo-front-builder/ja/docs/concepts/routing zfb は `pages/` ディレクトリ配下で **ファイルシステムルーティング** を採用しています。ルーターはビルド時(および dev では変更のたびに)`pages/` をスキャンし、各ページのソースファイルをルートへと変換します。この規約は Next.js や Astro で見覚えのあるものと一致します。 ## ファイルからルートへのマッピング | File | Route | Notes | | ---- | ----- | ----- | | `pages/index.tsx` | `/` | | | `pages/about.tsx` | `/about` | | | `pages/about.md` | `/about` | SSG 専用。MDX パイプライン | | `pages/about.html` | `/about` | SSG 専用。静的アセットのコピー | | `pages/blog/index.tsx` | `/blog` | | | `pages/blog/[slug].tsx` | `/blog/:slug`(動的) | | | `pages/docs/[...slug].tsx` | `/docs/:slug*`(catchall) | | | `pages/[lang]/[slug].tsx` | `/:lang/:slug` | | 最初に押さえておきたいルールがいくつかあります。 - `_` で始まるファイル(例: `_app.tsx`)は無視されます。ルートの隣に置きたいが公開はしたくない共有ヘルパーには、このプレフィックスを使ってください。 - ページとして受け付けられる拡張子は `.tsx`・`.md`・`.html` です。`pages/` 内のそれ以外の拡張子のファイルはスキップされます(警告がログに出ます)。そのため README やメモを置いても問題ありません。 - 同じルートに解決される 2 つのファイルがあると、ビルド時に `RouterError::AmbiguousRoute` が発生します。ルーターが暗黙のうちに勝者を選ぶことはありません。 `.md` と `.html` のページエントリの完全なコントラクトと v1 の制限については [Markdown and HTML Pages](/concepts/md-html-pages) を参照してください。 スキャンは `zfb-router` クレートの `Router::scan` が行います。結果は **静的ルートが動的ルートより優先され、動的ルートが catchall より優先される** ようにソートされます。より具体的なルートが先にマッチします。 ## 静的・動的・catchall ルート 静的ルート(`pages/about.tsx`)は単一の具体的な URL にマッチします。動的ルートはファイル名に `[param]` の角括弧を使って単一のパスセグメントをキャプチャし、catchall ルートは `[...param]` を使って末尾の任意個数のセグメントをキャプチャします。 ```tsx // pages/blog/[slug].tsx return Post for {slug}; } ``` ```tsx // pages/docs/[...slug].tsx return {slug.join("/")}; } ``` ## `paths()` エクスポート 動的ルートと catchall ルートは、ビルド時にどの具体的な URL をレンダリングするかを知る必要があります。これは同じファイルから `paths()` 関数をエクスポートすることで実現します。 ```tsx // pages/blog/[slug].tsx const posts = getCollection("blog"); return posts.map((p) => ({ params: { slug: p.slug } })); } return Post {params.slug}; } ``` `paths()` はビルド中に組み込みの V8 ホストによって評価されます。`zfb build` は静的・動的・catchall ルートを検出し、各動的・catchall ルートについて `paths()` を評価して、レンダリングすべき具体的な URL を列挙します。静的ルートに `paths()` エクスポートは不要です。 --- # はじめに > Source: https://takazudomodular.com/pj/zudo-front-builder/ja/docs/getting-started/introduction zfb は Rust で構築された静的サイトエンジンです。すでに Astro や Next.js を知っていて、ミリ秒単位のリビルド、単一バイナリのツールチェイン、オプトイン方式の島モデルを求める人に向いています。 `pages/` ディレクトリツリーを読み解き、TSX コンポーネントを書けるなら、zfb が要求することの 90 パーセントはすでに身についています。 ## zfb が提供するもの 「なぜこの形なのか」(狭いスコープ、プラグインよりレシピ、ミドルウェア層なし)という観点については [Design philosophy](/concepts/design-philosophy) を参照してください。 - **組み込み V8 ワーカー** — ページは Rust に組み込まれた V8 ランタイムを通じてレンダリングされます。ビルド時に別途 Node プロセスを起動する必要はありません。 - **アトミックな書き出し** — ビルドが出力ディレクトリを書きかけの状態で残すことはありません。各 `zfb build` の実行は完全に完了するか、`dist/` を一切変更しないかのいずれかです。 - **ページ単位のインクリメンタルリビルド** — 共有レイアウトを変更すると、それを import しているページだけが対象になり、サイト全体には及びません。大規模サイトでも開発ループのレイテンシは数十ミリ秒に保たれます。 - **オプトイン方式の島** — コンポーネントはデフォルトでサーバーレンダリングされます。コンポーネントファイルに `"use client"` を追加すると、クライアントサイドのハイドレーションをオプトインできます。島を持たないページは JavaScript をまったく出力しません。 - **フロントマターとコンテンツコレクション** — 名前付きディレクトリに置いた Markdown・MDX・TypeScript データファイルは型付きのコレクションになり、ページから `getCollection()` でクエリできます。 - **動的ルート** — `pages/blog/[slug].tsx` は `paths()` 関数をエクスポートします。zfb はビルド時にこれを評価し、ルートを slug ごとに 1 つの HTML ファイルへ展開します。 - **MDX コンポーネント** — 任意の TSX コンポーネントを `.mdx` コンテンツに import し、JSX として使えます。 - **HTML 以外のページ** — ページファイルは `contentType` をエクスポートすることで、HTML の代わりに JSON・XML・RSS など任意のテキスト形式を生成できます。 - **コンテンツクエリのブリッジ** — `getCollection()` はページコンポーネントだけでなく、MDX ファイル内や `paths()` 内でも利用できます。 - **単一バイナリの配布** — zfb は 1 つの Rust バイナリとして提供されます。フレームワーク自体に `node_modules` は不要で、監査すべきプラグインレジストリもありません。 ## Astro から見て馴染みのある点 | Astro のコンセプト | zfb での対応 | | --- | --- | | `src/pages/` のファイルシステムルーティング | `pages/` ディレクトリ、同じ命名規約 | | フロントマターフェンスを持つ `.astro` コンポーネント | `getStaticProps` をエクスポートする TSX ページファイル | | 型付きスキーマを持つ Content Collections | `getCollection()` + `zfb.config.ts` で宣言するスキーマ | | レイアウト内の `` | React の `children` prop — 素の TSX 合成 | | `client:load` ディレクティブ | コンポーネントファイル先頭の `"use client"` ディレクティブ | | 静的アセット用の `public/` | `public/` — 同じセマンティクス | | `astro.config.mjs` | `zfb.config.ts`(TypeScript、直接ロード) | 異なる点: zfb には独自のコンポーネント構文がなく、すべてが TSX です。学ぶべき `.astro` ファイル形式はありません。トレードオフとして Astro のコンポーネント単位のフェンス構文は失われますが、ページ・レイアウト・コンポーネントを通じて一貫した記述面が得られます。 ## Next.js から見て馴染みのある点 | Next.js のコンセプト | zfb での対応 | | --- | --- | | `pages/` ディレクトリルーティング(Pages Router) | `pages/` — 同じファイル命名とネストのルール | | `getStaticProps` / `getStaticPaths` | `getStaticProps` + `paths()` エクスポート、同じ 2 関数パターン | | 動的ルート `[slug].tsx` | `[slug].tsx` — 同一の構文 | | キャッチオールルート `[...slug].tsx` | `[...slug].tsx` — 同一の構文 | | 静的アセット用の `public/` | `public/` — 同じセマンティクス | | ページをラップするレイアウトコンポーネント | ページをラップするレイアウトコンポーネント — 素の TSX 合成 | 異なる点: zfb は **デフォルトで SSG** です。すべてのページはビルド時に計算され、静的 HTML になります。本当にリクエスト時の挙動を必要とするルートは `prerender = false` でオプトアウトし、設定済みのアダプター(例: [`@takazudo/zfb-adapter-cloudflare`](/guides/ssr-and-cloudflare-bindings) 経由の Cloudflare Pages)が処理します。zfb が目指していないのは、フル機能のアプリフレームワークになることです。App Router も、React Server Components も、ミドルウェア層も、ページ単位の自動 ISR もありません。ランタイムの位置づけは「SSG にアダプター形状の脱出口を加えたもの」であって、「`output: export` だけの Next.js」ではありません。 ## クライアントルーターと View Transitions `@takazudo/zfb-runtime` パッケージは `` コンポーネントを提供します。これは同一オリジンへのナビゲーションをインターセプトし、フルリロードなしでページコンテンツを差し替えます。オプションで View Transitions API もサポートします。ルートレイアウトにマウントしてオプトインしてください。マウントしないページは通常のリンク挙動になります。 ## マイグレーションガイド - [Migrating from Astro](/guides/migrating-from-astro) — 既存の Astro 静的サイトを zfb へ移行するための、コンセプトごとの対応表。 ## 次に読むもの [Installation](/getting-started/installation) では CLI のビルドとインストールを扱います。 [Your first site](/getting-started/your-first-site) では、プロジェクトのスキャフォールドからビルドまでを一通り解説します。 --- # Astro からの移行 > Source: https://takazudomodular.com/pj/zudo-front-builder/ja/docs/guides/migrating-from-astro このページは英語版のスタブです。完全な翻訳は別の PR で行われます。 英語版: [Migrating from Astro](/guides/migrating-from-astro) ## 概要 Astro サイトをお持ちの方は、zfb の多くの部分に馴染みを感じるでしょう。どちらもファイルベースのルーティング、コンテンツコレクション、パーシャルハイドレーションを提供しています。主な違いはスコープにあります。zfb はより小さく、よりエンド・ツー・エンドで Rust を基盤としています。 英語の最新ドキュメントは [Migrating from Astro](/guides/migrating-from-astro) を参照してください。 ## エンジンの追加機能(v0 以降) 以下の機能は初期リリース後に追加されたものです。Astro で同等のパターンを使用していた場合、zfb での対応箇所を示します。 **サイトの正規 URL(`site`)** Astro の `site` 設定キーは、zfb のトップレベル `site` オプションに直接対応します: ```ts // zfb.config.ts site: "https://example.com", }); ``` **プラグインライフサイクル: `setup`、`addAlias`、`addVirtualModule`、`injectRoute`** Astro インテグレーションの `astro:config:setup` フックに相当するのが、zfb プラグインの `setup` フックです。`addAlias` でインポートエイリアスを登録し、`addVirtualModule` で仮想 ESM モジュールを提供し、`injectRoute` で開発サーバー専用のルートを追加できます。詳細は [Plugins](/concepts/plugins) を参照してください。 **`postBuild` でのルートマニフェスト(`ctx.routes`)** `postBuild` フックは `ctx.routes` を受け取ります。これはビルドが出力したすべての URL の完全なマニフェストです。サイトマップや RSS フィードの生成に利用できます。詳細は [Plugins](/concepts/plugins) を参照してください。 **Markdown: 目次(TOC)、外部リンク、CJK フレンドリーな強調** Astro でよく使われる `remark-toc` や `rehype-external-links` に相当する機能は、zfb では `markdown` 設定ブロックの組み込みオプションとして提供されています: ```ts // zfb.config.ts markdown: { toc: { heading: "TOC", maxDepth: 2 }, externalLinks: { target: "_blank", rel: ["noopener", "noreferrer"] }, cjkFriendly: true, // デフォルトで有効 }, }); ``` 詳細は [Customizing Markdown](/guides/customizing-markdown) を参照してください。 **シンタックスハイライトのカスタムテーマ(`codeHighlight.themesDir`)** zfb は syntect(Sublime Text 互換の `.tmTheme` 形式)を使用しています。`codeHighlight.themesDir` で `.tmTheme` ファイルを含むディレクトリを指定します: ```ts codeHighlight: { themesDir: "./themes", theme: "Dracula", }, }); ``` 詳細は [Syntax Highlighting](/guides/syntax-highlighting) を参照してください。 ## ユーザースペースに移行するパターン zfb は Astro の一部の機能を意図的に省略しています。これらのパターンは、純粋な JSX/Preact モデルにおいて、エンジンサポートなしにクリーンなユーザーランドの解決策があるためです。 ### `client:media` — メディアクエリによる条件付きハイドレーション Astro の `client:media="(max-width: 768px)"` に相当するパターンは、コンポーネント内部での早期リターンです: ```tsx "use client"; interface Props { children: preact.ComponentChildren; } /** クエリにマッチするビューポートでのみ children をレンダリングする */ const [matches, setMatches] = useState(false); useEffect(() => { const mq = window.matchMedia("(max-width: 768px)"); setMatches(mq.matches); const handler = (e: MediaQueryListEvent) => setMatches(e.matches); mq.addEventListener("change", handler); return () => mq.removeEventListener("change", handler); }, []); if (!matches) return null; return <>{children}; } ``` ### ページごとの `` 追加(Astro の ``) Astro の `` に相当する機能はエンジンレベルでは提供されていません。 `PageMeta` は現在 unknown フィールドを拒否します。将来の ADR でこの制約が緩和された場合、このレシピはよりシンプルになります。 ユーザーランドのパターンは **Preact コンテキストベースのヘルメット** です。レイアウトがページボディを先にレンダリングしてコレクターに登録し、その後収集されたヘッドノードを出力します。詳細なスニペットは英語版 [Migrating from Astro](/guides/migrating-from-astro#per-page-head-additions-astros-fragment-slothead) を参照してください。 ### Preact-compat エイリアス(`@/components/svg`、`@/components/responsive-image`) Astro のコンポーネントモデルの回避策として登録されていたエイリアスは、移行時に**削除してください**。zfb ではすべてのコンポーネントが最初から `.tsx` であるため、これらのブリッジレイヤーは不要です。コンポーネントを直接ファイルパスで(またはシンプルなエイリアスで)インポートしてください。 ### `data-island` マーカーによるカスタムハイドレーション このパターンは zfb に**バイト単位でそのまま**移行できます。`"use client"` アイランドとして自前の `` を用意し、マーカーを `querySelectorAll` して Preact の `hydrate()` を呼び出します。詳細は英語版 [Migrating from Astro](/guides/migrating-from-astro#custom-hydration-with-data-island-markers) を参照してください。 ### カスタム remark / rehype プラグイン zfb の Markdown パイプラインは Rust バックエンドであり、npm レベルの remark/rehype プラグインローダーはありません。Markdown レベルのカスタマイズは in-tree の Rust visitor として実装します。一般的な組み込みプラグイン(TOC、外部リンク、CJK 修正)はすでに `zfb.config.ts` のオプションとして提供されています。より固有のカスタマイズが必要な場合は [Extending the Markdown Pipeline](/guides/extending-the-markdown-pipeline) を参照してください。 --- # Node なしでインストール > Source: https://takazudomodular.com/pj/zudo-front-builder/ja/docs/install/node-free zfb は自己完結型の Rust バイナリとして配布されます。**インストールにも実行にも Node.js は不要です** — ビルドエンジン・esbuild・Tailwind はすべてバイナリに コンパイル済み、あるいは Go バイナリとして同梱されています。Node が必要になるのは 一部の機能をオプトインで使う場合だけです(下記の「Node が依然として必要なケース」を 参照)。 Node なしの経路でもコンテンツサイト向けの全機能が利用できます。Markdown・MDX・ HTML ページ、**さらに islands** まで対応します — esbuild と Tailwind は zfb バイナリ内に 組み込まれた Go バイナリとして動作するため、クライアントサイドのハイドレーションも Node を一切介さずに動きます。 ## インストール経路 ### Linux / macOS — curl ```sh curl -fsSL https://raw.githubusercontent.com/Takazudo/zudo-front-builder/main/install.sh | sh ``` `zfb` は `$HOME/.local/bin/zfb` にインストールされます。このディレクトリがまだ `PATH` に含まれていない場合は追加してください: ```sh ``` **環境変数:** | 変数 | デフォルト | 用途 | | --- | --- | --- | | `ZFB_INSTALL` | `$HOME/.local` | インストール先プレフィックス。バイナリは `$ZFB_INSTALL/bin/zfb` に配置されます。 | | `ZFB_VERSION` | (最新の安定版) | リリースを固定します。特定のタグには `v0.X.Y`、プレリリースを使う場合は `latest-prerelease` を指定します。 | ### macOS — Homebrew ```sh brew install Takazudo/tap/zfb ``` tap は [Takazudo/homebrew-tap](https://github.com/Takazudo/homebrew-tap) でホストされています。formula は **安定版**リリースごとに `scripts/update-homebrew-formula.sh` によって更新されます。 npm の `latest` タグと同様に、Homebrew の formula は**安定版**リリースのみを 追跡します — バージョン固定やプレリリースのオプトインの仕組みはありません。特定 バージョンやプレリリースをインストールしたい場合は、代わりに curl インストーラで `ZFB_VERSION`(例:`ZFB_VERSION=latest-prerelease`)を指定してください。 現在の 1.0 未満の期間は、最初の安定版 `vX.Y.Z` がリリースされるまで、tap が最新の プレリリースを早期アクセス版として提供する場合があります。 後から新しいバージョンへ更新するには: ```sh brew upgrade zfb ``` ### Windows — PowerShell ```powershell irm https://raw.githubusercontent.com/Takazudo/zudo-front-builder/main/install.ps1 | iex ``` `zfb.exe` は `%LOCALAPPDATA%\zfb\bin\zfb.exe` にインストールされます。 **環境変数:** | 変数 | デフォルト | 用途 | | --- | --- | --- | | `ZFB_INSTALL` | `%LOCALAPPDATA%\zfb` | インストールルート。バイナリは `$ZFB_INSTALL\bin\zfb.exe` に配置されます。 | | `ZFB_VERSION` | (最新の安定版) | リリースタグ(例:`v0.2.0`)または `latest-prerelease` を固定します。 | バイナリを現在のセッションの `PATH` に追加するには: ```powershell $env:PATH = "$env:LOCALAPPDATA\zfb\bin;$env:PATH" ``` 恒久的に追加する場合(実行後は新しいターミナルを開いてください): ```powershell [System.Environment]::SetEnvironmentVariable( 'PATH', "$env:LOCALAPPDATA\zfb\bin;" + [System.Environment]::GetEnvironmentVariable('PATH', 'User'), 'User' ) ``` ### Windows — Scoop Scoop バケット(`Takazudo/scoop-bucket`)は計画中ですが、バケットのリポジトリは まだ作成されていません。それまでは上記の PowerShell スクリプトを利用してください。 ## インストールの確認 ```sh zfb --version ``` ## 設定ファイル:`zfb.config.json` と `zfb.config.ts` どちらの設定フォーマットも Node なしで動作します。`zfb.config.ts` は、デフォルトの `zfb` バイナリに同梱された組み込み V8 アイソレートによってインプロセスで評価されます — 実行時の Node 依存はありません。 `zfb.config.ts` は**データ設定**です — `node:*` のインポート、`process.env`、その他 Node 専用 API は評価器内では利用できません。TypeScript の型・コメント・計算値が 欲しい場合は `zfb.config.ts` を、プレーンな JSON で済ませたい場合は `zfb.config.json` を使ってください。 ```jsonc // zfb.config.json — こちらも Node なしで動作します { "framework": "preact", "outDir": "dist", "collections": { "blog": { "dir": "content/blog", "schema": { "type": "object", "properties": { "title": { "type": "string" } } } } } } ``` `zfb.config.ts` と `zfb.config.json` の両方が存在する場合は `.ts` ファイルが 優先されます。 ## Node が依然として必要なケース 以下の機能は `PATH` 上に Node が必要です: | 機能 | 理由 | | --- | --- | | `--skip-tsc` なしの `zfb check` | TypeScript の型チェックは Node パッケージである `tsc` に委譲されます。TS 以外のチェックだけを実行するには `--skip-tsc` を指定してください。 | | Cloudflare アダプタ(`@takazudo/zfb-adapter-cloudflare`) | アダプタは Node CLI である Wrangler に依存します。Cloudflare Pages への SSR デプロイでは、デプロイ環境に常に Node が必要です。 | それ以外 — ビルド、開発サーバー、islands、Tailwind、MDX、コンテンツコレクション、 `zfb.config.ts` の評価 — はすべて Node なしで動作します。 ## zfb のアップグレード その場でアップグレードする `zfb upgrade` コマンドは将来のリリースで計画されています。 現時点では、最初に使ったインストールコマンドを再実行してください — 既存のバイナリを 最新バージョンで上書きします。 --- # レシピ > Source: https://takazudomodular.com/pj/zudo-front-builder/ja/docs/recipes このセクションはプレースホルダーです。レシピは **独立した解説記事** として 1 つずつ追加されていきます。1 ページ 1 トピックで、そのパターンの元になったソースプロジェクトを知らない人にも分かるように書かれます。 まだ何も公開されていません。あらためてご確認ください。 ## このセクションで扱うもの zfb のプリミティブ(`addVirtualModule`、`postBuild` プラグイン、`ctx.routes`、非 HTML ページ、MDX コンポーネントなど)を組み合わせて具体的な解決策にまとめる、メンテナーがキュレーションした小さく焦点を絞ったパターン集です。あなた自身のケースに応用できます。 各レシピは次のようなものになります。 - 1 トピックにつき 1 記事 - コードの羅列ではなく解説として書かれる — _なぜ_ そうするのかは _どうやって_ と同じくらい重要です - 特定の zfb バージョン範囲に紐づく — どのエンジンの状態を前提としているかが分かります ## このセクションで扱わないもの - 検証されていないスニペットの寄せ集め - コミュニティ投稿のプール(キュレーションが重要です — その規律の根拠は [#348](https://github.com/Takazudo/zudo-front-builder/issues/348) を参照) - [Concepts](/concepts) セクションの代替(同セクションは zfb のプリミティブそのものを解説します) 最初のレシピが追加されると、このインデックスは本物のカタログエントリに変わります。それまでは、このページは枠を確保しているだけです。 --- # 動的ルート > Source: https://takazudomodular.com/pj/zudo-front-builder/ja/docs/concepts/dynamic-routes このページは英語版のスタブです。完全な翻訳は別の PR で行われます。 英語版: [Dynamic Routes](/concepts/dynamic-routes) ## 概要 `[slug].tsx` のような動的ルートで `paths()` 関数が返す形、および `params` と `props` をページコンポーネントに渡すしくみについて解説します。 英語の最新ドキュメントは [Dynamic Routes](/concepts/dynamic-routes) を参照してください。 --- # Markdown と HTML ページ > Source: https://takazudomodular.com/pj/zudo-front-builder/ja/docs/concepts/md-html-pages このページは英語版のスタブです。完全な翻訳は別の PR で行われます。 英語版: [Markdown and HTML Pages](/concepts/md-html-pages) ## 概要 `pages/` ディレクトリに `.md` や `.html` ファイルを置くことで、`.tsx` と同じルーティング規約を使ってページを作成できます。 それぞれのレンダリングパスと v1 の制限については英語ドキュメントを参照してください。 英語の最新ドキュメントは [Markdown and HTML Pages](/concepts/md-html-pages) を参照してください。 --- # getCollection > Source: https://takazudomodular.com/pj/zudo-front-builder/ja/docs/api/get-collection ## シグネチャ ```ts getCollection>(name: string): CollectionEntry[] ``` ページモジュールから `getCollection` を呼び出すと、名前付きコレクション内のすべてのエントリを読み込めます。コレクションは `zfb.config` の `collections` で宣言されている必要があります。この関数は同期的で、Promise ではなくプレーンな配列を返します。スキーマ検証は各エントリの読み込み時に実行されるため、配列がコンポーネントに届く頃にはフロントマターは型付けされ検証済みになっています。 ## CollectionEntry の形状 各エントリは以下のフィールドを持ちます。 - `slug: string` — `.md` 拡張子を除いたファイル名で、スラッシュ区切りのパスに正規化されます。ネストしたファイルはパスベースの slug を生成します(例: `"2024/hello"`)。 - `data: T` — パース済みのフロントマター。ジェネリックパラメータで型付けされます。 - `body: string` — フロントマターを除いた生のマークダウン本文。 - `module_specifier: string` — `mdx:///` 形式の安定したブリッジキー。レンダラが内部的に使用します。カスタムブリッジを構築する場合は `Content` のルックアップに渡します。 - `Content: (props: ContentProps) => ContentElement` — このエントリのレンダリング可能なコンポーネント。利用可能な場合はレンダラブリッジ経由でレンダリングし、ブリッジが存在しないテストや dev のコンテキストでは `` ブロックにフォールバックします。 ## ContentProps ```ts type ContentProps = { components?: Record; }; ``` `components` プロップは Astro の `` の慣習を踏襲しており、要素名からオーバーライドするコンポーネントへのフラットなマップです。ベースとして `"@takazudo/zfb"` の `defaultComponents` を使ってください。 ```tsx ``` ## 例 典型的なユースケースは、インデックスページでブログ記事の一覧をレンダリングすることです。 ```tsx const posts = getCollection<{ title: string; date: string }>("blog"); return ( {posts.map((post) => ( {post.data.title} ))} ); } ``` `getCollection` は同期的に返るため、`await` は不要です。エントリごとの動的ページには `getCollection` と `paths()` エクスポートを組み合わせます。関連するパターンについては [paginate](/api/paginate) を参照してください。 --- # JS ランタイム > Source: https://takazudomodular.com/pj/zudo-front-builder/ja/docs/architecture/js-runtime zfb は組み込み V8 アイソレートを介してサーバーサイドで TSX をレンダリングします。これは `@takazudo/zfb-runtime` からビルドされた Hono スタイルのワーカーバンドルを実行する、Rust がホストする V8 エンジンです。 ## ランタイムの選択に至った経緯 V8 を組み込むという決定は、利用可能な JS ホストの計測的な評価を経て到達しました。初期のイテレーションはインプロセスの `deno_core` アイソレートを使い(`deno_core`・`ssr_rs`・`rquickjs` の 3 候補をスパイクした上で承認)、その後 Cloudflare Workers の本番ターゲットとランタイムの一致を得るために、Node がホストする miniflare ワーカーへ移行しました。Tauri での配布が最終的にその選択を覆しました。zfb をデスクトップアプリとして出荷するには、エンドユーザーのマシンに Node.js 依存のない単一バイナリが必要であり、miniflare は Node のパッケージだからです。バイナリサイズと初回ビルド時間のコストを受け入れた上で、`deno_core` を介した V8 組み込みが再採用されました。Node なしの単一バイナリ制約を満たす唯一の経路だからです。 この間ずっと安定した本番のコントラクトは _バンドルの形_(`export default { fetch }`)であり、ビルド時のエンジンではありませんでした。zfb がビルド時にレンダリングするのと同じバンドルが、ランタイム SSR 向けに Cloudflare Workers へ変更なしでデプロイされます。 ## そもそもなぜ JS エンジンが必要なのか — そしてなぜ特に V8 なのか zfb が JS エンジンを使うのは TSX が JS エンジンを必要とするからではなく、コンパイルされた出力を *評価する* ことがそれを必要とするからです。この区別は重要です。 ### レイヤー 1 — JSX は単なる構文 SWC、oxc、esbuild はいずれも JSX を `h(...)` 呼び出しにコンパイルします。そのどれも JS エンジンを必要としません。TSX のパースと変換は純粋な構文操作です。だから「TSX をサポートする」ことは論理的に「V8 が必要だ」を意味しません。問題は変換の後に何が起こるかです。 ### レイヤー 2 — コンパイルされた JS はやはり実行が必要 JSX が `h(...)` になったら、何かがそのコードを *評価* しなければなりません。呼び出しツリーをたどって VDOM を生成し、次に `renderToString` を呼んで HTML を生成します。評価はどこかに JS エンジンを必要としますが、必ずしも V8 である必要はありません。軽量な代替が 2 つ存在します。 - **Boa**(純粋な Rust、約 30 秒の増分ビルド) — C++ ツールチェーン不要で完全に Rust で書かれた JS エンジン。ECMA 仕様を追っていますが、新しめの機能のカバレッジでは V8 に後れを取ります。 - **rquickjs**(QuickJS への Rust バインディング、バイナリへの寄与は約 210 KB) — コンパイルが速く、フットプリントが最小です。 どちらもシンプルで自己完結した Preact コンポーネントの SSR には十分機能します。問題は「どんな frontend 開発者の TSX でも、彼らが import したあらゆる npm パッケージとともに」です。ユーザーがカレンダーライブラリ、i18n ヘルパー、あるいは Proxy ベースの状態ストアを使うコンポーネントを追加した瞬間、Boa の ECMA の穴と QuickJS の限られた ES2022+ の表面は、静かな誤出力や手痛いパニックを生み始めます。これらは診断が極めて難しい失敗モードです。任意のエコシステムコードに対しては V8 が安全な賭けです。Node.js と Chromium が動作対象とするのと同じエンジンだからです。 両方の軽量エンジンの経路についてはリサーチが進行中です。[#344 — 本番ランタイムでのフィーチャーゲート付き V8](https://github.com/Takazudo/zudo-front-builder/issues/344) と [#345 — SSG 専用 JS エンジンとしての Boa / QuickJS](https://github.com/Takazudo/zudo-front-builder/issues/345) を参照してください。 ### レイヤー 3 — Rust のテンプレーティングは JS を丸ごとコンパイルで消す Leptos と Yew は JSX に似た構文(RSX)を受け取り、それをビルド時に Rust にコンパイルします。ランタイムにもビルド時にも JS エンジンは不要です。落とし穴は、彼らが借りたのは構文であって言語ではないことです。任意の npm パッケージのための `import` はなく、ランタイム値を捕捉する JS クロージャもなく、GitHub からそのまま使える Preact コンポーネントもありません。Leptos のプロジェクトは Rust のプロジェクトです。Rust を書き、Rust のクレートに依存します。これはコアな対象読者への約束を破ります。frontend 開発者の既存の TSX と npm の知識が転用できなくなるからです。 ### 対象読者のトレードオフ zfb の対象読者は、すでに TSX を知っている frontend 開発者です。V8 はその読者への入場料であり、「あなたの既存のコンポーネントがそのまま動く」をベストエフォートの近似ではなく真の言明にしているものです。この読者優先の枠組みについては [design philosophy](/concepts/design-philosophy) のセクションで詳しく説明します。 純粋な Rust の SSG(Boa/QuickJS)と、ランタイムでの V8 なし(ビルド時のみの V8)は、現実的な将来の方向性です — [#344](https://github.com/Takazudo/zudo-front-builder/issues/344) と [#345](https://github.com/Takazudo/zudo-front-builder/issues/345) を参照してください — が、これらはデフォルトの上の最適化であって、デフォルトそのものではありません。 ## 今日動いているもの `zfb build` / `zfb dev` / `zfb preview` では、Rust のオーケストレーターが `deno_core` を介してインプロセスの V8 アイソレートを生成します。それは `@takazudo/zfb-runtime` が esbuild でビルドする workerd 形のバンドルを読み込みます。ルートごとの props は JSON として渡され、アイソレートは合成された `Request` でバンドルの `fetch` エントリを呼び、`Response`(HTML)を集めて返します。オーケストレーターは素の HTML を `dist/` に書き込みます。サブプロセスは生成されず、Node.js も不要です。 同じバンドル(`export default { fetch }`)は、`prerender = false` ルートのランタイム SSR 向けに Cloudflare Workers へ変更なしでデプロイされます。workerd がリクエスト時にそれを実行します。本番の経路は、どのビルド時エンジンがバンドルを生成したかを関知しません。 ## 必要性 zfb のレンダラーはページの TSX を SWC を通してコンパイルし、その結果の ESM を JS ホストに渡し、モジュールの `default` エクスポートを呼び、返された HTML をディスクに書き込みます。ホストは次を満たさなければなりません。 - ページが共有コンポーネントを自然に import できるよう、ESM(`import` / `export`)をサポートすること。 - ページがモジュールスコープで `await fetch(...)` や `await loadCollection(...)` できるよう、トップレベル await をサポートすること。 - スローされたエラーを **ソースに忠実な** 位置で表面化すること — スタックトレースの行が、ラップされたスクリプト内のオフセットではなく、ユーザーの TSX ファイル内の行に一致しなければなりません。 - 数百ページのビルドにわたって同じモジュールを繰り返し評価しても、メモリをリークしないこと。 ## ランタイム候補の評価 スパイク用クレート(`crates/zfb-runtime-spike/`)は、重い V8 ビルドをオプトインにするため cargo フィーチャーでゲートした上で、同じ `RenderHost` トレイトを 3 つの候補に対して実装しました。次の表は歴史的な文脈です — この評価がランタイムの選択を裏付けました。 ### 候補 - **`deno_core`** — Deno の再利用可能なコアでラップされた V8。ES モジュールローダー、トップレベル await のためのイベントループ、ソースマップ付きエラーをすぐに備えています。**現在の選択。** - **`ssr_rs`** — Preact / React の SSR に特化した薄い V8 ラッパー。スコープが狭く、単一バンドルのエントリポイントモデルです。 - **`rquickjs`** — QuickJS をラップする Rust バインディング。フットプリントが小さく、コンパイルが速く、シングルスレッドです。 もう 2 つは計測的なスパイクなしに却下されました。`rusty_v8` は `deno_core` を通して得られるのと同じ V8 を、ただより低レベルで扱うものです。これを選ぶことは、`deno_core` がすでに与えてくれるローダーとアイソレートの配管を再実装することを意味します。`boa` は純粋な Rust の ESM 実装です。ECMA への準拠とソースマップの忠実度が V8 に対して十分に劣るため、実世界の Preact / React の SSR は未対応の隅に当たります。 ### トレードオフのマトリクス スパイクのベンチハーネスは 5 つの代表的なシナリオ(静的ページ、動的ルート、コンテンツコレクション、`"use client"`、トップレベル await)を読み込み、コールドスタート、ウォームの平均、ウォームの p95、定常状態の RSS を計測しました。計測実行からの主要な数値は次のとおりです。 | 軸 | `deno_core` | `ssr_rs` | `rquickjs` | | --------------- | ----------- | --------- | ------------------ | | ウォームレンダー | 16us | 1.37ms | 106us | | コールドスタート | 181us | 5.88ms | 572us | | 定常状態の RSS | 19MB | 317MB | 4.5MB | | ESM | native | bundle | partial | | トップレベル await | native | bundle | not supported sync | | ソースマップ | native | partial | offsets only | | ビルドコスト | 約 3 分 | 同程度 | 約 30 秒 | V8 クラスのエンジンは正しさの軸で勝ち、QuickJS はフットプリントとビルドコストで勝ちます。`ssr_rs` のデフォルトのレンダリングごとのアイソレート生成は他のすべてを支配し、負荷がかかるとメモリを蓄積します。 ## zfb が課す制約 2 つのプロジェクトレベルの不変条件が選択を形作ります。 **レンダースレッドごとに単一のアイソレート。** V8 のアイソレートは 1 つのスレッドにピン留めされます。レンダラーはホストを `!Send` として扱い、専用のスレッドで実行し、作業をチャンネル越しに交換します。 **ランタイムはバイナリによって固定され、設定では決まらない。** `zfb.config.ts` は自前の JS ランタイムを選べません。ランタイムは、あなたがインストールした `zfb` バイナリがコンパイルされたときのものです。これにより zfb とユーザーコードの間のコントラクトが単一値に保たれます — 1 つのバイナリでビルドされたすべてのプロジェクトが同じランタイムを使います。ブートストラップのルールは `crates/zfb/src/config.rs` にあります。 ## 境界 ホストより上のすべては、`crates/zfb-render/src/render_host.rs` の `RenderHost` トレイトに対して書かれています。 ```rust pub trait RenderHost { fn execute_module(&mut self, name: &str, source: &str) -> Result; fn call_default(&mut self, handle: &ModuleHandle, props: JsonValue) -> Result; fn get_export(&mut self, handle: &ModuleHandle, name: &str) -> Result; } ``` これがレンダラーが使うインターフェースの全体です。`zfb-render`、`zfb-build`、そしてフレームワークアダプターは、具体的なランタイムの名前を一切口にしません。呼び出し箇所に触れることなく実装を差し替えられます — 新しいホストを追加し、ベンチを再実行し、アダプターの結合テストを走らせてバイト単位で同一の HTML を確認します。 --- # Content Collections > Source: https://takazudomodular.com/pj/zudo-front-builder/ja/docs/concepts/content-collections このページは内容更新が予定されています。最新の英語版は [Content Collections](/concepts/content-collections) を参照してください。 A **content collection** is a directory of Markdown (or MDX) files declared in your project config. zfb scans the directory at build time, parses each file's frontmatter against a schema you supply, and exposes the entries through a `getCollection()` helper your pages can call. ## Declaring a collection Collections are configured in `zfb.config.ts` (or `zfb.config.json`) under the `collections` key. Each entry has a `name` and a `path`: ```ts collections: [ { name: "blog", path: "content/blog", }, ], }; ``` The `name` is the identifier you pass to `getCollection()`. The `path` is the directory (relative to the project root) holding the entries. zfb walks that directory, treats each file as a Markdown document with frontmatter, and exposes its entries through `getCollection("blog")`. You can additionally supply an optional `schema` field — a JSON Schema subset that validates each entry's frontmatter at build time. The supported keywords (`type`, `properties`, `items`, `required`) are documented on the [`defineConfig`](/api/define-config) page. The `[{ name, path }]` form remains supported for projects that don't need per-field validation. ## Loading entries from a page Pages use `getCollection()` to enumerate entries: ```tsx const posts = getCollection("blog"); return ( {posts.map((post) => ( {post.data.title} ))} ); } ``` Each entry has three things you can rely on: - `data` — the parsed, validated frontmatter (typed against your schema). - `Content` — a renderable React/Preact component compiled from the body. Render it as `` and pass element-level overrides through the `components` prop. This is the same contract Astro's `@astrojs/mdx` exposes; see [MDX Components](/concepts/mdx-components) for details and `defaultComponents` recipes. - `slug` — derived from the file name (`my-first-post.md` → `my-first-post`). Nested directories become slash-separated slugs. The function signature lives in [`getCollection`](/api/get-collection). ## How parsing works Under the hood, the `zfb-content` crate handles three jobs: it walks the configured directory, parses each file's YAML frontmatter, and compiles the Markdown/MDX body through an mdast → JSX-source emitter that is then handed to the existing SWC TSX → JS pipeline. The result is a JSX module per entry, addressed by a stable `mdx:///` specifier; the page renderer evaluates that module on demand and surfaces it to your page as `entry.Content`. The compilation and surface contract are stable. See [MDX Components](/concepts/mdx-components) for the rendering side; the Rust↔JS bridge contract is an internal stable interface between the `zfb-content` crate and the page renderer. --- # インストール > Source: https://takazudomodular.com/pj/zudo-front-builder/ja/docs/getting-started/installation `@takazudo/zfb` は npm パッケージとして配布され、npm の optional-deps 経由でビルド済みのプラットフォームバイナリを取得します。Rust ツールチェインは不要です。 **Node がない場合は?** npm や Node なしでも、zfb をスタンドアロンバイナリとしてインストールできます。curl・Homebrew・Windows でのインストール手順は [Install without Node](/install/node-free) を参照してください。 ## 前提条件 以下が必要です。 - **Node.js ≥ 22** — 設定ローダーや一部のビルドステップが Node を呼び出します。 - **pnpm** — [公式インストーラー](https://pnpm.io/installation) からインストールしてください。zfb は pnpm を使うプロジェクトをスキャフォールドし、`PATH` 上に pnpm が見つかる場合は `pnpm install` を自動で実行します。 ## CLI のインストール `@takazudo/zfb` をプロジェクトの開発依存としてインストールします。 ```bash pnpm add -D @takazudo/zfb ``` ```bash npm install -D @takazudo/zfb ``` ## インストールの確認 ```bash npx zfb --help ``` 4 つのサブコマンドが表示されるはずです。 - `zfb new` — 新しいプロジェクトをスキャフォールドする - `zfb dev` — ウォッチャーとライブリロード付きの開発サーバーを起動する - `zfb build` — 静的ビルドを生成する - `zfb preview` — ビルド出力を配信する `npx zfb --help` で 4 つすべてが表示されれば、[Your first site](/getting-started/your-first-site) に進む準備ができています。 ## ソースからビルドする(コントリビューター向け) zfb 本体にコントリビュートしていて CLI をソースからビルドする必要がある場合は、Rust ツールチェインの前提条件を含む詳しい手順を [BUILDING.md](https://github.com/Takazudo/zudo-front-builder/blob/main/BUILDING.md) で確認してください。 最短の手順: ```bash git clone https://github.com/Takazudo/zudo-front-builder.git cd zudo-front-builder cargo install --path crates/zfb ``` --- # Eleventy からの移行 > Source: https://takazudomodular.com/pj/zudo-front-builder/ja/docs/guides/migrating-from-eleventy Eleventy と zfb は大きな点で一致しています。小さな依存フットプリント、ファイルベースのルーティング、Markdown とフロントマターで駆動されるコンテンツです。両者が異なるのはテンプレートまわりです。移植に踏み切る前に、末尾の「率直な注記」セクションを読んでください。 ## レイアウトとインクルード 11ty はレイアウトを `_includes/` に置き、フロントマターの `layout:` キーで参照します。zfb はレイアウトを `layouts/` から読み込み、それをページの TSX に直接インポートします: ```tsx return {post.body}; } ``` フロントマターの `layout:` によるルックアップはありません。合成はインポートによって行います。 ## テンプレート 11ty は Liquid、Nunjucks、Handlebars、その他いくつかを提供します。zfb が提供するテンプレート言語はちょうど 1 つ、TSX だけです。Markdown コンテンツは依然としてパーサーを通りますが、そのコンテンツをラップするものはすべて JSX です。 コードベースが Nunjucks マクロや Liquid フィルターに大きく依存している場合、実際の移植作業を見込んでください。互換のためのシムレイヤーはありません。 ## データカスケード 11ty の `_data/` ディレクトリと、グローバル / ディレクトリ / テンプレートのデータカスケードには、zfb に直接の対応物はありません。代わりに、データを 2 か所で表現します: - **静的 / 共有データ** — `zfb.config.ts`(またはレガシーの `zfb.config.json`)でデータコレクションを宣言します。全体の形については [`defineConfig`](/api/define-config) を参照してください: ```ts collections: [ { name: "site", path: "data/site" }, ], }); ``` - **ページごとのデータ** — ページの `paths()` エクスポートから返します。これは 11ty の `eleventyComputed` におおよそ相当します: ```ts const posts = await getCollection("blog"); return posts.map((post) => ({ params: { slug: post.slug }, props: { post, year: post.data.pubDate.getFullYear() }, })); } ``` ## ページネーション 11ty の `pagination` フロントマターキーは、`paths()` の内部で使う `paginate()` ヘルパーになります。全体の API については /ja/api/paginate を参照してください。形は次のとおりです: ```ts const posts = await getCollection("blog"); return paginate(posts, { pageSize: 10 }); } ``` ページごとに 1 つのルートが得られ、加えて標準の `currentPage`、`totalPages`、`data` の props が渡されます。 ## Markdown フロントマター この部分はほとんど変わりません。`.md` ファイルの先頭で区切りとして `---` を使います。zfb-content はフロントマターを `entry.data` に、本文を `entry.body` にパースします。利用できるフィールドは、`zfb.config` で宣言したコレクションスキーマに依存します。 ## 率直な注記 Liquid や Nunjucks が好きで JSX が好きでないなら、zfb はあなた向けではありません。このフレームワークは JSX をテンプレートとして使うことに対して意見が強く、代替手段を追加するロードマップ項目もありません。そのワークフローには、Eleventy が今後もずっとより良い選択肢であり続けるでしょう。zfb があなたの時間に最も見合うのは、すでに別の場所で React や Preact のコンポーネントを書いていて、同じ言語を話す小さく高速な SSG が欲しい場合です。 --- # MDX Components > Source: https://takazudomodular.com/pj/zudo-front-builder/ja/docs/concepts/mdx-components `.mdx` コンテンツコレクションのエントリがどのようにレンダリング可能な JSX コンポーネントになるのか、`getCollection()` を通じてデフォルトで何が届くのか、個々の HTML 要素を自前のコンポーネントでオーバーライドする方法、グローバルな `mdx-components.tsx` 規約の仕組み、そして各オーバーライドが受け取る完全な props の契約を解説します。 ## メンタルモデル コンテンツコレクション内のすべての `.mdx` ファイルは、ビルド時に JSX モジュールへコンパイルされます。コンパイル後のモジュールは `MDXContent({ components })` 関数をエクスポートします。これは投稿本文をレンダリングするだけが役割の通常のコンポーネントで、`components` prop を通じて要素ごとのオーバーライドを任意に差し込めます。 これは Astro の `@astrojs/mdx` が `` で公開しているものと同じ契約です。zfb はこれをそのまま採用しているため、Astro エコシステム全体でメンタルモデルがそのまま通用します。 その形は次のとおりです。 ```tsx // Conceptually, your `hello-zfb.mdx` becomes: // ...renders the post body, looking up , , , etc. in `components` } ``` MDX が `some text`(Markdown の段落がコンパイルされた結果)に出会うと、`components` マップから `p` を探します。自前のコンポーネントを渡していればそれがレンダリングされ、渡していなければ素の HTML 要素がレンダリングされます。同じルックアップはカスタム JSX にも適用されます。投稿本文中の `...` は、評価時に `components.Note` に対して解決されます。 ## `getCollection()` がすでに与えてくれるもの `MDXContent` を自分で組み立てることはありません。コレクション層が各エントリのコンパイル済みモジュールを `Content` コンポーネントでラップし、エントリに付与します。つまり次のようになります。 ```tsx const posts = getCollection("blog"); const post = posts[0]; // `post.Content` is a renderable component: ; ``` `post.data`(パース済みのフロントマター)、`post.slug`、そして `post.Content`(レンダリング可能な本文)が得られます。`.md` と `.mdx` で形は同じです。CommonMark は MDX の厳密なサブセットなので、同じパイプラインが両方を扱います。プレーンな Markdown エントリにはオーバーライドすべき JSX ノードがそもそもありませんが、それでも `components` prop はそれらが出力する HTML 要素に適用されます。 `post.Content` を実際のコンパイル済み JSX モジュールに繋ぐブリッジは `globalThis.__zfb.content.get(specifier)` にあり、ページモジュールが評価される前に Rust 側のレンダラーがインストールします。ブリッジが存在しない場合(ユニットテストや開発用サンドボックスなど)、`Content` は明示的にマークされた `` ブロックにフォールバックし、欠落がひと目で分かるようになっています。 ## `defaultComponents` をスプレッドする zfb はルートエクスポートから `defaultComponents` マップを提供しています。これは MDX が出力する素の HTML タグを、適切なコンポーネント実装でラップする要素レベルのオーバーライド群(**htmlOverrides** 規約)です。使い方の定石は、まずこれをスプレッドし、その上に自前のオーバーライドを重ねることです。 ```tsx ; ``` スプレッドの順序が重要です。右側のキーが勝ちます。`defaultComponents` を最初に置くことで、衝突時には個々のオーバーライドが優先され、オーバーライドしていないものはすべてデフォルトの挙動を引き継ぎます。 現在の `defaultComponents` セットがカバーするのは次のとおりです。 | Key | Component | Wraps | | ------------ | ------------------- | ------------------------------------------- | | `h2` | `ContentH2` | `` | | `h3` | `ContentH3` | `` | | `h4` | `ContentH4` | `` | | `p` | `ContentParagraph` | `` | | `a` | `ContentLink` | `` | | `strong` | `ContentStrong` | `` | | `blockquote` | `ContentBlockquote` | `` | | `ul` | `ContentUl` | `` | | `ol` | `ContentOl` | `` | | `table` | `ContentTable` | `` | | `code` | `ContentCode` | `` (inline; block code is unaffected) | それぞれ個別にもエクスポートされており(`import { ContentLink } from "zfb"`)、マップ全体を引き込まずに単一のオーバーライドだけを使いたい場合に利用できます。 `h1` は意図的に含まれていません。ページタイトルは zudo-doc の規約に従ってフロントマターから `` をレンダリングします。`h1` をオーバーライドに加えると、タイトルが気付かないうちに二重レンダリングされてしまいます。 ## グローバルな `mdx-components.tsx` 規約 すべての `` 呼び出しに対して、呼び出しごとのスプレッドなしで適用したいプロジェクト全体の要素オーバーライドには、プロジェクトルート(`zfb.config.ts` の隣)に `mdx-components.tsx` ファイルを置き、フラットな `{ tag: Component }` マップを**デフォルトエクスポート**します。 ```tsx // mdx-components.tsx (project root) h2: MyH2, }; ``` ビルドパイプラインはこのファイルを検出してシャドウバンドルにコピーし、ページルーターが走る前にそのデフォルトエクスポートを `globalThis.__zfb.mdxComponents` にインストールします。その時点以降、すべての `` 呼び出しが自動的にこれを拾います。呼び出し側でのスプレッドは不要です。 ### 優先順位 コンポーネントがマージされるとき、後のエントリが勝ちます。マージの順序は次のとおりです。 1. `defaultComponents`(最も優先度が低い。組み込みの 11 個のパススルー) 2. `mdx-components.tsx` のデフォルトエクスポート(プロジェクト全体のオーバーライド) 3. `` の `components` prop(最も優先度が高い。呼び出しごとのオーバーライド) したがって、呼び出しごとの `components={{ h2: MyCallSiteH2 }}` はプロジェクトルートのファイルに勝ち、そのファイルは組み込みのデフォルトに勝ちます。内部的には `mergeMdxComponents`(`"zfb"` からエクスポート)がこの三方向のスプレッドを実行します。 ```ts { ...defaultComponents, ...globalSlot, ...perCall } ``` ### 実行可能な例 — `` を `` でラップする よくあるパターンは、見出しをスタイル付きのコンテナでラップすることです。 ```tsx // mdx-components.tsx (project root) type Props = { id?: string; children?: unknown }; function MyH2({ id, children }: Props) { return ( {children} ); } ``` これで、すべてのコンテンツエントリのすべての `##` 見出しが、ページごとの配線なしにプロジェクト全体で `…` としてレンダリングされます。 ## 標準的にマップ可能な要素の集合 次の HTML タグは MDX エミッターによって `_components.` 経由でルーティングされ、`components` マップでオーバーライド可能になります。このリストにないタグは、そもそも Markdown から出力されないか、生の HTML リテラルとして出力されます(後者は `components` マップではオーバーライドできません)。 **コア CommonMark:** `a` `blockquote` `br` `code` `em` `h1` `h2` `h3` `h4` `h5` `h6` `hr` `img` `li` `ol` `p` `pre` `strong` `ul` **GFM 拡張**(`zfb.config.ts` で GFM 構文が有効なときに利用可能): `del` `input` `section` `sup` `table` `tbody` `td` `th` `thead` `tr` `defaultComponents` マップはこのうち 11 個(h2–h4、p、a、strong、blockquote、ul、ol、table、code)をあらかじめ配線します。残りは素の HTML 文字列のままですが、キーを `components` マップに含めればどれでもオーバーライドできます。 ## Props の契約 各オーバーライドコンポーネントが受け取るのは、その要素に対して Markdown パイプラインが実際に生成する属性だけです。任意の HTML 属性を `{...props}` で網羅的にスプレッドして渡すような仕組みはありません。渡されるのは下表に挙げたフィールドだけです。 | Element(s) | Props received | | ------------- | ----------------------------------------------------- | | `a` | `href`、`title`(任意)、`children` | | `img` | `src`、`alt`、`title`(任意) — void、children なし | | `h2`–`h6` | `id`(見出しリンクプラグインが有効で、その見出しに slug が付与されている場合に存在)、`children` | | `h1` | `children` のみ — `h1` には slug は付与されない | | `pre` | `children`(`` の子をラップする) | | `code` | `className="language-*"`(フェンス付きブロックのみ。インラインコードは `children` のみ受け取る) | | `ol` | `start`(1 以外のときのみ)、`children` | | その他すべて | `children` のみ | オーバーライドに `{...rest}` を加えて HTML 要素にスプレッドしても、未知の属性は届きません。エミッターは Markdown ソースから任意の属性を転送しないからです。 ## 小文字と PascalCase の非対称性 `components` マップにおける小文字の HTML タグと PascalCase のコンポーネント名のあいだには、意図的な挙動の違いがあります。 - **小文字のタグ**(`h2`、`p`、`a` など)は、出力される `_components` マップの中で素の HTML 文字列をデフォルトとします。小文字のキーを省略した場合、生の要素がレンダリングされます。エラーにはなりません。 - **PascalCase の名前**(`Note`、`Callout` など)は、呼び出し側の `components` prop からルックアップされ、見つからなければ即座に throw します。 ```js // Inside compiled MDX output: const Note = _components.Note ?? components.Note; if (!Note) throw new Error("MDX requires `Note` to be passed via the `components` prop"); ``` `.mdx` ファイルで参照されている PascalCase コンポーネントが、レンダリング時に `components` マップに存在しない場合、ビルド時ではなく実行時に throw します。これは意図的なものです。コンパイル済みモジュールはポータブルなまま保たれ、必要なコンポーネントをすべて渡す責任は呼び出し側にあります。 ## Islands による制約 `components` マップは、SSR 時に解決されるプレーンな JavaScript オブジェクトです。値が関数参照であるため、JSON としてハイドレーション境界を越えられません。これは次のことを意味します。 - マップ内のコンポーネントは**サーバーサイド専用**です。サーバー上でレンダリングされ、その HTML が静的なマークアップとしてブラウザに届けられます。 - マップ内のコンポーネントがクライアントサイドのインタラクティブ性(状態、イベントハンドラ、ブラウザ API)を必要とする場合は、そのインタラクティブな部分を `` コンポーネントでラップし、インタラクティブなコンポーネントを直接マップに入れないでください。 ```tsx // Correct: interactive content goes through Island function WrappedCounter() { return ( ); } ``` `components` マップは SSR を通ります。`` はブラウザへ抜けるための脱出口です。 ## 保留中の未解決項目 — `wrapper` MDX は `components` マップ内の特別な `wrapper` キーをサポートしています。これはレンダリング結果全体をラップするコンポーネントです。zfb のエミッターは `wrapper` を認識しません。本文は直接 `<_Fragment>` でラップされ、`wrapper` キーに対するルックアップは行われません。`wrapper` のサポートは追跡中の未解決項目であり、現在のリリースでは実装されていません。 ## カスタムコンポーネントで `.mdx` を書く 同梱の `basic-blog` テンプレートが、エンドツーエンドの形を実演しています(全ソースは [zfb-example-blog スタンドアロンリポジトリ](https://github.com/Takazudo/zfb-example-blog) を参照)。この MDX 投稿はカスタムの `` admonition を使っています。 ```mdx --- title: Hello, zfb date: 2026-04-20 description: A short introduction to the basic-blog dogfood example. --- Welcome to the **basic-blog** example. This post is `.mdx`, not plain markdown. The `` admonition you are reading right now is a custom JSX component passed in via the `components` prop on `` — exactly the same delivery contract Astro's `@astrojs/mdx` exposes. ``` 投稿ごとのルートは、`` を `defaultComponents` と一緒に渡すことで `components` マップに対して解決します。 ```tsx // pages/blog/[slug].tsx return ( {post.data.title} ); } ``` `Note` コンポーネント自体は、`title` と `children` を受け取るごく普通の Preact(または React)コンポーネントです。 ```tsx // components/note.tsx type Props = { title?: string; children: ComponentChildren; }; return ( {title ? {title} : null} {children} ); } ``` 全ソースは [zfb-example-blog スタンドアロンリポジトリ](https://github.com/Takazudo/zfb-example-blog) にあります。`content/blog/hello-zfb.mdx`、`pages/blog/[slug].tsx`、`components/note.tsx` を参照してください。ライブビルドは [zfb-example-blog.pages.dev](https://zfb-example-blog.pages.dev/) で確認できます。 ## リファレンス - `defaultComponents` セットは、[zudo-doc](https://github.com/zudolab/zudo-doc) が確立した htmlOverrides 規約を zfb に移植したものです。zudo-doc では、同じマップの形が同じ問題をそのドキュメントフレームワーク向けに解決しています。 - Astro の上流パターンは [Astro: Assigning custom components to HTML elements](https://docs.astro.build/en/guides/integrations-guide/mdx/#assigning-custom-components-to-html-elements) に記載されています。契約は意図的に同一です。要素名 → コンポーネントのフラットなレコードを `components` prop で渡すというものです。 ## あわせて読む - [Content Collections](/concepts/content-collections) — エントリがどのように発見され、`getCollection()` を通じて公開されるか。 - [Build Pipeline](/concepts/build-pipeline) — MDX から JSX へのコンパイルがエンドツーエンドのフローのどこに位置するか。 - [Custom Directives](/concepts/custom-directives) — 自前の JSX コンポーネントにマッピングされる新しい MDX ディレクティブ構文(例: `:::callout`)を追加する。 - [Islands](/concepts/islands) — コンテンツエントリ内でインタラクティブなコンポーネントを動かす方法。 --- # カスタムディレクティブ > Source: https://takazudomodular.com/pj/zudo-front-builder/ja/docs/concepts/custom-directives このページは英語版のスタブです。完全な翻訳は別の PR で行われます。 英語版: [Custom Directives](/concepts/custom-directives) ## 概要 MDX のディレクティブレジストリを使い、Rust コードを書かずに独自のディレクティブを追加する方法について解説します。 英語の最新ドキュメントは [Custom Directives](/concepts/custom-directives) を参照してください。 --- # paginate > Source: https://takazudomodular.com/pj/zudo-front-builder/ja/docs/api/paginate ## シグネチャ ```ts paginate( items: readonly T[], opts: PaginateOptions, ): PaginateRoute[] ``` `paginate` は `paths()` エクスポート内で使い、アイテムのリストをページネーションされたルートへ展開します。この関数は `items` を `pageSize` ごとにチャンク分割し、チャンクごとに 1 つの `PaginateRoute` を生成します。各ルートはコンポーネントが受け取る URL パラメータとページプロップを保持します。 `paginate` は常に少なくとも 1 つのルートを発行します。空の入力リストでは 1 つの空ページが生成され、インデックスルートが 404 にならずレンダリングされます。 ## PaginateOptions ```ts type PaginateOptions = { pageSize: number; param: K; }; ``` - `pageSize` — 1 ページあたりのアイテム数。正の整数である必要があります。 - `param` — 埋め込む動的 URL セグメントの名前(例: `[page].tsx` という名前のルートファイルなら `"page"`)。 ## PaginateRoute の形状 ```ts type PaginateRoute = { params: Record; props: { page: PaginatedPage }; }; ``` 各ルートは以下を保持します。 - `params` — URL セグメントの値。`param: "page"` の呼び出しでは `{ page: "1" }`、`{ page: "2" }` などになります。 - `props.page` — 以下を持つ `PaginatedPage` レコード: - `data: T[]` — このページに属するアイテム。 - `page: number` — 1 始まりのページ番号。 - `lastPage: number` — 総ページ数。 - `pageSize: number` — 1 ページあたりのアイテム数(便宜上のエコー)。 - `total: number` — 全ページにわたるアイテムの総数。 ## 例 ブログインデックスを `/blog/page/[page].tsx` 配下で 10 記事ずつページネーションします。 ```tsx const posts = getCollection("blog"); return paginate(posts, { pageSize: 10, param: "page", }); } const { data: items, page: currentPage, lastPage } = page; return ( Blog — page {currentPage} of {lastPage} {items.map((post) => ( {post.data.title} ))} ); } ``` `param` の値(`"page"`)は、ルートファイル名の動的セグメント名(`[page].tsx`)と一致している必要があります。ページ番号は 1 から始まり、`params` では文字列として渡されます。 ページコンポーネントは `PaginatedPage` レコードを `props.page` として受け取ります。分割代入で `data`、`page`、`lastPage`、`pageSize`、`total` にアクセスしてください。 --- # フレームワークアダプター > Source: https://takazudomodular.com/pj/zudo-front-builder/ja/docs/architecture/framework-adapters zfb は TSX をレンダリングします。JSX は構文であり、それを HTML に変える *ランタイム* はフレームワークが供給します。zfb は 2 つをサポートします。**Preact**(デフォルト)と **React** です。 ## 必要性 2 つの目標が反対方向に引っ張ります。1 つ目は、両方のフレームワークで同じように動く単一のレンダラーパイプラインです。これによりビルドを分岐させずに済みます。2 つ目は、多くのユーザーが React を望み(既存のコンポーネント、React 専用のライブラリ)、多くが Preact を望む(フットプリント、シグナル)ことです。zfb は選べるようにします。 ```ts // zfb.config.ts framework: "preact", // or "react" }; ``` 設定の読み込み時に一度だけ読まれ、ビルド全体に通されます。ユーザーにとっては 1 行の変更です。レンダラーにとっては、その選択が一切現れません。レンダラーは統一された `Adapter` トレイトと対話し、アダプターの構築以降に `if framework == "react"` の分岐を持ちません。 ## 境界 コントラクトは `crates/zfb-render/src/adapters/mod.rs` にあります。すべてのアダプターは同じ狭いトレイトを実装します。 ```rust pub trait Adapter { fn name(&self) -> &'static str; fn jsx_import_source(&self) -> &'static str; fn render_to_string_module(&self) -> &'static str; fn pre_render_setup(&self, host: &mut dyn RenderHost) -> Result<(), RenderError>; fn hydrate_shim_specifier(&self) -> &'static str; fn hydrate_shim_source(&self) -> &'static str; } ``` 責務は 3 つのグループだけです。 1. **SWC パイプラインにどの JSX import source を注入するかを伝える**(`jsx_import_source`)。これは `runtime: "automatic"` での `transform-react` パスを駆動するため、ユーザーのコードは `import { h } from "preact"` も `import React from "react"` も書かず、`@jsxImportSource` プラグマを綴ることもありません。フレームワークの選択は設定に存在し、SWC がそれをあらゆる場所に通します。 2. **同期的な `renderToString` がどこにあるかをランタイムに伝え**(`render_to_string_module`)、それを `globalThis.__zfbRenderToString` としてエイリアスする **シムをインストールする**(`pre_render_setup`)。すると `render.rs` のレンダーオーケストレーターは、フレームワークの識別子で分岐することなく、ページごとに一度だけ `__zfbRenderToString(vnode)` を呼びます。 3. **クライアントサイドのハイドレーションシムを提供する**(`hydrate_shim_specifier` + `hydrate_shim_source`)。アイランドバンドラーはこのモジュールをフレームワーク固有のエントリとしてアイランドバンドルに折り込みます。これは `hydrateIsland(Component, props, element)` をエクスポートし、`zfb-islands` のフレームワーク非依存なハイドレーションランタイムが DOM 内のすべての `[data-zfb-island]` 要素に対してそれを呼び出します。 これがインターフェースの全体です。フックのセマンティクス、シグナルの相互運用、イベント委譲の戦略 — これらはどれも境界の内側にありません。各フレームワークは自身の振る舞いを保ち、アダプターは *どの* フレームワークに到達するかだけを制御します。 ## なぜセットアップフェーズのシムなのか `pre_render_setup` は最初のページレンダリングの前に一度だけ実行され、`globalThis` に `__zfbRenderToString` をインストールします。するとページごとのレンダリングは、Rust から JS への 1 回の関数呼び出しになります。モジュールの再解決も、シムの再インストールも、呼び出しごとの import の手順も不要です。コストはホストの生存期間につき一度だけ支払われ、その恩恵は数千ページにわたって積み上がります。 魅力的に見える 2 つの代替案は除外されます。私たちは `preact-render-to-string` を直接 import するページごとの JS モジュールを生成しません。それはモジュール解決をレンダリングごとのホットパスに押し込むからです。また、Rust がソースにテンプレート展開するための JS 式を返す `hydrate_call()` API も公開しません。それは JS 式の連結を Rust という不向きな言語に置くことになるからです。シムは Rust から JS への境界を、純粋に静的なモジュール文字列として表現し続けます。 ## Preact アダプター `crates/zfb-render/src/adapters/preact.rs` にあります。JSX import source は `"preact"`、render モジュールは `"preact-render-to-string"`、ハイドレーションシムは `hydrate(vnode, container)` をラップします。Preact がデフォルトなのは、そのフットプリントが zfb の静的出力ターゲットにきれいに収まるからです。`preact-render-to-string` は同期的な SSR のために専用設計されており、ランタイムのイメージは小さく保たれます。 ## React アダプター `crates/zfb-render/src/adapters/react.rs` にあります。Preact より大きく、React のエコシステムを必要とするユーザー向けのオプトインです。JSX import source は `"react"`、render モジュールは `"react-dom/server"`、ハイドレーションシムは React 18 以降の `hydrateRoot(container, vnode)` をラップします。Preact と比べて引数の順序が入れ替わっている点に注意してください。これはアダプターが引き受けるため、ユーザーのコードが対応する必要はありません。 React アダプターは **同期的な** `renderToString` を使い、`renderToReadableStream` や `renderToPipeableStream` は使いません。zfb は事前生成の静的 HTML を作るため、同期的な文字列の方がシンプルで決定論的であり、Node のストリーミングプリミティブをビルドホストに引きずり込まずに済みます。コストは、v1 ではストリーミング HTML と React Server Components がないことです。 ## 将来のアダプター Solid、Vue SSR、Svelte SSR — いずれも今日は出荷されていません。`Adapter` トレイトは十分小さいので、1 つ追加するのは純粋に加算的です。新しいアダプターファイル、新しい `Framework` enum のバリアント、新しい `make_adapter` のアームを増やすだけです。レンダーオーケストレーターは変わりません。 将来のフレームワークがトレイトを満たせない場合 — 例えば `renderToString → string` に収まらない非同期の render エントリを必要とする場合 — それはトレイトを意図的に広げる理由であって、特別扱いする理由ではありません。 --- # データ > Source: https://takazudomodular.com/pj/zudo-front-builder/ja/docs/concepts/data zfb は構造化データをページに取り込むための 3 つの異なる経路を用意しています。どれが正しい 選択かは、データの形と、その周りにどれだけの手続きを求めるかによって決まります。 ## 1. コンテンツコレクション データが **文章** のとき(ブログ記事、ドキュメント記事、レシピなど)はコンテンツコレクションを 使います。各エントリはフロントマター付きの Markdown ファイルで、フロントマターはスキーマに 対して検証され、本文はあなたのために HTML へレンダリングされます。詳しくは [Content Collections](/concepts/content-collections) を参照してください。 各エントリの本文が読まれることを意図している場合は、常にこれが正しい選択です。 ## 2. データコレクション データが **構造化されている** とき(製品のリスト、チームメンバーのディレクトリ、料金ティアの 集合など)はデータコレクションを使います。`zfb.config.ts` で宣言する JSON / YAML / TOML ファイルのディレクトリです。コンテンツコレクションが使うのと同じ `[{ name, path }]` の形 ([Content Collections](/concepts/content-collections) を参照)が、手続きなしでデータ ディレクトリを受け付けます。 ```ts collections: [ { name: "products", path: "data/products", }, ], }; ``` 各ファイルが 1 つのエントリになります。フィールドごとのバリデータを持つオプションの `schema` フィールドを指定できます(完全な形は [`defineConfig`](/api/define-config) を参照)。データ エントリはコンテンツエントリと同じ方法でロードします。 ```tsx const products = getCollection("products"); ``` データコレクションが真価を発揮するのは、**エントリ単位のバリデーション、slug の導出、そして 文章コンテンツと同じロード API** が欲しいけれど、Markdown のレンダリングステップは不要、 というときです。 ## 3. `data/` 配下のプレーンな TS モジュール それ以外すべて(小さなルックアップ、ヘルパー関数、導出された定数など)には、通常の TypeScript モジュールが最も軽量な経路です。 ```ts // data/site.ts return date.toISOString().slice(0, 10); } ``` 任意のページやコンポーネントからインポートします。 ```tsx ``` スキーマも slug もエントリ単位のオーバーヘッドもありません。「コレクション」というフレーミングが、 そのデータに見合う以上の手続きになってしまう場合に使います。 ## 正しいツールを選ぶ 役に立つ目安: - データが **読まれることを意図したコンテンツ** → コンテンツコレクション。 - データが **同じ形を共有する構造化レコード** → データコレクション。 - データが **ひと握りの定数やヘルパー** → `data/` 配下の TS モジュール。 3 つすべてを同じプロジェクトの中で衝突なく混在させられます。 --- # 最初のサイト > Source: https://takazudomodular.com/pj/zudo-front-builder/ja/docs/getting-started/your-first-site このウォークスルーでは、空のディレクトリから開発サーバーの起動、そしてサイトのビルドまでを案内します。グローバルな CLI インストールは不要です。スキャフォールドコマンドが必要なものをすべてオンデマンドで取得します。 ## 新しいプロジェクトをスキャフォールドする ```bash pnpm create zfb@latest my-site # または npm create zfb@latest my-site ``` これは `create-zfb` イニシャライザを実行します。内部では `zfb new` を呼び出し、同梱の `basic-blog` テンプレートを `my-site/` にコピーします。`PATH` 上に pnpm があれば、zfb はテンプレートのコピー直後に `pnpm install` を自動実行します。なければ、自分で実行するよう促す短いメッセージを表示します。 ```bash cd my-site ``` zfb CLI をすでにグローバルインストールしている場合([Installation](/getting-started/installation) を参照)、`zfb new` を直接呼び出しても同じ結果が得られます。 ```bash zfb new my-site --template basic-blog ``` `--template basic-blog` はデフォルトであり、現時点で唯一利用可能なテンプレートなので、省略できます。 ## 開発サーバーを起動する ```bash pnpm zfb dev # または npx zfb dev ``` ホストとポートを示す「ready」バナーが表示されます(デフォルトは `http://localhost:3000`)。zfb はプロジェクトディレクトリを監視します。`pages/`・`layouts/`・`components/`・`content/`・`styles/` 内のファイルを保存すると、接続中のブラウザにライブリロードがブロードキャストされます。サーバーは `public/` に置いたものも配信します。 ホストとポートは CLI から上書きできます。これらは常に `zfb.config.ts` より優先されます。 ```bash pnpm zfb dev --port 4000 --host 0.0.0.0 # または npx zfb dev --port 4000 --host 0.0.0.0 ``` ## ビルドとプレビュー サイトの静的ビルドを生成するには: ```bash pnpm zfb build pnpm zfb preview # または npx zfb build npx zfb preview ``` `zfb build` は出力ディレクトリ(デフォルトは `dist/`)を解決し、ファイルシステムルーターでルートを列挙し、静的ルートごとに 1 つの HTML ファイルを書き出します。続いて `zfb preview` がそのディレクトリをデフォルトでポート `4321` の HTTP で配信します。CLI フラグ(`--outdir`・`--port`)は両コマンドとも設定ファイルより優先されます。 ## 動作するサンプル `basic-blog` を使って完全にビルドされたサイトが [https://github.com/Takazudo/zfb-example-blog](https://github.com/Takazudo/zfb-example-blog) で公開されています。 ## 次に読むもの - [Project structure](/getting-started/project-structure) — テンプレートが作成した各ディレクトリが実際に何をするのか。 - [Routing](/concepts/routing) — `pages/` がどのように URL になるか。動的ルートやキャッチオールルートも含みます。 - [Islands](/concepts/islands) — 個々のコンポーネントをクライアントサイドのハイドレーションにオプトインする方法。 - [Build engine](/architecture/build-engine) — Rust クレートが内部でどう組み合わさっているか。 --- # Markdown のカスタマイズ > Source: https://takazudomodular.com/pj/zudo-front-builder/ja/docs/guides/customizing-markdown このページは英語版のスタブです。完全な翻訳は別の PR で行われます。 英語版: [Customizing Markdown](/guides/customizing-markdown) ## 概要 `zfb-content` クレートが Markdown / MDX をどう扱い、現時点で何をカスタマイズでき、何が今後の作業として残っているかをまとめたページです。コンテンツコレクションから受け取るエントリの `entry.body`(フロントマターを除いた生の Markdown 文字列)と `entry.Content`(コンパイル済みの JSX コンポーネント)を起点にした「消費側」の視点でまとめています。 エンジン側を Rust の visitor で拡張したい場合は [Extending the Markdown Pipeline](/guides/extending-the-markdown-pipeline) を参照してください。ディレクティブ(`:::name` / `::name` / `:name`)を追加するだけなら [Custom Directives](/concepts/custom-directives) で十分です。 英語の最新ドキュメントは [Customizing Markdown](/guides/customizing-markdown) を参照してください。 ## 目次(TOC)の自動生成 zfb は `remark-toc` 相当の機能を opt-in の hast ビジターとして提供しています。有効にすると、設定したアンカー文字列(デフォルト `"TOC"`、大小文字を区別しない)と一致する見出しを検出し、後続の見出しを `/` リストとして次の兄弟要素に挿入します。 このビジターは **hast フェーズの `HeadingLinksPlugin` 後** に実行されるため、`id` 属性を再計算することなく、すでに付与された最終的な(重複排除済みの)`id` をそのまま参照します。 詳細は英語版の [Customizing Markdown](/guides/customizing-markdown#table-of-contents-toc) を参照してください。 ## 外部リンク `markdown.externalLinks` オプションを使うと、サイト外へのリンクに `target` や `rel` 属性を自動付与できます。 ```ts // zfb.config.ts markdown: { externalLinks: { target: "_blank", rel: ["noopener", "noreferrer"], }, ## CJK フレンドリーな強調 CommonMark の強調フランキングルールは、CJK 文字(漢字・かな・ハングルなど)を空白でも句読点でもないとみなします。そのため `**テスト。**テスト` のように CJK テキストに隣接した `**` は CommonMark の仕様では右フランキングと判定されず、`` タグではなくリテラルのアスタリスクとして出力されます。 zfb には組み込みの `CjkFriendlyPlugin` があり、パース後の Markdown AST を後処理してこのケースを再トークナイズします。**デフォルトで有効**になっているため、既存の CJK コンテンツサイトは設定変更なしでそのまま動作します。 ```ts title="zfb.config.ts" markdown: { // absent または true の場合、CJK フレンドリーは有効(デフォルト)。 cjkFriendly: true, // 厳密な CommonMark 出力が必要で、かつ強調マーカーが隣接した // CJK コンテンツがない場合のみ false に設定してください。 // cjkFriendly: false, }, }); ``` href が外部リンクと判定される条件: 絶対 URL(`http:` または `https:` スキーム)であり、かつ `site` 設定が指定されている場合はそのオリジンと異なる場合。`site` が未設定のときは、すべての絶対 HTTP/HTTPS URL が外部リンクとして扱われます。 `mailto:`・`tel:` などの非 HTTP(S) スキームは常にそのまま出力されます。相対パス(`/internal/`・`./page.mdx`・`#anchor`)は常に内部リンクです。 詳細は英語版 [Customizing Markdown](/guides/customizing-markdown) を参照してください。 **保守的なデフォルトの理由:** `cjkFriendly` をデフォルト `false` にすると、日本語の句読点やかなに隣接した `**bold**` を使用している既存のサイトがすべて壊れます。このフィールドは既存の動作を変えるためではなく、必要な場合にオプトアウトするために導入されました。 **GFM 取り消し線**(`~~foo~~`)は CJK 境界でも正常に動作します。markdown-rs の GFM トークナイザーが `~~` 区切りを CommonMark の強調フランキングルールとは独立して処理するため、このトグルの影響を受けません。 --- # Markdown パイプラインの拡張 > Source: https://takazudomodular.com/pj/zudo-front-builder/ja/docs/guides/extending-the-markdown-pipeline このページは英語版のスタブです。完全な翻訳は別の PR で行われます。 英語版: [Extending the Markdown Pipeline](/guides/extending-the-markdown-pipeline) ## 概要 `zfb-content` クレートの Markdown パイプラインをエンジン側で拡張する方法について解説します。新しいディレクティブを追加するだけなら [Custom Directives](/concepts/custom-directives) で十分ですが、AST レベルの書き換え(見出しスラッグ、コードブロック装飾、画像変換、リンク解決など)が必要な場合は `MdastVisitor` または `HastVisitor` を実装して in-tree のプラグインとして追加します。 主なトピック: - 2 段階パイプライン(mdast → hast)と、どちらの段階で何をすべきか - visitor トレイトの形(`visit` メソッド一つ、再帰は呼び出し側責任、in-place 変更) - プラグインファイルの置き場所と `plugins.rs` への公開方法 - `Pipeline::with_defaults` に組み込まれた依存順序のルール - markdown-rs がパースしているがコンバーターが捨てている構文(テーブル、脚注、定義、math など)を有効化するための `mdast_to_hast` 拡張手順 英語の最新ドキュメントは [Extending the Markdown Pipeline](/guides/extending-the-markdown-pipeline) を参照してください。 --- # Island > Source: https://takazudomodular.com/pj/zudo-front-builder/ja/docs/api/island ## `` ラッパー `"@takazudo/zfb"` から `Island` をインポートし、ブラウザでハイドレートしたいコンポーネントをラップします。それ以外はサーバーレンダリングされた HTML のままになります。 ```tsx return ( My Page ); } ``` SSR 時、`` ラッパーは `` マーカーを発行し、その内側にサーバーレンダリングされた子要素を配置します。クライアントランタイム(`@takazudo/zfb-runtime`)はこれらのマーカーを問い合わせ、`when` 条件が発火したときにコンポーネントをマウントします。 ## IslandProps ```ts interface IslandProps { when?: When; ssrFallback?: VNode; children?: VNode; } ``` - `when` — ハイドレーション戦略。デフォルトは `"load"`。後述の [ハイドレーション戦略](#ハイドレーション戦略) を参照してください。 - `ssrFallback` — SSR スキップモードを有効にします(Astro の `client:only` に相当)。指定すると、重い `children` はサーバーサイドで評価され**ません**。代わりに `ssrFallback` がレンダリングされ、クライアントはハイドレーション時に本物のコンポーネントに差し替えます。 - `children` — ハイドレートするコンポーネント。 ## ハイドレーション戦略 `when` プロップは 3 つの値を受け付けます。いずれも現在出荷済みです。 | 値 | 振る舞い | | --- | --- | | `"load"` | ページの JavaScript が実行されたら即座にハイドレートします。`when` を省略した場合のデフォルトです。 | | `"visible"` | 島のルート要素がビューポートに入ったときにハイドレートします(IntersectionObserver、しきい値 0)。画面外コンテンツに対して最も低コストの遅延手段です。 | | `"idle"` | ブラウザの次のアイドルコールバック時にハイドレートします。`requestIdleCallback` がないプラットフォームでは `setTimeout(0)` にフォールバックします。 | ## SSR スキップモード `ssrFallback` を渡すと、重い子要素のサーバーレンダリングを完全にスキップできます。 ```tsx return ( Loading chart…}> ); } ``` サーバーは `…fallback…` を発行します。ハイドレーション時にクライアントランタイムがプレースホルダーへ `HeavyChart` をレンダリングします。 ## エクスポートされる定数とヘルパー 以下は `Island` とともに `"@takazudo/zfb"` からエクスポートされます。 - `HYDRATE_MARKER_ATTR` — `data-zfb-island` 属性名。 - `SKIP_SSR_MARKER_ATTR` — `data-zfb-island-skip-ssr` 属性名。 - `ANONYMOUS_COMPONENT_NAME` — ラップした子要素の素性を特定できないときに使われるフォールバック名。 - `resolveWhen(when: unknown): When` — `when` 値を検証して正規化します。不明な入力は `"load"` にフォールバックします(開発時は警告つき)。 ## "use client" ディレクティブ zfb は代替の記述スタイルとして、ファイルレベルの `"use client"` ディレクティブもサポートします。最初の文がリテラル文字列 `"use client"` であるコンポーネントファイルは、ビルドスキャナ(`crates/zfb-islands`)によって島のエントリとして扱われます。esbuild ベースのバンドラは、それを独自の依存グラフとともに ESM へ個別にコンパイルします。 ```tsx "use client"; const [count, setCount] = useState(0); return ( setCount(count + 1)}> Count: {count} ); } ``` サーバーコンポーネントやページモジュールから島をインポートします。ビルドパイプラインがハイドレーションのブートストラップを自動的に配線します。 島のアーキテクチャ全般や、いつ島に手を伸ばすべきかについては [Islands](/concepts/islands) を参照してください。 --- # なぜ Rust か > Source: https://takazudomodular.com/pj/zudo-front-builder/ja/docs/architecture/why-rust zfb は、ユーザーが TypeScript と JSX を書く静的サイトフレームワークです。フレームワーク自体は Rust です。このページではその理由を説明します。 ## パフォーマンス 見出しは、ミリ秒単位で計測されるページ単位のリビルド時間です。パース、型チェック、コンテンツ処理、アトミックなファイル書き込みはすべてネイティブな Rust であり、「十分速い」ではなく、純粋に本当に速いのです。2,000 ページのサイトでも、共有ヘッダーが変更されたとき影響を受けるページを 1 秒を十分に下回る時間でリビルドします。作業が既知の依存グラフ上を回る緊密な Rust のループだからです。 dev ループも同じ形です。保存が FS イベントを生み、ウォッチャーがデバウンスし、オーケストレーターがグラフに問い合わせ、影響を受けるページが再レンダリングされ、dev サーバーがリロードをブロードキャストします。典型的な編集では、エンドツーエンドのレイテンシは数十ミリ秒の範囲に収まります。遅いのはブラウザのリフレッシュの部分です。 これは魔法ではありません。フレームワークが保存のたびにインタプリタを JIT ウォームアップせず、数ギガバイトのモジュールグラフをメモリに保持しないときに得られるものです。 ## メモリ安全性と構造化されたエラー Rust のコンパイラは、use-after-free、データ競合、null 参照をコンパイル時に排除します。ビルドは `dist/` を書き込んでいる途中でセグフォルトしません。ウォッチャーは保存のバーストが重なってもパニックしません。退屈な勝利、いったん手にすると気づかなくなる類のものです。 目に見える勝利はエラーの質です。zfb は `anyhow` を端から端まで使うため、すべてのエラーはチェーンとして CLI に届きます。直接の失敗、失敗した操作、ファイルパス、より高レベルの意図です。TSX のコンパイルエラーは、ファイル・行・列・それを引き起こした操作を示します — 汎用的な「build failed」ではありません。 ## 適合する並行性 ビルドオーケストレーターは CPU をまたいでスケジューリングし、dev サーバーは `axum` のリスナーと SSE のブロードキャストを同時に走らせます。Rayon が並列ページレンダリングを扱い、tokio が非同期 I/O を扱います。これらは儀式なしに共存します — Rust の型システムが境界を強制するため、スレッドが共有すべきでない状態を共有することは決してありません。 同じコードを Node で書けば、同等の隔離のためにワーカースレッドかチャイルドプロセスが必要になります。ワーカーはモジュールを共有せず、チャイルドプロセスはメモリを共有しません。どちらにせよ、Rust 版がただで行うことに対して、ページごとに調整のオーバーヘッドを支払います。 ## 配布 zfb は npm 経由で配布される単一バイナリです。`@takazudo/zfb` は npm の optional-deps を介してプリビルドのプラットフォームバイナリを引き込みます — その実行ファイルがフレームワークです。zfb 自体には `node_modules` がなく、監査すべき推移的なツリーもありません。あなたのプロジェクトは import する依存関係(TypeScript、npm パッケージ、ロックファイル)のために自前の `pnpm` を持ち込みますが、フレームワークはそのツリーに含まれません。(ソースからビルドする必要があるコントリビューターは [From source](/getting-started/installation#from-source-contributors) の手順に従えます。) 単一バイナリはきれいにピン留めされます。CI は必要なバージョンをインストールし、本番デプロイは dev で動いたのと同じバイナリを出荷します。チームが使っているバージョンは 1 つの数字であり、`package.json` のレンジの星座ではありません。 ## 正直な反論: ビルドコスト zfb のワークスペースの最初の `cargo build` は遅いです。V8 のソースバンドル(`zfb-render` が `deno_core` を介して引き込む)は、典型的なコントリビューターのマシンで 15 分から 30 分かけてコンパイルされます。増分ビルドは速いものの、最初の 1 回はこたえます。このコストは受け入れられています。Tauri での配布はエンドユーザーのマシンに Node 依存のない単一バイナリを必要とし、`deno_core` を組み込むことがその制約を満たす唯一の方法だからです。完全な理由は [JS runtime](/architecture/js-runtime) を参照してください。 zfb は `zfb-render` のデフォルトで有効な `embed_v8` cargo フィーチャーの背後で V8 のビルドをゲートします。JavaScript のレンダリングを必要としないクレートは V8 を一切引き込みません。`cargo test -p zfb-content` を走らせるコントリビューターは数秒でコンパイルします。エンドユーザーはこのコストを支払いません — プリビルドのバイナリをインストールするからです。 このゲートは、リンクされた `zfb` バイナリそのものにとってはタダの昼食ではありません。V8 が成果物を支配し(バンドルされた `v8` rlib はそれ自体で約 169 MiB、残りの `deno_*` ツリーがさらに約 13 MiB を加えます)、そのため V8 オフの `zfb` はストリップ後で V8 オンの出荷バイナリより約 82 MiB 小さくなります。今日のところ、その軽いバイナリは `zfb-render` ライブラリの V8 なしの利用者にとってのみ有用です — SSG レンダリングがユーザーのページを動かすために V8 を必要とするため、V8 なしの `zfb` ビルドステップはまだありません。このフィーチャーは、将来の出荷経路(Tauri サイドカー、スタンドアロン SSR サーバー、`cargo install` によるデプロイ)がワークスペースを再設計せずに小さい成果物をオプトインできるよう配線されています。 ## JS ベースの代替との比較 Vite、Next、Astro、SvelteKit — すべて優れたツールです。zfb の賭けは異なるものであり、あらゆる方向で優れているわけではありません。**もしフレームワークが Rust で、ユーザーのコードだけが JS / TS だったら?** というものです。 それはトレードオフを傾けます。ビルド速度は安くなります。フレームワークのコストで JS のモジュールグラフを走査するバンドラーがないからです。配布はシンプルになります — フレームワークの `node_modules` がありません。メモリと並行性は言語レベルの保証を得ます。エラーの質は `anyhow` を通して統一された形を得ます。 コストは、フレームワークのネイティブ言語における小さめのプラグインエコシステムと、コントリビューターにとっての長めの初回ビルドです。zfb は両方を意図的に支払います。設計の狙いが、ビルドに邪魔されたくないユーザーだからです。 膨大なプラグインエコシステムと、何年もかけて磨かれたホットリロードの体験を備えた JS ネイティブのフレームワークが欲しいなら、そうしたツールは存在します。フレームワークが速いバイナリの中に消え去り、リビルドが何も起きなかったかのように感じられることを望むなら、それこそが zfb の目的です。 --- # スタイリング > Source: https://takazudomodular.com/pj/zudo-front-builder/ja/docs/concepts/styling zfb のスタイリングには 2 つのレイヤー(グローバル CSS と Tailwind v4)があり、それ以外のすべてに対しては 1 つの十分にサポートされたパターンがあります。マークアップそのものに付与するユーティリティクラスです。 ## グローバル CSS デフォルトテンプレートには `styles/global.css` が付属しています。これはプレーンな CSS で、zfb の CSS パイプラインによって処理され、すべてのページから利用できます。デザイントークン、リセット、ベースとなるタイポグラフィなど、サイト全体に適用すべきものに使ってください。 ```css :root { --color-text: #1a1a1a; --color-bg: #ffffff; --font-body: system-ui, sans-serif; } body { color: var(--color-text); background: var(--color-bg); font-family: var(--font-body); } ``` CSS 内の `import` は期待どおりに動作します。スタイルを複数のファイルに分割し、`global.css` からまとめて読み込めます。 ## Tailwind v4 Tailwind v4 は `zfb.config.{ts,json}` から opt-in で有効化します。 ```json { "tailwind": { "enabled": true } } ``` 有効にすると、`zfb-css` クレートがビルドの一環として同梱の `tailwindcss-v4` バイナリを実行します。**プロジェクトごとに Tailwind をインストールする必要はありません**。`package.json` に `tailwindcss` を追加することも、`tailwind.config.js` を保守することもありません。コンパイラは zfb 自体に組み込まれています。 Tailwind を有効にすると、ユーティリティクラスはあらゆる `.tsx` ファイルで動作します。 ```tsx return ( Hello ); } ``` Tailwind v4 の CSS ファーストな設定は `global.css` 内の `@theme` ディレクティブを通じてサポートされます。トークンのカスタマイズは JS の設定ファイルではなく CSS を編集して行います。 ## コンポーネントスコープなスタイリング コンポーネントレベルのスタイリングには、十分にサポートされたパターンが 2 つあります。Tailwind ユーティリティクラスと CSS Modules です。 ### Tailwind ユーティリティクラス 最もシンプルなパターンは、**サイト全体の関心事にはグローバル CSS を、コンポーネントレベルのスタイリングには Tailwind ユーティリティクラスを使う** というものです。これによりビルドは高速に、ランタイムは些細なものに保たれ、Tailwind v4 のデザイントークンモデルにきれいにマッピングできます。 ### CSS Modules 真にコンポーネントスコープな CSS(コンポーネント間で衝突してはならないクラス名)には、zfb は CSS Modules をサポートしています。`*.module.css` という名前のファイルはすべて CSS Module です。そのクラス名はビルド時にスコープ付きでファイル安定な識別子へと書き換えられるため、2 つのコンポーネントが衝突することなく両方とも `.button` クラスを定義できます。 スタイルは `.module.css` ファイルに記述します。 ```css title="components/card.module.css" .card { border: 1px solid var(--color-border); border-radius: 8px; padding: 1rem; } .title { font-weight: 700; } ``` モジュールは **デフォルトインポート** でインポートし、インポートしたオブジェクトからクラス名を読み取ります。 ```tsx title="components/Card.tsx" return ( Hello ); } ``` ビルド時に zfb は `styles.card` をスコープ付きのクラス名(例: `KdPA9G_card`)に解決します。レンダリングされた HTML はそのスコープ付きクラスを持ち、スコープ付き CSS は残りの CSS と同じハッシュ付きの `dist/assets/styles-.css` スタイルシートに畳み込まれます。モジュールごとに別の `.css` ファイルが作られることはなく、ランタイムコストもありません。ルックアップはビルド中に解決されます。 仕組み: - `import styles from "./x.module.css"` は **デフォルトインポート** でなければなりません。`styles` は元のクラス名をスコープ付きのものへマッピングするプレーンオブジェクトです。 - クラスへはメンバーアクセスでアクセスします(`styles.card` または `styles["card"]`)。どちらも動作しますが、動的なキーによる計算アクセスは動作しません。書き換えがビルド時に行われるためです。 - プレーンな `.css` のインポート(`.module.css` で終わら *ない* ファイル)は引き続きグローバル CSS として扱われます。スコープ化を opt-in するのは `.module.css` サフィックスだけです。 - `.module.css` ファイルは、`pages/`・`components/`・`layouts/`・`content/` 配下の `.tsx`/`.ts`/`.jsx`/`.js` ファイルからインポートされたときに発見されます。 制限: - `node_modules` から **ベア specifier** でインポートされる CSS Modules(例: `import s from "@org/pkg/x.module.css"`)はスコープ化されません。スコープ化されるのはプロジェクト相対の `./` / `../` インポートだけです。 - `:export` ブロックと `composes` ディレクティブはサポートされていません。プレーンなクラスセレクタを使ってください。 ## `dist/` に出力されるもの ビルドパイプラインは Tailwind と PostCSS を実行し、ハッシュ付きのスタイルシートを `dist/assets/` に書き出し、レンダリングされた各 HTML ページに `` を注入します。スタイルシートの参照は安定しているため、CDN キャッシュは内容が変わるまでデプロイをまたいで保持できます。 --- # プロジェクト構造 > Source: https://takazudomodular.com/pj/zudo-front-builder/ja/docs/getting-started/project-structure `zfb new my-site` を実行すると、`basic-blog` テンプレートが小さくも完結したプロジェクトを配置します。各ディレクトリが何のためのものかを知っておくと、これ以降のドキュメントをぐっと読み進めやすくなります。 ```text my-site/ ├── pages/ │ ├── index.tsx │ └── blog/ │ └── [slug].tsx ├── layouts/ │ └── default.tsx ├── components/ │ ├── note.tsx │ └── theme-toggle.tsx ├── content/ │ └── blog/ │ └── hello-zfb.mdx ├── styles/ │ └── global.css ├── public/ ├── zfb.config.json ├── package.json ├── tsconfig.json └── .gitignore ``` ## pages/ ファイルシステムルーティングはここに置かれます。`pages/` 配下のすべての `.tsx` ファイルがルートになります。 - `pages/index.tsx` → `/` - `pages/about.tsx` → `/about` - `pages/blog/[slug].tsx` → `/blog/:slug`(動的ルート — 解決された slug ごとに 1 つの HTML ファイル) - `pages/docs/[...slug].tsx` → キャッチオール、任意の深さにマッチ デフォルトテンプレートには index ページと 1 つの動的なブログルートが含まれます。動的ルートファイルは `paths()` 関数をエクスポートし、zfb はビルド時にそれを呼び出して `[slug]` を解決された slug ごとに 1 つの HTML ファイルへ展開します。完全な API は [Dynamic routes](/concepts/dynamic-routes) を参照してください。 ## layouts/ 再利用可能なページラッパーです。`layouts/default.tsx` はすべてのページが import するシェルで、ヘッダー・フッター・共通メタデータなどを担います。レイアウトは素の TSX コンポーネントなので、好きなように合成できます。 ## components/ 素のコンポーネントと **島** です。テンプレートには `components/note.tsx`(サーバーレンダリングされる注意書き)と `components/theme-toggle.tsx`(ダークモードを切り替える `"use client"` 島)が含まれます。ファイル先頭の `"use client"` ディレクティブこそが、コンポーネントをクライアントサイドの島に変えるものです。これがないコンポーネントはサーバー上でのみレンダリングされ、JavaScript をまったく出力しません。考え方の全体像は [islands ページ](/concepts/islands) にあります。 ## content/ コンテンツコレクションです。`content/blog/` は `blog` コレクションのシードエントリを保持します。名前付きディレクトリ内の Markdown・MDX ファイルで、ページから `getCollection("blog")` でクエリできます。コレクションスキーマは `zfb.config.ts` の `collections` 配下で宣言します。 ## styles/ グローバル CSS です。`styles/global.css` がエントリポイントで、ルートレイアウトから import します。デフォルトテンプレートは、設定で `tailwind.enabled` が指定されているときにここで Tailwind を組み込みます。 ## public/ サイトのルートからそのまま配信される静的アセットです。`favicon.ico`・`robots.txt`・SVG・ラスター画像・フォント・マニフェストファイルなど、絶対 URL で参照したいバイナリをここに置きます。このディレクトリは URL には現れません。`public/logo.svg` は `zfb dev` でも `zfb build` 後でも `/logo.svg` で到達できます。 これらのファイルは TSX・MDX・CSS から URL で参照します。 ```tsx ``` 静的アセットにバンドラスタイルの import(`import logo from "./logo.svg"`)を使わないでください。zfb は `public/` に対してアセットパイプラインを実行しません。このディレクトリはそのままのミラーであり、ファイルは相対パスに一致する URL で出力されます。 `zfb.config.ts` で `base` が設定されている場合(例: `base: "/pj/site/"`)、`public/` 内のファイルもそのプレフィックス配下で配信されます。`public/logo.svg` → `/pj/site/logo.svg` です。同じ前置がビルド時にも行われるため、プレフィックスは dev と prod で一貫します。 `public/` を使う場合と島向けに TSX import を使う場合の使い分けを含む完全なリファレンスは、[Static Assets](/concepts/static-assets) を参照してください。 ## zfb.config.json 設定ファイルは次のキーを持つ camelCase スキーマを使います。`outDir`(デフォルト `"dist"`)、`publicDir`(デフォルト `"public"`)、`host`(デフォルト `"localhost"`)、`port`(デフォルト `3000`)、`framework`(`"preact"` または `"react"`、デフォルト `"preact"`)、`collections`、`tailwind`、`plugins`。 テンプレートは `zfb.config.json` をスキャフォールドします。これを `zfb.config.ts` にリネームすると、zfb は同梱の esbuild バイナリ経由でロードし、完全な TypeScript 型と IDE 補完が得られます。スキーマはどちらの形式でも同一です。 ## package.json、tsconfig.json、.gitignore 標準的なプロジェクトの配管です。`package.json` はフレームワークのランタイム依存(デフォルトでは `preact`)と、島が import する任意のライブラリを宣言します。`tsconfig.json` は `pages/`・`layouts/`・`components/` 内の TSX がクリーンに型チェックされるよう構成されています。同梱の `.gitignore` は `dist/` と `node_modules/` を除外し、ビルド出力と依存関係をバージョン管理の外に保ちます。 --- # シンタックスハイライト > Source: https://takazudomodular.com/pj/zudo-front-builder/ja/docs/guides/syntax-highlighting このページは英語版のスタブです。完全な翻訳は別の PR で行われます。 英語版: [Syntax Highlighting](/guides/syntax-highlighting) ## 概要 zfb は [syntect](https://github.com/trishume/syntect) を使ったビルド時シンタックスハイライトを搭載しています。コードブロックは HTML に変換されてバンドルに埋め込まれるため、ブラウザへの JS は不要です。 ## カスタムテーマ(.tmTheme)の使い方 `themesDir` に `.tmTheme` ファイルを格納したディレクトリを指定することで、Dracula などの任意のテーマを利用できます。 ```ts // zfb.config.ts codeHighlight: { themesDir: "./themes", // プロジェクトルートからの相対パス theme: "Dracula", // .tmTheme ファイル内の name キーの値 }, }; ``` ディレクトリ構成: ``` my-project/ ├── themes/ │ └── dracula.tmTheme ├── pages/ ├── content/ └── zfb.config.ts ``` `themesDir` が存在しない場合や `.tmTheme` のパースに失敗した場合は、ビルド開始時に(ページの描画前に)ファイルパスを含むエラーメッセージが表示されます。 英語の最新ドキュメントは [Syntax Highlighting](/guides/syntax-highlighting) を参照してください。 --- # 静的アセット > Source: https://takazudomodular.com/pj/zudo-front-builder/ja/docs/concepts/static-assets 静的ファイル(画像・SVG・フォント・favicon・`robots.txt`・JSON マニフェスト、あらゆるバイナリ)を `public/` ディレクトリ経由で配信する方法を説明します。URL の規約、dev/prod の一致保証、ファイル名がページと衝突したときの優先順位ルール、`base` マウントプレフィックスとの相互作用、そして代わりに TSX の `import` を使うべきケースを扱います。 zfb はコード以外のアセットを 1 つのディレクトリ `public/` で扱います。ファイルを入れて絶対 URL で参照すれば、同じ URL が `zfb dev`・`zfb preview`・ビルドが出力する静的な `dist/` のいずれでも動作します。インストールするプラグインも、書くべき `import` も、壊しうるバンドラのステップもありません。 ## 規約 `public/` 内のものはすべてサイトルートでそのまま配信されます。`public` というセグメントは URL に **現れません**。 ```text public/favicon.ico → /favicon.ico public/logo.svg → /logo.svg public/robots.txt → /robots.txt public/img/hero.png → /img/hero.png public/fonts/Inter.woff2 → /fonts/Inter.woff2 ``` サブディレクトリは保持されますが、トップレベルの `public/` という名前は取り除かれます。`/img/hero.png` へのリクエストは、dev では `/public/img/hero.png` に、`zfb build` 後は `dist/img/hero.png` に解決されます。 ## アセットの参照 絶対 URL を使ってください。アセットパスはレンダリングされた HTML に現れるものと一致します。 ```tsx // pages/index.tsx return ( ); } ``` CSS でも同じ方法が使えます。URL はブラウザが最終的にリクエストするものそのものです。 ```css /* styles/global.css */ .hero { background-image: url("/img/hero.png"); } @font-face { font-family: "Inter"; src: url("/fonts/Inter.woff2") format("woff2"); } ``` ## 静的アセットをモジュールとして import しない zfb は `public/` に対してバンドラを実行 **しません**。以下のようなパターン(Vite・webpack などのツールチェーンでよく見られるもの)はここでは動作しません。 ```tsx // ❌ 静的ファイルにこれをしてはいけません。 ``` これらの import を URL に変換するアセットパイプラインは存在しません。代わりに絶対 URL の形式(`src="/logo.svg"`)を使ってください。import は **コード**(islands が使う `.ts`・`.tsx`・`.css` モジュール)には依然として正しい答えですが、ブラウザにそのまま取得させたい画像・フォント・SVG のようなバイナリファイルには適しません。 CSS がストロークや塗りなどをスタイルできるよう SVG を JSX としてインライン化する必要が本当にある場合は、SVG マークアップを TSX コンポーネントにコピーしてください。それはコードの経路です。`public/` はバイト単位そのままの経路です。 ## dev / prod の一致 dev サーバーとプロダクションビルドは URL の形について一致します。これは偶然ではなく保証です。 - **`zfb dev`** — ページハンドラは、ページキャッシュのミスと `dist/` のミスのあと、`/` からの読み取りにフォールバックします。`public/` ディレクトリには URL プレフィックスもトップレベルの `nest_service` マウントもなく、ファイルはサイトルートに直接現れます。 - **`zfb build`** — `copy_public_dir`(`crates/zfb/src/commands/build.rs` 内)が `public/` 配下のすべてのファイルを `dist/` へ再帰的にコピーします。エッジ CDN が配信する静的な `dist/` ツリーは、dev でブラウザが見たものと同じ形です。 つまり、ページに一度書いた `` は、条件分岐・環境チェック・`withBase` 風のヘルパーなしで両モードで動作します。 ## 優先順位: ページが public ファイルに優先する `pages/foo.tsx` ルートと同じ URL を持つ `public/foo` ファイルを持つことは可能です(通常は意図しないものですが)。zfb はこれを決定論的に解決します。 1. **プラグインの dev ミドルウェア** で `/foo` を主張するものが最初に実行されます。 2. **ページキャッシュ** — `pages/foo.tsx` のレンダリング出力が次に優先されます。 3. **`dist/` ディレクトリ** — ビルドパイプラインが書き出したファイルが次に配信されます。 4. **`public/` ディレクトリ** — 上記 3 つすべてがミスした場合にのみ参照されます。 5. それ以外は **404**。 したがって、同名の TSX ページは常に public ファイルを覆い隠します。逆は不可能です。`public/foo` がルートを上書きすることはできません。ページも主張する URL に静的ファイルを置きたい場合は、どちらか一方をリネームしてください。 ## `base` との相互作用 `zfb.config.ts` が `base` プレフィックス(例: サブパス配下へのデプロイのための `base: "/pj/site/"`)を設定すると、`public/` 内のファイルもそのプレフィックスの下へ移動します。 ```text config: base: "/pj/site/" public/logo.svg → /pj/site/logo.svg (dev と prod) ``` dev サーバーの `serve_page` フォールバックも、ビルド時の `copy_public_dir` も、このプレフィックスを尊重します。プロジェクトの他の部分と同じやり方で HTML 内のアセット URL を書いている限り(通常は markdown / TSX パイプラインがすでに実行しているリンクリライターを経由して)、プレフィックスは自動的に適用されます。 ## 設定 このディレクトリは設定可能です。デフォルト以外を指すには `zfb.config.ts` に `publicDir` を追加します。 ```ts // zfb.config.ts publicDir: "static", }); ``` デフォルト: `"public"`。パスはプロジェクトルートからの相対で解決されます。ディレクトリが存在しない場合は黙って no-op になります。すべてのプロジェクトに必要なわけではありません。 ## `public/` に入れないもの `public/` が適した置き場所: - サイト全体のアイコンと favicon(`favicon.ico`・`apple-touch-icon.png`) - Open Graph / ソーシャルシェア用画像 - `robots.txt`・`humans.txt`・security.txt - Web アプリマニフェスト(`manifest.webmanifest`) - 自前でホストするフォント - 多くのページから絶対 URL で参照される装飾的な画像 `public/` が適さない置き場所: - **変換するソース画像**(リサイズ・最適化・AVIF/WebP への変換)。zfb には組み込みの画像パイプラインがありません。変換が必要なら帯域外で実行し(例: `prebuild` スクリプト経由)、最適化された出力を `public/` にチェックインするか、別のツールを使ってください。 - **islands のコード依存**。`"use client"` 島がインポートする TSX / JSX / TS / CSS は、島の隣に置いてバンドルすべきです。コードを `public/` に置くとバンドラを完全にスキップしてしまい、ブラウザはランタイムが実行できない生のソースを取得することになります。 - **拡張子が示すものと異なる `Content-Type` が必要なファイル**。zfb は Content-Type をファイル拡張子から導出します。オーバーライドが必要なら、代わりに TSX ページ経由でファイルをレンダリングしてください([Non-HTML Pages](/concepts/non-html-pages) を参照)。 ## 関連 - [Project structure: `public/`](/getting-started/project-structure) — ディレクトリレイアウトの概観。 - [Non-HTML Pages](/concepts/non-html-pages) — ヘッダーを制御したい場合や、ページをコレクションデータに依存させたい場合に、`.xml`・`.json`・`.txt` を TSX ページ経由でレンダリングする方法。 - [Islands](/concepts/islands) — クライアントサイド JS の経路。ここで説明した静的アセットの経路とは別物です。 --- # meta export > Source: https://takazudomodular.com/pj/zudo-front-builder/ja/docs/api/meta-export ## パターン すべてのページモジュールは、ページの `` データ(タイトル、説明、Open Graph タグ、正規 URL など)を記述する `meta` 定数(または同じレコードを返す async 関数)をエクスポートできます。レンダラはこのエクスポートを `RenderHost::get_export` 経由で読み取り、ページレイアウトの `` ブロックにマージします。 ## 例 ```tsx title: "Blog — My Site", description: "Latest writing on web development.", canonical: "https://example.com/blog", og: { image: "/og/blog.png", type: "website", }, }; return {/* ... */}; } ``` meta を計算するためにコレクションや async ソースからのデータが必要な場合(例: CMS フィールドから導出する OG 画像)は、`meta` を async 関数としてエクスポートします。レンダラはページシェルをレンダリングする前にそれを await します。 ```tsx const post = getCollection("blog")[0]; return { title: post.data.title, og: { image: `/og/${post.slug}.png` }, }; } ``` ## サポートされるキー レンダラは `meta` を寛容なバッグとして扱いますが、ページレイアウトが参照するサポート対象のサブセットは以下のキーです。 - `title?: string` — このページについてレイアウトのデフォルトを上書きします。 - `description?: string` — `` タグに使われ、OG のフォールバックにもなります。 - `canonical?: string` — `` 用の絶対 URL。 - `og?: { title?, description?, image?, type?, url? }` — Open Graph タグ。欠けているフィールドは、意味が通る範囲でトップレベルの `title` / `description` にフォールバックします。 - `twitter?: { card?, site?, creator?, image? }` — Twitter Card タグ。 - `layout?: string` — ページレイアウトコンポーネントのモジュール specifier(相対パスまたはベア specifier)。指定がない場合はプロジェクトのデフォルトレイアウトが使われます。 未知のキーはそのまま素通りするため、プラグインやカスタムレイアウトコンポーネント向けの構造化データを格納しておけます。 --- # Islands > Source: https://takazudomodular.com/pj/zudo-front-builder/ja/docs/concepts/islands zfb のページはデフォルトで静的 HTML にレンダリングされます。**Islands(島)** はその抜け道です。ブラウザに JavaScript を配信してクライアントでハイドレートする小さなコンポーネントで、ページの残りの部分はプレーンな HTML のままに保たれます。 メンタルモデルは単純です。ページの大部分は静的なドキュメントです。いくつかのインタラクティブな要素(カウンター、検索ボックス、テーマ切り替え)は、そのドキュメントに埋め込まれた島であり、それぞれが独立してロードされハイドレートされます。 ## 島とは何か `.tsx` ファイルの先頭に `"use client"` ディレクティブを追加します。 ```tsx "use client"; const [count, setCount] = useState(0); return ( setCount(count + 1)}> Count: {count} ); } ``` この 1 つのディレクティブが opt-in のすべてです。これを持たないファイルは純粋なサーバーコンポーネントです。ビルド時に一度レンダリングされるだけで、ブラウザには決して届きません。 [zfb-example-blog のスタンドアロンリポジトリ](https://github.com/Takazudo/zfb-example-blog) にある `theme-toggle.tsx` コンポーネントは、典型的な実例です。`localStorage` と `matchMedia` を読み取り、自身の状態を管理し、アクティブなテーマを `document.documentElement.dataset.theme` にミラーします。鍵となるパターンは、初回ペイントでは決定論的で SSR セーフなデフォルトをレンダリングし、その後 `useEffect` 内でユーザーの設定に同期する点です。 ```tsx "use client"; type Theme = "light" | "dark"; // 決定論的で SSR セーフなデフォルト。実際の設定は useEffect で適用される。 const [theme, setTheme] = useState("light"); useEffect(() => { const saved = window.localStorage.getItem("theme"); if (saved === "light" || saved === "dark") setTheme(saved); }, []); const next: Theme = theme === "dark" ? "light" : "dark"; return ( setTheme(next)} > {theme === "dark" ? "Light mode" : "Dark mode"} ); } ``` ## N 個の島に対して esbuild が生成するもの 3 つの島(`Counter`・`ThemeToggle`・`SearchBox`)を持つプロジェクトの場合、islands のビルドステップは **4 つのファイル** を出力します。 ``` dist/islands/Counter.js dist/islands/ThemeToggle.js dist/islands/SearchBox.js dist/islands/islands-runtime.js ``` 島ごとのバンドルが 3 つ(コンポーネントごとに 1 つ)、それにハイドレーションを駆動する共有ランタイムバンドルが 1 つです。島ごとの各ファイルは自己完結した ESM モジュールです。コンポーネントとフレームワーク固有のハイドレーション接着コード(例: Preact の `hydrate`)をインポートし、何もエクスポートしません。島同士の共有は各バンドル内の esbuild レベルで行われ、ランタイム自体は別のバンドルです。 **ファイル名は安定しています** — 名前にコンテンツハッシュは含まれません。`ProductionAssetPipeline` がハッシュ化の単一の真実の源であり、出力された HTML 内の URL の書き換えを処理します。バンドラは予測可能なパスを書き出すため、下流の処理が推測する必要はありません。 ## 島がどうロードされるか レンダリング後、各島のサーバーレンダリングされた HTML は、メタデータを持つ `` でラップされます。 ```html Dark mode ``` 少なくとも 1 つの島を持つページには、**1 つの `` タグ** が `` に注入されます。 ```html ``` ランタイム(`islands-runtime.js`)はページ上のすべての `[data-zfb-island]` 要素を走査します。見つけた各要素について、対応する島ごとのバンドル(`data-zfb-island="ThemeToggle"` に対しては `/islands/ThemeToggle.js`)を `dynamic-import()` し、シリアライズされた `data-props` を読み取り、既存のサーバーレンダリング済み DOM に対して `hydrate()` を呼び出します。 ## dynamic-import 設計の帰結 ランタイムは `import()` を通じて島を遅延的に解決するため、次のことが言えます。 - **ページが実際に使う島だけが取得されます。** `ThemeToggle` を使うページは `Counter.js` も `SearchBox.js` もダウンロードしません。 - **島のないページにはランタイムが入りません。** ページのレンダリングツリーに `"use client"` コンポーネントが含まれない場合、ビルドパイプラインは `` の注入を完全にスキップします。完全に静的なページには JavaScript が 1 バイトも届きません。 - **1 つのページに新しい島を追加しても他のページには影響しません。** 各ページの HTML は独立しており、島は名前で取得されるため、ホームページに `SearchBox` を追加しても他のすべてのページの HTML やネットワークトラフィックは変わりません。 安定したファイル名がこれを補強します。CI デプロイが `dist/islands/` ディレクトリに `SearchBox.js` を追加しても、既存のすべての `*.js` ファイルはバイト単位で同一のまま保たれ、ブラウザのキャッシュは有効なままです。 ## フレームワークの選択 zfb は島について 2 つのフレームワークをサポートしています。 | Config value | Runtime | |---|---| | `"preact"`(デフォルト) | Preact + `preact/jsx-runtime` | | `"react"` | React 18 + `react-dom/client` | `zfb.config.ts` で一度だけ設定します。 ```ts framework: "preact", // または "react" }; ``` これは **プロジェクト全体の設定** です。プロジェクトごとに 1 つのフレームワークです。バンドラ(`crates/zfb-islands` の `FrameworkKind` enum)が、その選択を JSX 変換オプション(`--jsx-import-source`)と各島エントリをラップするハイドレーション接着コードに通します。同じプロジェクト内で Preact の島と React の島を混在させることはできません。 zfb は Vue・Svelte・Solid をサポートしていません。`FrameworkKind` enum は意図的に 2 バリアントの enum であり、プラグインポイントではありません。別のフレームワークが必要な場合は、以下の抜け道が一般的なケースをカバーします。 2 つのアダプタがどう動作するかの詳細は [Framework adapters](/architecture/framework-adapters) を参照してください。 ## 島ではないクライアント JS のための抜け道 島はステートフルな UI コンポーネントをカバーします。それ以外のクライアントサイド JavaScript のニーズには、標準的な HTML の仕組みを直接使ってください。 **インラインスクリプト** — ページの TSX やレイアウトに直接 `` タグを書きます。 ```tsx return ( {children} ); } ``` これは、スタイルシートのパース前に実行されなければならない同期的なハイドレーション前の処理(FOUC 防止、テーマ初期化、アナリティクスのセットアップ)に適したツールです。 **外部スクリプト** — `public/` または CDN の任意の `.js` ファイルを参照します。 ```tsx ``` **カスタムビルドステップ** — 追加の TypeScript モジュールをバンドルする必要がある場合は、ビルドパイプラインに別の esbuild または Rollup のステップを追加し、その出力を `` から参照します。zfb は `"use client"` でない任意のモジュールを自動バンドルしません。自動で実行されるのは islands パイプラインだけです。 **できないこと**: 通常の(`"use client"` でない)`.ts` や `.tsx` モジュールをページからインポートして、そのブラウザ側のコードがクライアントに届くことを期待すること。ディレクティブを持たないモジュールはサーバー専用です。SWC がビルド時にコンパイルして評価し、そのバイトは出力に一切含まれません。 ## 島を使わないほうがよいとき 島は JavaScript を配信します。それにはコストがあります。追加する前に、それなしで問題を解決できないか自問してください。 **DOM のクラス入れ替えトグル**(アコーディオン、ディスクロージャーメニュー、表示・非表示パネル)は、数行のプレーンな CSS や小さなインラインの `` で解決できることがよくあります。ネイティブの `` / `` 要素は JavaScript なしでアコーディオンの挙動を扱います。 ```html Frequently asked question The answer goes here. ``` CSS のみのアプローチ(`:target`・`:checked` + ``・`@starting-style`)は、かつて JavaScript を必要とした多くのインタラクティブなパターンを扱えます。 **島が適したツールとなるのは次の場合です。** - コンポーネントが、単一のインタラクションを超えて存続しなければならない状態を持つ場合(例: カート、ユーザーセッション、複数ステップのフォーム)。 - コンポーネントがビルド時には利用できないブラウザ API に依存する場合(`canvas`・`WebGL`・`getUserMedia`・リアルタイムデータ)。 - そうでなければコンポーネントを 2 回(サーバーレンダリング用に 1 回、クライアント用に 1 回)書いて手動で同期し続けることになる場合。 正直な答えが「クリックでクラスを 1 つ切り替えたいだけ」なら、まず CSS か小さなインラインスクリプトに手を伸ばしてください。「2 回書くことになるステートフルな UI」に島を、というのが正しい判断基準です。 パイプラインの形についてさらに詳しくは [Build pipeline](/concepts/build-pipeline) と [Build engine](/architecture/build-engine) を参照してください。 --- # HTML 以外のページ > Source: https://takazudomodular.com/pj/zudo-front-builder/ja/docs/concepts/non-html-pages このページは英語版のスタブです。完全な翻訳は別の PR で行われます。 英語版: [Non-HTML Pages](/concepts/non-html-pages) ## 概要 `sitemap.xml.tsx` のようなファイル名規約と `frontmatter.extension` を使って、HTML 以外の出力(XML、テキストなど)を生成するしくみについて解説します。 英語の最新ドキュメントは [Non-HTML Pages](/concepts/non-html-pages) を参照してください。 --- # ビルドパイプライン > Source: https://takazudomodular.com/pj/zudo-front-builder/ja/docs/concepts/build-pipeline zfb は、それぞれが 1 つの役割を担う小さな Rust クレート群として作られています。このページは、 「どのクレートが何を、どの順序で行うか」というレベルでパイプライン全体をめぐるツアーです。 個々の部品に深入りすることなくメンタルマップを得るのに十分な内容です。 ## クレート、順番に 1. **CLI** — `crates/zfb`。コマンドライン引数をパースし、`zfb.config.{ts,json}` をロードし、`dev`・`build`・`preview` のいずれかのコマンドにディスパッチします。 2. **Router** — `crates/zfb-router`。`pages/` をスキャンしてルートテーブルを生成します。[Routing](/concepts/routing) を参照してください。 3. **Watcher** — `crates/zfb-watcher`。dev モード専用。`pages/`・`content/`・`components/`・`layouts/`・`styles/`・`data/`・`public/`、そして `zfb.config.ts` を監視し、変更イベントのストリームを発行します。 4. **Dependency graph** — `crates/zfb-graph`。ページとソースの依存関係を追跡します。ウォッチャーが変更を報告すると、グラフは「どのページをリビルドする必要があるか」という 1 つの問いに答えます。 5. **Build orchestrator** — `crates/zfb-build`。ページ単位の作業を調整し、`atomic_write_string` 経由で出力をアトミックに書き込むため、書き込みの途中で `dist/` が中途半端に壊れた状態になることは決してありません。 6. **Renderer** — `crates/zfb-render`。SWC で TSX を JS にコンパイルし、得られた ES モジュールを JS ランタイムホスト経由で評価し、ページの `default()` エクスポートを呼び出して HTML 文字列を生成します。コンテンツコレクションのエントリ(`.md` / `.mdx`)は JSX モジュールと同じパイプラインを通してコンパイルされます。レンダラは `mdx:///` の specifier を解決し、それらを `entry.Content` としてユーザーページに提供します([MDX Components](/concepts/mdx-components) を参照)。 7. **CSS pipeline** — `crates/zfb-css`。必要に応じて Tailwind v4 と PostCSS を実行します。[Styling](/concepts/styling) を参照してください。 8. **Islands pipeline** — `crates/zfb-islands`。各 `"use client"` コンポーネントを esbuild 経由で個別の ESM モジュールとしてバンドルします。[Islands](/concepts/islands) を参照してください。 9. **Server** — `crates/zfb-server`。dev モード専用。ページキャッシュを静的ファイルとして配信する axum ベースの HTTP サーバーに加え、ブラウザリロードイベント用に `/__zfb/reload` で SSE チャネルを提供します(イベント種別の完全な内訳は [Dev mode lifecycle](/concepts/dev-mode-lifecycle) を参照)。 ## 本番: `zfb build` `zfb build` はパイプラインを一度だけ実行し、完全なサイトを `outDir` に書き出します。 CLI → Router → Graph → Build orchestrator → Renderer → CSS pipeline → Islands pipeline。 ウォッチャーと dev サーバーは関与しません。すべてのページがレンダリングされ、すべての CSS バンドルが生成され、すべてのアイランドがバンドルされ、その結果が `dist/`(または `outDir` が指す場所)に配置されます。アトミック書き込みにより、ビルドが中断されても、半分上書きされた ぐちゃぐちゃの状態ではなく、以前の `dist/` がそのまま残ります。`prerender = false` の ルートについては、アダプタが静的 HTML の代わりにワーカーバンドルを出力します。その出力が どう構成されるかは [SSR on a Worker (adapter mode)](/concepts/ssr-on-a-worker) を 参照してください。 ## 開発: `zfb dev` `zfb dev` は `zfb build` と同じパイプラインを長命のループとして実行し、加えてウォッチャーと サーバーを動かします。 CLI → Router → Watcher → Build orchestrator → Renderer → CSS pipeline → Islands pipeline → Server。 ウォッチャーが変更を報告すると、依存グラフが影響を受けるページを選び、オーケストレーターは それらだけをリビルドし、サーバーは SSE チャネル経由でリロードイベントをブロードキャストします。 ブラウザは自動的に再接続してリロードします。 ## プレビュー: `zfb preview` `zfb preview` は 3 つのコマンドの中で最もシンプルです。既存の `dist/` ディレクトリを `:4321` で静的ファイルとして配信します。レンダリングもウォッチングもリビルドもありません。デプロイ前に 本番ビルドをローカルで確認できるように存在しています。 ## なぜ分割するのか クレートの境界は偶然ではありません。ルーティングはレンダリングとは別の問題であり、レンダリングは ウォッチングとは別、ウォッチングは配信とは別です。これらを分割することで、各クレートは単体で テストできるくらい小さく保たれ、dev ループは本番経路に触れることなく最適化(インクリメンタル リビルド、ページキャッシュ、部分的な CSS 抽出)を差し込めます。より踏み込んだ理由付けは [/architecture/build-engine](/architecture/build-engine) にあります。 --- # デスクトップ展開 > Source: https://takazudomodular.com/pj/zudo-front-builder/ja/docs/guides/desktop-deployment このガイドでは、zfb サイトをデスクトップアプリに組み込みたい場合に、現時点で現実的に何ができるかを解説します。zfb と Tauri を組み合わせる構成には 4 つの異なるモードがあり、それぞれ異なるプロジェクトに対する正解です。意図を持って選択してください。 - **モード A** は、コンテンツがビルド時に作成され、各リリースに同梱される場合に最適です。 - **モード B** は、Tauri 側のコードを最小限に抑えつつ zfb のランタイムの動的機能をフルに使いたい場合に最適です。 - **モード C** は、アプリの動的な部分を、zfb をプロセスに含めずに Rust で実現できる場合に最適です。 - **モード D** は、インプロセス埋め込み API が提供されたあとの将来のモードです。 4 つのモードすべてを支配する中核的な不変条件: > **ビルド時 = Node.js が必須。ランタイム = Node.js は一切不要。** zfb の静的な出力は意図的にランタイム非依存に設計されています。一方、ビルドツールはそうではありません。この境界に逆らうのではなく、この境界を前提にアーキテクチャを設計してください。 ## モード A — `dist/` のみを同梱する **概要。** `zfb build` は `dist/` に完全に静的なサイト(HTML・CSS・JavaScript ファイルで、ランタイムにサーバーサイドの依存を持たない)を生成します。デスクトップアプリは、フレームワーク組み込みのアセットサーバーを使ってそのディレクトリを直接配信できます。zfb はランタイムでは見えず、パッケージされたアプリはディスク上のただのファイルです。 **選ぶべきとき。** ドキュメントアプリ、ヘルプシステム、そしてコンテンツがビルド時に作成されリリースに同梱されるあらゆるデスクトップアプリ。ユーザーが実行中のアプリ内でコンテンツを編集する必要がないなら、モード A が正解です。 **必要な作業。** Tauri の場合、関連する `tauri.conf.json` のキーは次のとおりです: ```json { "build": { "distDir": "../dist", "devPath": "http://localhost:4321" } } ``` `distDir` を `dist/` フォルダに向けます(相対パスはプロジェクト構成に合わせて調整してください)。残りは Tauri 組み込みの静的ファイルサーバーが処理します。パッケージされたアプリに Node.js プロセスや HTTP サーバーは不要です。 1. 開発マシン(または CI)で `zfb build` を実行する。 2. 生成された `dist/` を Tauri アプリのバンドルに含める。 3. 出荷する。完了です。 アセット配信のオプション一式(カスタムプロトコルやセキュリティポリシーを含む)については、[Tauri `tauri.conf.json` リファレンス](https://tauri.app/v1/api/config/)を参照してください。 **トレードオフ。** コンテンツの更新には新しいアプリのリリースが必要です。新しいビルドなしにユーザーが最新のコンテンツを見られる仕組みはありません。 --- ## モード B — zfb バイナリを Tauri のサイドカーとして動かす **概要。** Tauri が `zfb` バイナリを子プロセスとして起動し、WebView は `localhost:` を指します。zfb はその子プロセスの中で、リクエスト時の MDX レンダリングやリソース探索などを処理します。サイドカーのバイナリは Rust であり、パッケージされたアプリに Node.js は不要です。 **選ぶべきとき。** zfb の動的機能をフルに使いたいが(`prerender = false` のルート、リクエスト時の MDX レンダリング、リビルドなしのコンテンツ再読み込み)、zfb サーバーを Tauri プロセス自体の中に置く必要はない、という場合。 **必要な作業。** [Tauri サイドカー](https://v2.tauri.app/develop/sidecar/)として `zfb` バイナリを Tauri アプリと並べて同梱します。Tauri フロントエンドがサイドカーを起動し、ポートを発見し、WebView を `http://127.0.0.1:` に開けるよう、薄い IPC ブリッジを書きます。サイドカーと Tauri の境界はクリーンですが、統合のためのつなぎ込みは標準では提供されません。 **トレードオフ。** 追加で可動部品を抱えることになります(子プロセスのライフサイクル管理、ポート発見、Tauri とサイドカー間の IPC 配線)。その代わり、Node.js 依存なしで zfb のフルなレンダリングパイプラインをランタイムで利用できます。 --- ## モード C — zfb をビルド時のみ使うカスタム Rust クレート **概要。** zfb はビルド時に一度だけ Preact のシェルを `dist/` にコンパイルします。それ以降のランタイムの動的機能(リソース探索、Markdown レンダリング、配信)はすべて手書きの Rust コードであり、典型的にはディスクからファイルを読み込み、事前ビルドしたシェルにコンテンツを差し込む Axum サーバーです。zfb はランタイムには一切存在せず、純粋にビルド時のツールです。 **選ぶべきとき。** アプリの動的な部分が Markdown レンダリングか、あるいは素直な Rust ライブラリですでにうまく扱える何かであり、zfb のフルなレンダリングパイプラインをバイナリに持ち込むより、焦点を絞った Rust サーバーコードを書きたい場合。可能な限り小さいランタイムフットプリントを求めている場合です。 **必要な作業。** `zfb build` で Preact のシェルをビルドします。続いて、リクエスト時に Markdown ファイルを読み込み、Rust の Markdown クレートでレンダリングし、その結果をセンチネル置換などのパターンで静的シェルに差し込む、専用の Rust サーバー(または既存の Tauri アプリへの Axum ルートの追加)を書きます。[CCResDoc](https://github.com/Takazudo/ccresdoc) はこのパターンの具体的で動作する実例です。zfb が一度だけ Preact のシェルをコンパイルし、手書きの Rust バックエンドがランタイムにコンテンツを配信します。 **トレードオフ。** より多くの Rust のつなぎコードを書くことになり、ランタイムでの TSX レベルのページコンポーネントを諦めることになります。その代わり、可能な限り小さいバイナリ、ランタイム動作の完全な制御、そしてパッケージされたアプリでの V8 依存ゼロを得られます。 --- ## モード D — zfb を Rust クレートとして Tauri 内に埋め込む(将来) **概要。** Tauri の `setup` フックが、Tauri プロセス内の Tokio スレッド上で zfb サーバーを起動します。子プロセスもポート管理もなく、`prerender = false` のルートや Tauri IPC 呼び出しを Rust 対 Rust のインプロセス受け渡しで処理します。WebView はネットワークポートを介さず、埋め込まれたサーバーと直接やり取りします。 **選ぶべきとき(提供されたら)。** zfb のランタイムの動的機能と緊密な Tauri 統合(IPC、ファイルシステムアクセス、コンテンツのホットリロード)を、モード B のサイドカーのライフサイクル配線なしに使いたい場合。 **必要な作業。** 埋め込み API はまだ存在しません。提供される際には、`zfb-core` などのクレートに依存し、Tauri の `setup` フックで初期化関数を呼び出すことが必要になる見込みです。正確な API 表面は現在も設計中です。 **このモードは現時点では動作しません。** 進捗は次のリサーチイシューで追跡してください: [https://github.com/Takazudo/zudo-front-builder/issues/346](https://github.com/Takazudo/zudo-front-builder/issues/346) **トレードオフ。** 提供されたあとは、サイドカーの配線なし、より緊密な IPC、よりクリーンなパッケージング。提供されるまでは、選択肢になりません。 --- ## より難しいケース: アプリ内でコンテンツをリビルドする ユーザーがローカルで Markdown ファイルを編集し、実行中のアプリ内でライブプレビューを見られるようにする必要がある場合(要するにパッケージされたウィンドウ内で `zfb dev` を動かす場合)、正しいアプローチは必要なものによって変わります。 - フルな MDX レンダリングが必要なら、**モード B**(サイドカー)が今日の最善の道です。サイドカーがファイル監視 / インクリメンタルリビルドのループをバックグラウンドで動かし続け、WebView はアプリの再起動なしに変更を反映します。 - **モード D**(インプロセス埋め込み)は、提供されたあとに望ましい道になります。子プロセスもポートもなく、Tauri IPC とファイルシステムとの統合がより緊密です。 - **モード A や C** はここでは役に立ちません。どちらもパッケージされたアプリが動く時点でコンテンツが静的であることを前提にしているためです。 ランタイムで Node.js が必要な場合(たとえば Node.js 依存を持つカスタム `zfb` プラグインを動かす場合)は、代わりに Electron を検討してください。**Electron** は Node.js を埋め込むため、`zfb dev` はメインプロセス内で自然に動作します。その代償として、バイナリサイズが大幅に増加し、パッケージングがより複雑になります。これは zfb のモードそのものではなく、ランタイムでの Node.js 要件によって決まるフレームワークの選択です。 --- ## Electron・Wails などのデスクトップフレームワークはどうか どのデスクトップフレームワークを使っても、話は同じです。 - **Electron** — `dist/` は静的アセットディレクトリとして機能します(`loadFile()` か `file://` プロトコルハンドラを使用)。Electron は Node.js を埋め込むためメインプロセス内で `zfb dev` を動かすことは可能ですが、それは zfb のビルドツールチェーン一式をアプリと一緒に同梱することを意味します。 - **Wails** — WebView を埋め込み、埋め込み Go サーバーからアセットを配信します。`dist/` ディレクトリに向けてください。ビルド時の Node.js 要件は上記と同じです。 - **Neutralino・Tauri、その他**組み込みの静的ファイルサーバーを持つフレームワーク — `dist/` を同梱すれば、Node へのランタイム依存はありません。 `dist/` の出力はただのファイルです。ディスクからファイルを配信できるフレームワークなら、どれでも機能します。 ## Tauri 固有のヒント Tauri 統合についての実践的なメモをいくつか。 **アセットソースとしての `dist/`。** `distDir` を zfb が生成する `dist/` ディレクトリに向けて設定します。開発中は `devPath` を `http://localhost:4321`(デフォルトの `zfb dev` ポート)に設定し、古いスナップショットではなくライブの開発サーバーから Tauri がロードするようにします。 **コンテンツセキュリティポリシー(CSP)。** Tauri のデフォルト CSP はインラインスクリプトをブロックすることがあります。zfb のアイランドハイドレーションはインラインの `` タグを使用します。アプリでアイランドを使う場合は、`tauri.conf.json` の CSP ポリシーを緩和または拡張してください。 **ファイルパス。** `zfb build` はデフォルトで絶対ルート相対のパス(`/assets/main.css`)を生成します。`tauri://` プロトコルを使う場合、Tauri のカスタムプロトコルがこれらを正しく書き換えます。`file://` を直接使う場合は、`zfb.config.ts` の `base` を相対パスに設定する必要があるかもしれません。 ## 関連項目 - [Build pipeline](/concepts/build-pipeline) — `zfb build` がどのように `dist/` を生成し、各クレートが出力に何を寄与するか。 - [Architecture: build engine](/architecture/build-engine) — クレートの分割と、`dist/` への書き込みがアトミックである理由。 - [Installation](/getting-started/installation) — ビルドおよび開発ツールの Node.js 要件はここに記載されています。 --- # インクリメンタルリビルド > Source: https://takazudomodular.com/pj/zudo-front-builder/ja/docs/concepts/incremental-rebuild 1 つのファイルを編集すると、それに依存するページだけがリビルドされます。このページでは、その保証がどう機能するか、グラフが何を追跡するか、効果が本物なのはどこか、そして現状のコストがどこにあるかを説明します。 ## 約束 `zfb dev` 中に `content/blog/post-3.md` を保存したとき、zfb はサイトのすべてのページをリビルドするわけではありません。依存グラフにそのファイルをインポートしたページを問い合わせ、それらだけを再レンダリングします。コンテンツ中心のブログでは通常、1〜2 ページです。 具体例: `blog` コレクションを消費するブログインデックス(`pages/blog/index.tsx`)と投稿ごとのページ(`pages/blog/[slug].tsx`)を持つ 500 ページのサイト。`content/blog/post-3.md` を保存すると、`pages/blog/index.tsx`(全投稿を一覧)とその slug をレンダリングする単一の投稿ページがダーティになります。500 ページではなく 2 ページです。 グラフはこれを厳密にします。ヒューリスティックでもキャッシュ無効化のタイムアウトでもありません。各ページは、記録された依存関係が変わった場合に限り、ちょうどそのときだけリビルドされます。 ## zfb-graph が追跡するもの `crates/zfb-graph` は 2 層の依存情報を保持します。 **ソースレベルの依存(ページ単位):** 各ページには `PageId`(ソースの `.tsx` パス)が割り当てられます。グラフは、そのページが依存するすべてのファイルを `DepKind` でタグ付けして記録します。 | Kind | Examples | |---|---| | `Module` | TSX/TS のレイアウト、コンポーネント、lib ファイル | | `Content` | コンテンツコレクション経由の Markdown/MDX エントリ | | `Style` | CSS ソース、CSS モジュール | | `Data` | JSON/TOML/YAML データモジュール | | `Asset` | `public/` 配下の静的ファイル | 逆引きインデックスは、すべての依存パスを、それを消費するページの集合へとマッピングし返します。これが `dirty_pages()` が問い合わせるインデックスです。 **アセットレベルの依存(ページ単位):** ソースグラフと並んで、各ページは `AssetDeps` レコードも保持します。 - `islands` — ページがハイドレートするすべての `"use client"` 島の安定したコンポーネント識別子。 - `css_modules` — ページがインポートする CSS-Modules のソースパス。 この第 2 の層は別の問いに答えます。「どのページが再レンダリングされるか?」ではなく「どのアセットバンドルが再出力されるか?」です。`"use client"` コンポーネントへの変更は、JS を配信するすべてのページを再レンダリングすることなく、どのページスコープの islands バンドルを無効化すべきかをグラフに伝えます。 グラフは成功した各レンダリングのあとに両方の層を更新するため、次の問い合わせは常に最新の状態を反映します。 ## DirtySet ファイルが変更されると、ビルドオーケストレーターは `DependencyGraph::dirty_pages(path)`(または変更が連続するときのバッチ版 `dirty_pages_batch`)を呼び出します。戻り値の型は `DirtySet` です。 ```rust pub enum DirtySet { /// Rebuild every page. Triggered by global files like zfb.config.ts. All, /// Rebuild exactly this set of pages. May be empty. Specific(BTreeSet), } ``` **`DirtySet::Specific`** が一般的なケースです。グラフは O(consumers) の時間で逆引きインデックスのルックアップを行い、変更されたパスをインポートしたページだけを返します。 **`DirtySet::All`** は最終手段です。`DependencyGraph::mark_global()` 経由で登録されたファイルは、記録されたエッジに関係なく常に `All` を返します。`zfb.config.ts` はデフォルトでグローバル登録されています。他の候補にはトップレベルの `_app.tsx` や `_document.tsx` など、その変更が意味的にすべてのページに影響するものが含まれます。 未知のパス(グラフが依存関係として一度も見たことのないファイル)は空の `DirtySet::Specific` を返します。リビルドするものはなく、何も起こりません。 ## dev パイプラインがダーティセットで行うこと オーケストレーター(`crates/zfb-build`)は `DirtySet` を受け取り、それを dev パイプライン(`DevAssetPipeline`)に流し込みます。 1. **ダーティなページだけを再レンダリングする。** パイプラインは `DirtySet::Specific` 内のページに対してレンダラを呼び出します。集合に含まれないページは手つかずのままです。 2. **HTML がバイト単位で同一なら書き込みをスキップする。** レンダリング後、各ページの新しい HTML が直前に判明しているバイト列と比較されます。バイトが一致すれば、ファイルは書き込まれず、リロードシグナルにもページは含まれません。意味的な HTML 変更を生まない純粋なリファクタリングは、ブラウザのリロードを生みません。 3. **消費するページが変わったときだけ islands を再バンドルする。** islands サブパイプラインは、プランの `rerun_islands` フラグが立っているときだけ実行されます。これをオーケストレーターが立てるのは、変更されたファイルが islands ルート(例: `components/`)の内部にあるときだけです。コンテンツのみの変更は islands の再バンドルをトリガーしません。 4. **ファイル名を安定に保つ。** dev モードの出力ファイルは安定した名前を使います(コンテンツハッシュなし)。ブラウザのキャッシュコントラクトはウォッチャーのティック間で変わりません。コンテンツハッシュ化はプロダクションパイプラインの仕事です。 総合的な効果は次のとおりです。2 ページに触れるコンテンツ編集は、2 回の HTML 書き込み、1 つのライブリロードイベントを生み、島のバンドリングは行いません。ヘッダーコンポーネントの編集は、ヘッダーをインポートするすべてのページに書き込みを生みますが、それでもそれらのページだけです。SSE イベントの種類(`Page`・`Css`・`Islands`)の全体像と、ブラウザがそれぞれにどう反応するかについては [Dev mode lifecycle](/concepts/dev-mode-lifecycle) を参照してください。 オーケストレーターの設計をより深く知るには [Build engine](/architecture/build-engine) を参照してください。 ## Astro の dev モデルとの比較 Astro は内部で Vite を使います。Vite の hot module replacement(HMR)はモジュールを無効化し、モジュールのインポートグラフを辿って影響を受ける ESM の境界を見つけ、WebSocket 経由でブラウザに更新をプッシュします。集中管理された Content Layer はパース済みのコンテンツを SQLite ストアにキャッシュし、コレクションの変更時にリロードします。 zfb のモデルは粒度のレベルが異なります。 - **Astro の無効化の単位は ES モジュールです。** 変更はモジュールを無効化し、バンドラがモジュールグラフに基づいてどのチャンクを再出力するかを決めます。 - **zfb の無効化の単位はページです。** 依存グラフはモジュールではなくレンダリング出力をキーとするため、共有コンポーネントの変更は、インポートのクロージャ全体ではなく、ちょうど HTML を生成するページだけをダーティにします。 コンテンツ中心のサイトではこの違いが効きます。Astro で 1 つの MDX ファイルを編集すると、ページが再レンダリングされる前に Content Layer のリロードが起き、すべてのコレクションエントリが再パースされることがあります。zfb は変更をグラフ経由でルーティングし、そのエントリを消費する 2 ページを見つけ、他のエントリのスナップショットに触れることなくそれらだけをレンダリングします。 より広いアーキテクチャの全体像については [Architecture overview](/concepts/architecture-overview) を参照してください。 ## 正直な現状のボトルネック 依存グラフはボトルネックではありません。完璧なダーティセット(リビルドすべきページが 1 つ)であっても、dev モードのリビルドごとに 2 つのコストが支払われます。 **1. リビルドごとのエンジン起動。** zfb は、組み込みの V8 ホストにワーカーバンドルをロードしてページをレンダリングします。各リビルドはレンダラを再ロードします。V8 アイソレートが起動し、ワーカーバンドルを評価し、ダーティなページがレンダリングされるとテアダウンします。このオーバーヘッドは、いくつのページがダーティかに関係なく一定です。グラフは 500 ページのレンダリングからは救ってくれますが、どのページのレンダリングよりも前に起きるアイソレートの起動コストからは救えません。 これはグラフの問題ではありません。これは別の最適化軸です。ティック間でアイソレートを温かく保つ、あるいは永続的なレンダラホストへ切り替えるというもので、ロードマップ上にありますが今日は未実装です。 **2. コンテンツ変更時のワーカーバンドルのリビルド。** `ContentSnapshot`(すべてのコンテンツコレクションエントリを JSON にシリアライズしたもの)は、V8 ホストがロードするワーカーバンドルに直接埋め込まれます。今日、コンテンツの変更は、たとえ 1 エントリだけが変わったとしても、フルバンドルのリビルドをトリガーします。バンドルのリビルド時間は O(snapshot size) であり、O(changed entries) ではありません。 zfb が対象とするプロジェクト規模(数百の MDX ファイル、典型的なドキュメントサイト)では、これは余裕で収まります。非常に大きなコンテンツセットはリビルド時間を線形に押し上げます。予定されている緩和策(変更されたエントリだけを再シリアライズするスナップショットパッチ、各バンドルが 1 コレクションをカバーするコレクション単位のシャーディング)は今後のロードマップとして追跡されていますが、今日は未実装です。 ビルドのスナップショットフットプリントを測定するには `ZFB_DEBUG_SNAPSHOT=1` を設定します。 ```sh ZFB_DEBUG_SNAPSHOT=1 pnpm exec zfb build ``` これは stderr に次のような行を出力します。 ``` content snapshot: 187 entries / 412 KB ``` 完全な測定ガイドと予定されているシャーディング作業については [README のスナップショットセクション](https://github.com/Takazudo/zudo-front-builder#limits) を参照してください。 ## スケーリングのスイートスポット 下の表は、現状のアーキテクチャがどこで快適でどこで苦しいかのおおよその感覚を示します。数値はオーダーレベルの見積もりです。実際の時間はハードウェア、コンテンツエントリのサイズ、コンポーネントの深さによって変わります。 | Site size | Cold-start boot | Graph lookup | Pages re-rendered | Overall feel | |---|---|---|---|---| | ~100 ページ、~100 コンテンツエントリ | 高速 | 即時 | 1〜3 | 保存からリロードまで即座 | | ~1,000 ページ、~1,000 エントリ | 高速 | 即時 | 1〜5 | 軽快。スナップショットサイズはまだ小さい | | ~10,000 ページ、~5,000 エントリ | 高速 | 即時 | 1〜10 | 体感あり。スナップショットのリビルドがコスト | | ~100,000 ページ以上 | 高速 | 即時 | 1〜多数 | 線形に苦しい。分割ビルドやコレクション単位のシャーディングを検討 | グラフのルックアップとページレンダリングの列は、サイトサイズによってあまり変わりません。それが勝ち筋です。スナップショットのリビルドの列はコンテンツセットのサイズとともに増大し、現状の上限となっています。エンジン起動は一定のオーバーヘッドであり、ダーティセットが小さいとき(レンダリングするページは 1 つでも、同じ起動コストを払う)に最も効いてきます。 ~10k の線を超えるサイトについては、今日のアドバイスは次のとおりです。`ZFB_DEBUG_SNAPSHOT` でスナップショットサイズを監視し、コンテンツエントリを短く保ち、ロードマップが提供したときにはコレクション単位のシャーディングを計画してください。 --- # ライブラリとして組み込む > Source: https://takazudomodular.com/pj/zudo-front-builder/ja/docs/guides/embed-as-library `zfb-server` クレートは小さなビルダー API を提供しており、Rust ホストが zfb の HTTP サーバーをインプロセスで動かせます。子サイドカーも追加のバイナリも不要です。`zfb dev` を支えるのと同じクレートが組み込みサーバーを支えており、唯一の違いは誰がライフサイクルを所有するかだけです。 このガイドでは、公開ビルダーの形、リクエスト拡張の注入ポイント、そしてホストハンドラの継ぎ目(`with_ssr_handler`)を解説します。 ## どんなときに使うか ホストが次のことを必要とするとき、ライブラリとして組み込む方法を選びます。 - zfb のフルなルートテーブルをデスクトップまたは CLI プロセス内で動かす、 - ハンドラが毎リクエストで読むべきプロセス単位のコンテキスト(`AppHandle`、FS の権限セット、認証トークンなど)を付与する、 - 特定の URL パターンを、JS ランタイムを一切経由せずに Rust 所有のレスポンス(Markdown ルックアップ、データベース読み込み、サイドカー API ブリッジ)でショートサーキットする。 このモードと子サイドカーを同梱する方法のどちらにするかを決める構成マトリクスについては、[Desktop Deployment](/guides/desktop-deployment) を参照してください。 ## ビルダーの形 公開される API 表面は小さいものです。`Server` 型と `ServerBuilder` 型を合わせても、公開メソッドは 10 個以下です。 ```rust use zfb_server::{Server, ServerHandle, ServerMode, RouteParams}; use axum::http::{Request, StatusCode}; use axum::body::Body; let server = Server::builder() .config_path("./zfb.config.json") .mode(ServerMode::Embed) .bind("127.0.0.1:0".parse()?) .with_request_extension(host_ctx.clone()) .with_ssr_handler( "/api/echo/:msg", |req: Request, params: RouteParams| async move { let msg = params.get("msg").unwrap_or("(none)"); let ctx = req.extensions().get::().cloned(); (StatusCode::OK, format!("ctx={ctx:?} msg={msg}")) }, ) .build()?; let handle: ServerHandle = server.serve_in_thread()?; println!("listening on {}", handle.addr()); // later: handle.shutdown()?; ``` ビルダーが確定する主な選択肢: - **`ServerMode::Embed`** はライブリロードのスクリプト注入と `/__zfb/reload` SSE エンドポイントをオフにします。それ以外のルートテーブルは `zfb dev` と同一です。 - **`bind("127.0.0.1:0")`** は OS にエフェメラルポートを要求します。実際のポートは `ServerHandle::addr()` から読み取ってください。 - **`serve_in_thread()`** は独自の `current_thread` tokio ランタイムを持つ専用の OS スレッドを起動するため、ホスト側に独自の tokio ランタイムは不要です(Tauri の同期的な `setup` コールバックも変更なしで機能します)。 - **`ServerHandle::shutdown()`** は冪等です。2 回呼んでも何も起こりません。 すでに独自の tokio ランタイムを駆動しているホスト向けに、非同期の終端 `Server::serve(self, shutdown).await` も利用できます。 ## リクエスト拡張の注入 `ServerBuilder::with_request_extension::(value)` は、受信するすべてのリクエストの `http::Extensions` マップにクローンされるプロセス単位の値を登録します。ハンドラは `req.extensions().get::()` でそれを読みます: ```rust #[derive(Clone)] struct HostCtx { /* … */ } let server = Server::builder() .config_path("./zfb.config.json") .mode(ServerMode::Embed) .with_request_extension(host_ctx) .with_ssr_handler("/whoami", |req: Request, _params| async move { let ctx = req.extensions().get::().cloned(); format!("{ctx:?}") }) .build()?; ``` 境界となる `T: Clone + Send + Sync + 'static` は、リクエスト単位のコンテキスト型に最低限必要なものです。ハンドラが `axum::Extension` をインポートする必要は一切ありません。そのエクストラクタ型は意図的に `zfb-server` の公開 API 表面から外されています。 異なる `T` で `with_request_extension` を複数回呼ぶと値が蓄積されます。同じ `T` で 2 回呼ぶと最初のものを上書きします(これは `http::Extensions::insert` のセマンティクスと一致します)。 ## ハンドラのシグネチャ `with_ssr_handler` は URL パターンと、次の形の非同期関数を受け取ります: ```rust async fn(http::Request, zfb_server::RouteParams) -> impl IntoResponse ``` それがコントラクトのすべてです。ハンドラは: - 受信したリクエストをそのままの `http::Request` として受け取ります。クエリ文字列、ヘッダー、ボディ、拡張のすべてがリクエスト自体からアクセス可能です。 - キャプチャされたルートパラメータを、`params.get("name")` でルックアップできる `RouteParams` 値として受け取ります。 - `axum::response::IntoResponse` を実装する任意のもの(`String`、タプル `(StatusCode, String)`、完全な `http::Response<…>`、カスタム型など)を返します。 このシグネチャが意図的に HTTP 的な形になっているのは、ハンドラが何のためのものかに言及しないためです。同じプリミティブが Markdown ルックアップ、データベース読み込み、IPC ブリッジ、あるいはその他のリクエスト時レスポンスを支えられます。サーバーが見るのはバイトの入出力だけです。 ### ルートパターンの文法 パターンは先頭スラッシュ付きのパスです。各セグメントは次のいずれかです: | 形式 | マッチ対象 | キャプチャ先 | | --- | --- | --- | | `foo` | リテラルなセグメント `foo` | — | | `:name` | ちょうど 1 つの空でないセグメント | `params.get("name")` | | `*name` | 残りの 1 つ以上のセグメントを `/` で連結したもの | `params.get("name")` | `*name` ワイルドカードはパターンの最終セグメントでなければなりません。空のキャプチャ(`:name` スロットに対する素の `/`)は拒否されます。 例: - `/health` は `/health` のみにマッチします。 - `/users/:id` は `/users/42` にマッチし、`id = "42"` となります。 - `/files/*rest` は `/files/a/b.txt` にマッチし、`rest = "a/b.txt"` となります。 - `/projects/:proj/refs/*rest` は両方を組み合わせています。 ## 優先順位: ホストハンドラがランタイム SSR に勝つ これは組み込みの継ぎ目における最も重要なルールです。開発ルーターがリクエストを受け取ると、次の順序で各レイヤーを試します: 1. **プラグインの dev-middleware**(最長プレフィックスマッチ)— 登録されたプラグインがそのパスを要求する場合、 2. **ホストが登録した Rust ハンドラ**(このビルダーメソッド)— 上記のパターンのいずれかがマッチする場合、 3. **リクエスト時 SSR** — `SsrRouteSet` がそのパスを要求する場合、 4. メモリ内ページキャッシュ(SSG 出力)、 5. `dist/` へのディスク上フォールバック、 6. `public/` へのディスク上フォールバック、 7. dev の 404。 ホストハンドラは、**同じ URL を要求するランタイム SSR ページよりも常に優先されます**。プロジェクトに `/dynamic` の Rust ハンドラと、同じく `/dynamic` を配信する `prerender = false` ページの両方がある場合、Rust ハンドラが勝ち、SSR ディスパッチャは一切呼び出されません。 この方向にする理由: - ホストはプロセスの信頼された所有者です。あるパスにハンドラを登録するなら、それは意図的なオーバーライドです。 - フォールスルーが必要なハンドラは、登録しないことを選べます。優先順位は登録時に決まるのであって、ハンドラがセンチネルを返すことによってではありません。 - 逆順にすると、ランタイムページをオーバーライドしたいホストはすべて、ページのエクスポートをビルド時フラグの後ろにゲートする必要が生じます。これは、組み込みの継ぎ目が意図的に分離している 2 つの関心事を結合させてしまいます。 ## ライフサイクルとシャットダウン `Server::serve_in_thread()` は `ServerHandle` を返します。このハンドルは `Clone` なので、ホストは複数のシャットダウン呼び出し箇所(Tauri の `on_window_event`、Ctrl-C ハンドラ、HTTP の `/admin/stop` ルート)にコピーを渡せます。すべてのクローンは `Arc>` の背後で同じワンショットのセンダーと join ハンドルを共有します: ```rust let handle = server.serve_in_thread()?; let h2 = handle.clone(); ctrl_c_callback(move || { let _ = h2.shutdown(); // idempotent }); // On the main thread, wait for the server to exit: handle.join()??; ``` `shutdown()` はグレースフルシャットダウンのシグナルを一度だけ送ります。それ以降の呼び出しは何もしません。`join()` はシングルショットで、スレッドを待てるのは 1 つの呼び出し元だけです。 ## メソッド予算 組み込みの API 表面は意図的に小さく保たれています。`Server` 型と `ServerBuilder` 型を合わせて公開するメソッドは 9 個です。`Server::builder`、`Server::serve`、`Server::serve_in_thread`、それに 6 つのビルダーメソッド(`config_path`、`mode`、`bind`、`with_request_extension`、`with_ssr_handler`、`build`)です。`ServerHandle::addr`、`ServerHandle::shutdown`、`ServerHandle::join` は意図的に別の型に置かれており、この予算からは除外されています。 将来の機能で合計 10 個を超える必要が生じた場合、正しい対応は継ぎ目を再設計するフォローアップイシューであって、10 個目のメソッドを付け足すことではありません。 ## サンプルクレート 最小限のエンドツーエンドの例が `crates/zfb-server/examples/embed/` にあります。ワークスペースのルートからビルドしてください: ```sh cargo build --manifest-path crates/zfb-server/examples/embed/Cargo.toml ``` あるいは実行します(この例はサーバーを起動し、ハンドラが注入されたコンテキストとともに応答することを示すために HTTP リクエストを 1 回発行し、シャットダウンします): ```sh cargo run --manifest-path crates/zfb-server/examples/embed/Cargo.toml ``` このサンプルクレートは空の `[workspace]` テーブルを使って親ワークスペースから自身を除外しているため、ルートのマニフェストがこれをメンバーとして列挙する必要はありません。 --- # SSR と Cloudflare バインディング > Source: https://takazudomodular.com/pj/zudo-front-builder/ja/docs/guides/ssr-and-cloudflare-bindings ルートをビルド時の静的レンダリングから除外し、`@takazudo/zfb-adapter-cloudflare` で Cloudflare Pages Worker としてデプロイし、ルートの SSR ハンドラ内から Cloudflare Worker バインディング(シークレット、環境変数、**D1 データベース**)を読み取る方法。 zfb + Cloudflare のプロジェクトでは、どちらも「Worker」と呼ばれる 2 つの異なる概念があります。これらを混同することが、最もよくある混乱の原因です。 **zfb が生成する `dist/_worker.js`** — `prerender = false` をエクスポートするすべてのルートに対して Cloudflare アダプターが生成します。これは静的ページと同じ TSX パイプラインの中で動作します。共有レイアウト、コンポーネント、MDX 仮想モジュールはすべて、SSG ルートとまったく同じように機能します。 **外部の単独 Worker** — 別途デプロイされる、あなた自身の wrangler でビルドした Worker バンドル(認証 Worker、写真アップロード Worker、決済 Webhook Worker など)です。zfb はこれらをまったく認識しません。zfb と外部 Worker の継ぎ目は常に HTTP/JSON です。外部 Worker を `fetch()` する `pages/api/*.tsx` のプロキシルートか、直接 `fetch()` を呼ぶ `prerender = false` ページのどちらかになります。 これが重要な理由: AI エージェントや人間の読者は、共有レイアウトの TSX を外部 Worker に直接インポートしようとしがちです。それは機能しません。外部 Worker は別のバンドラ、別のランタイムを持ち、zfb の仮想モジュールレイヤーへのアクセスもありません。レイアウトを wrangler プロジェクトに `import` しようとしているなら、間違った境界を越えています。 生成された Worker が実際にどう動くかの概念的なメンタルモデルについては、[SSR on a Worker (adapter mode)](/concepts/ssr-on-a-worker) を参照してください。 ## zfb における SSG と SSR デフォルトでは、zfb のすべてのページは**ビルド時に一度だけ**静的 HTML にレンダリングされます(SSG)。これはコンテンツサイトにとって正しいデフォルトです。高速で、キャッシュ可能で、サーバーを必要としません。 **リクエストごとに**実行されなければならないルート(データベースの読み込み、セッションクッキーの確認、`POST` の処理など)は、単一のエクスポートで SSG から除外します: ```tsx // pages/api/products.tsx ``` `prerender = false` は、静的レンダリング中にこのページをスキップし、代わりに設定済みのアダプターに渡される **SSR バンドル**に含めるよう `zfb build` に指示します。 ルートが `prerender = false` をエクスポートしているのにアダプターが設定されていない場合、ビルドは問題のルート名を示すエラーとともに即座に失敗します。zfb はデプロイできないルートを黙って取りこぼすことはありません。 ## `prerender = false` の dev と本番の同等性 `zfb dev` は `prerender = false` のルートを、Cloudflare が本番で実行するのと**同じレンダリングコード**を通して動かします。開発サーバーは埋め込みの V8 アイソレート(ビルド時 SSG を駆動するのと同じもの)をホストし、開発ルーターは `prerender = false` の URL をリクエスト時にそのアイソレートへディスパッチします。ビルド時でも静的スナップショットからでもありません。 同等性の保証は**バイト単位ではなく意味的**です。ステータスコード、レスポンスボディ、`Content-Type` は dev とデプロイされた Cloudflare アダプターの間で一致します。実行ごとに正当に変わる値(レスポンスに刻まれるタイムスタンプ、ランダムに生成されるリクエスト ID など)は異なってもよいとされます。 これが実際に意味すること: - `?id=…` クエリパラメータに基づいて異なる HTML を返すページは、開発時のページ再読み込みのたびに正しい HTML をレンダリングします。前回のビルドの古いスナップショットではありません。 - 例外を投げる SSR ハンドラは、`zfb build` + デプロイのあとに初めて失敗するのではなく、開発時にブラウザでインラインに V8 スタックトレースを表示します。 - プラグインの dev-middleware は依然として登録済みの URL を最初に要求します(プラグインルートは dev 専用のモックレスポンスなどのために SSR をオーバーライドできます)。SSR レイヤーはプラグインミドルウェアと静的ページキャッシュの間に位置します。 現時点での dev 側 SSR パスの意図的な制限が 1 つあります: **SSR バンドルのライブリロードはありません。** `zfb dev` セッション中に `prerender = false` ページのソースを編集しても、実行中の V8 ホスト内でバンドルが再評価されることはありません。新しいコードを反映するには `zfb dev` を再起動してください。(静的 HTML キャッシュのライブリロードは変わらず機能します。再起動が必要なのは SSR バンドルのパスだけです。) この制限は**本番**の Cloudflare アダプターには適用されません。開発サーバーが鏡映しているのはそちらです。dev の同等性の要点は、レンダリングされた出力が一致することであって、すべての開発機能が最終的な出荷形態であることではありません。 zfb は `prerender` を**ビルド時の静的 AST 検査**で検出します。ランタイムの評価ではありません。エクスポートは**リテラルな `export const`** 宣言でなければなりません: ```tsx ``` 次の形は検出され**ず**、黙って SSG にフォールバックします: ```tsx // ❌ indirect assignment — not a literal export const const flags = { prerender: false }; // ❌ function call — not a literal export const ``` 同じ制約は `frontmatter` エクスポートにも適用されます。リテラルのみのコントラクトについては [Frontmatter](/concepts/frontmatter) を参照してください。 ## Cloudflare アダプターの設定 アダプターをインストールし、`zfb.config.json` で名前を指定します: ```sh pnpm add -D @takazudo/zfb-adapter-cloudflare ``` ```jsonc { "framework": "preact", "adapter": "@takazudo/zfb-adapter-cloudflare" } ``` すると `zfb build` は `dist/` の下に次を生成します: - すべての SSG ページの静的 HTML、そして - `_worker.js` + `_zfb_inner.mjs` — `prerender = false` のルートを配信する Cloudflare Pages の**アドバンストモード** Worker エントリ。 `dist/` を通常どおり Cloudflare Pages にデプロイします。Worker が動的ルートを処理し、静的アセットサーバーがそれ以外のすべてを処理します。 アダプターはリクエスト単位の `(env, ctx, request)` コンテキストを `AsyncLocalStorage`(`node:async_hooks` 由来)を通して引き回し、`getCloudflareContext()` がそこから読み取ります。Workerd はデフォルトでは `node:async_hooks` を公開しません。`wrangler.toml` でオプトインする必要があります: ```toml # wrangler.toml compatibility_flags = ["nodejs_compat"] ``` このフラグがないと、Worker は `node:async_hooks` を欠落モジュールとして示すエラーとともに起動に失敗します。より深い仕組みについては [SSR on a Worker (adapter mode)](/concepts/ssr-on-a-worker#the-getcloudflarecontext-trick) を参照してください。 ## SSR ハンドラから Worker の `env` を読む Cloudflare Worker の `fetch` ハンドラは `(request, env, ctx)` を受け取ります。アダプターは `env` と `ctx` を、リクエスト単位の [`AsyncLocalStorage`][als] スコープを通してページに引き回すため、SSR ルートは `getCloudflareContext()` でそれらを読みます: ```tsx // pages/api/whoami.tsx interface Env { ANTHROPIC_API_KEY: string; } const { env, ctx } = getCloudflareContext(); ctx.waitUntil(reportToAnalytics()); // fire-and-forget background work return new Response(env.ANTHROPIC_API_KEY ? "ok" : "missing key"); } ``` `Env` ジェネリックがバインディングの形を絞り込むため、TypeScript は `env.ANTRHOPIC_KEY` のようなタイプミスを捕捉します。 `getCloudflareContext()` は Worker のリクエストスコープの外(たとえばビルド時 SSG 中)で呼ばれると例外を投げます。これは設計どおりです。バインディングを必要とするルートは**必ず** `prerender = false` をエクスポートしなければなりません。ルートを両方のモードで動かしたい場合は、エラーをキャッチして分岐してください。 ## D1 データベース(`env.DB`)を読む [D1][d1] は Cloudflare のサーバーレス SQLite です。D1 バインディングは他のどのバインディングともまったく同じように `env` に公開されます。アダプターはこれを特別扱いしません。バインディングの TypeScript の形を宣言してクエリします: ```tsx // pages/api/products.tsx interface Env { // `D1Database` comes from `@cloudflare/workers-types`. Install it as // a devDependency if you want the full typed surface; otherwise a // minimal structural shape like the one below works too. DB: D1Database; } const { env } = getCloudflareContext(); // Always use `.bind(...)` for user input — D1 prepared statements // are parameterised, which prevents SQL injection. const { results } = await env.DB .prepare("SELECT id, name, price_cents FROM products ORDER BY id") .all(); return new Response(JSON.stringify({ products: results }), { status: 200, headers: { "content-type": "application/json" }, }); } ``` 単一行の読み込みには `.first()` を使います: ```tsx const product = await env.DB .prepare("SELECT * FROM products WHERE id = ?") .bind(productId) .first(); ``` 書き込み(`INSERT` / `UPDATE` / `DELETE`)には `.run()` を使います: ```tsx await env.DB .prepare("INSERT INTO orders (user_id, total_cents) VALUES (?, ?)") .bind(userId, totalCents) .run(); ``` ## D1 バインディングの配線 D1 は `wrangler.toml` を通して Pages プロジェクトにバインドされます。バインディングの**名前**(下記の `DB`)が、`env` で読むプロパティになります: ```toml # wrangler.toml [[d1_databases]] binding = "DB" # → env.DB inside the Worker database_name = "webshop" database_id = "" # printed by `wrangler d1 create` ``` エンドツーエンドのライフサイクル: 1. **データベースを作成する** — `wrangler d1 create webshop`。これが `database_id` を出力します。`wrangler.toml` に貼り付けてください。 2. **マイグレーションを書く** — `.sql` ファイルを `migrations/`(wrangler のデフォルト)の下に置きます。各マイグレーションは素の SQL(`CREATE TABLE` など)です。 3. **マイグレーションを適用する** — `wrangler d1 migrations apply webshop`(ローカルの開発用データベースには `--local`、デプロイ済みのものには `--remote` を追加)。 4. **デプロイする** — `zfb build` を実行し、`dist/` を Cloudflare Pages にデプロイします。 **プレビューと本番**を分ける場合は、名前付き環境の下にバインディングを宣言し、それぞれが独自のデータベースを持つようにします: ```toml [[d1_databases]] binding = "DB" database_name = "webshop" database_id = "" [[env.preview.d1_databases]] binding = "DB" database_name = "webshop-preview" database_id = "" ``` ## ローカル開発 `wrangler pages dev dist/` は、ビルドされた `_worker.js` を**ローカル**の D1 データベース(`.wrangler/` 下の SQLite ファイル)とともにローカルで動かします。最初の実行前に、`wrangler d1 migrations apply webshop --local` でマイグレーションを適用してください。 [als]: https://nodejs.org/api/async_context.html [d1]: https://developers.cloudflare.com/d1/ --- # dev モードのライフサイクル > Source: https://takazudomodular.com/pj/zudo-front-builder/ja/docs/concepts/dev-mode-lifecycle `zfb dev` の「保存からピクセルまで」のループ。ウォッチャーがどのように変更を検出するか、 オーケストレーターが何をリビルドするかをどう決めるか、そして 3 つの SSE イベント種別が どのように、不要なフルページリロードなしにブラウザを反応させるか。ビルドステップ全体の 順序については [Build pipeline](/concepts/build-pipeline) を、依存グラフがどのように リビルドを影響を受けるページに限定するかについては [Incremental rebuild](/concepts/incremental-rebuild) を参照してください。 このページは **SSG / 静的 HTML の dev ループのみ** を説明します。`prerender = false` の SSR ルートについては、`zfb dev` セッション中のソース編集は **ホットリロードされません** — V8 SSR バンドルは起動時の JS バンドルにバインドされており、再起動でのみ反映されます。下記の ティックごとのリビルドは SSR レンダラを **リロードしません**。それはレンダラがブートストラップ された際の SSG 出力を再レンダリングします。SSR ガイドの [No live reload of the SSR bundle](/guides/ssr-and-cloudflare-bindings#dev-prod-parity-for-prerender--false) を参照してください。 ## `.tsx` ファイルを保存する。それから? 保存した瞬間、オペレーティングシステムがそのファイルのパスに対してファイルシステムイベントを 発火します。`crates/zfb-watcher` は関連するすべてのディレクトリ — `pages/`・`components/`・ `content/`・`layouts/`・`styles/`・`data/`・`public/`、そして 2 つの設定ファイル `zfb.config.json` と `zfb.config.ts` — を [`notify`](https://crates.io/crates/notify) クレート経由で監視しています。(正規のリストは `crates/zfb/src/commands/dev.rs` の `DEFAULT_WATCH_ROOTS` を参照してください。) エディタの保存が単一のきれいなイベントであることはまれです。vim はスワップファイルを元の ファイルにリネームし、VS Code はメタデータ→データのイベントを立て続けに複数発行し、 `git checkout` は一度に数百を発火します。ウォッチャーのデバウンサーは、50ms の静寂ウィンドウ 内のすべてを単一の `Change { path, kind }` 値に統合します。`kind` フィールドは `ChangeKind::Created`・`ChangeKind::Modified`・`ChangeKind::Removed` のいずれかです。 OS が分類できないイベント種別はすべて `Modified` に折りたたまれ、実際の変更が静かに 取りこぼされることが決してないようにします。 バーストが収まりデバウンスウィンドウが閉じると、ビルドオーケストレーター(`crates/zfb-build`)が 変更を受け取ります。変更されたパスを携えて `crates/zfb-graph` を呼び出し、`DirtySet` を 受け取ります。`All`(`zfb.config.ts` のようなグローバルファイル)か、変更されたファイルを 実際にインポートしているページの `Specific(set_of_page_ids)` のどちらかです。ダーティセットの 外のページは触られません。(依存追跡の完全なストーリーは [Incremental rebuild](/concepts/incremental-rebuild) にあります。) オーケストレーターはダーティセットから `RebuildPlan` を組み立てます。変更されたパスが アイランドルート(例: `components/`)の中にあれば、プランの `rerun_islands` フラグが 立てられます。CSS ソースが変更されていれば `rerun_css` が立てられます。その後、 `DevAssetPipeline::apply()` メソッドが次の順序で実行されます。 1. **ページの再レンダリング。** オーケストレーターはダーティセット内の各ページについてレンダラを 呼び出し、`zfb dev` 起動時にブートされた組み込み V8 ホストを通して SSG 出力を駆動します。 レンダリングされた各 `RenderedPage.html` は、直近の既知の出力とバイト単位で比較されます。 バイトが同一なら(セマンティックな HTML の変化を生まない純粋なリファクタリング)、そのページ についてはファイルが書き込まれず、リロードシグナルも送られません。V8 ホスト自体はティック ごとに **リロードされません** — 今日の dev では `BuildContext::reload_renderer` は `None` であり(`crates/zfb/src/commands/dev.rs` を参照)、レンダラはセッション全体を通して 起動時のバンドルにバインドされたままです。 2. **CSS パイプライン。** `rerun_css` が true のとき、Tailwind v4 + PostCSS が実行されます。 CSS 出力が前のティックとバイト単位で同一なら、`Css` イベントは発行されません。 3. **アイランドの再バンドル。** `rerun_islands` が true のとき、esbuild の Go バイナリの サブプロセスが呼び出されます。すべての `"use client"` コンポーネントをバンドルし、単一の 結合モジュールを **安定したファイル名** — `dist/assets/islands.js`(`crates/zfb-types/src/asset_urls.rs` の `STABLE_ISLANDS_FILENAME` 定数で、再エクスポートされてアイランドバンドラに消費されます) — に書き込みます。ファイル名にコンテンツハッシュはありません。[なぜ dev ではファイル名が安定したままなのか](#なぜ-dev-ではファイル名が安定したままなのか) を参照してください。 4. **ビルド結果のブロードキャスト。** パイプラインは `BuildOutcome` 構造体を返します。 `crates/zfb-server/src/livereload.rs` の `outcome_to_events()` がその結果を検査し、 `/__zfb/reload` の SSE チャネル経由でブロードキャストされる `ReloadEvent` 値にマッピング します。あなたのサイトを開いているすべてのブラウザタブはそのチャネルを購読しており、即座に 反応します。 ## 3 つの SSE イベント種別 | 結果のトリガー | イベント | ブラウザの振る舞い | |---|---|---| | `pages_written.len() > 0` | `Page` | 完全な `location.reload()` | | `css_changed` | `Css` | すべての `` をホットスワップ — ドキュメントをリロードせずにブラウザキャッシュを破棄するため `?v=` を付加 | | `islands_bundle.is_some()` | `Islands { component, bundle_url }` | 新しいバンドル URL の動的 `import()`(キャッシュ破棄の `?v=` 付き)。新しくインポートされたモジュールがハイドレーションを実行し、デフォルトでは現在のページ上の **すべての** `[data-zfb-island]` 要素を再マウント — ドキュメントのリロードなし | 複数のイベントが同じティックで発火すると、サーバーは該当するイベントをすべて発行し、 **接続中のすべてのタブが同じ SSE チャネルを購読しており、それらすべてを受け取ります。** ブラウザはイベントを到着順に処理します。`Page` イベントを受け取ると、各タブは `location.reload()` を呼び出し、現在のドキュメントを破棄します — これにより、同じティックで 発行された `Css` や `Islands` イベントは、そのタブにとって無意味になります。「このタブだけが フルリロードを受け取る」というパターンはここには存在しません。**インプレースの `Css` と `Islands` のスワップは、そのティックでアクティブなタブだけでなく、購読しているすべてのタブに とって無意味です。** `Islands` について 1 つの詳細: 今日の dev モードでは、`BuildContext::run_islands` は ビルド側のペイロードが現状アイランドごとの名前を提供しないため、`outcome_to_events()` に `components: Vec::new()` を報告します。そのためサーバーは `component: ""` と安定した バンドル URL(`/assets/islands.js`)を持つ **単一の** `Islands` イベントを発行します。 `/__zfb/livereload.js` のクライアントスクリプトは `bundleUrl` だけを読み、新鮮な タイムスタンプ(`?v=`)を付加し、その結果を動的インポートします。インポートされた バンドルのトップレベルのハイドレーションコードが実行され、ページ上のすべての `[data-zfb-island]` を巡回します。つまり、単一のアイランドティックは、デフォルトでは単一の コンポーネントではなくページ全体を再ハイドレートします。 **ターゲットを絞った再ハイドレーションはオプトインです。** クライアントがユーザー提供の `window.__zfbIslandsReload(component, swapUrl)` 関数を検出すると、プレーンな動的インポートを 行う代わりに、そのフックにインポートを委譲します。アイランドのホットスワップを通してスクロール 位置やコンポーネントの状態を保持したいアプリケーションは、このフックをインストールし、どの コンポーネントを再マウントするかを自分自身で決めます。フックがなければ、デフォルトのページ全体の 再ハイドレーションが実行されます。 ## なぜ dev ではファイル名が安定したままなのか 本番ビルドはコンテンツハッシュ付きのアセット URL(`/assets/islands-abc12345.js`)を使います。 これにより、デプロイされた CDN レスポンスを無期限にキャッシュでき、新しいデプロイで変更された アセットは新鮮な URL を得ます。ハッシュはファイルの内容によって決まり、バンドルが変わるたびに 変化します。 dev モードは意図的にコンテンツハッシュをスキップします。出力は `dist/assets/islands.js` に 配置されます — 毎ティック同じ URL です。これが、SSE 駆動のホットスワップを機能させる URL コントラクトの保証です。ブラウザが `Islands` イベントを受け取ったとき、新しいバンドルが 常に同じベース URL で到達可能だと分かっています。リビルドのたびに URL を変えると、ブラウザに スワップ元のキャッシュ参照がなくなるため、フルページリロードが強制されてしまいます。 コンテンツハッシュは本番パイプラインの責務です。dev では安定した名前が設計上正しいのであって、 見落としではありません。 ## 実際にはどういう意味になるのか 典型的な 3 つの編集シナリオと、どのイベントが発火するか。 **`.tsx` アイランドコンポーネントの本体のみを編集する。** ウォッチャーがコンポーネントファイルで 発火します。依存グラフはそれを消費するページをダーティとしてマークし、オーケストレーターは まず影響を受けるページを再レンダリングし、その後アイランドを再バンドルします。レンダリングされた HTML が変わっていれば、`Page` イベントが発火し、ブラウザがリロードします。HTML がバイト単位で 同一なら(例: サーバーレンダリングに決して到達しないクライアント側の状態ロジックだけを変えた 場合)、`Islands` イベントだけが発火します — 新しいバンドルが動的インポートされ、その ハイドレーションが実行され、ページ上のすべてのアイランドを再マウントします。そのスワップを通して スクロール位置やコンポーネントごとの状態を保持するには、`window.__zfbIslandsReload` フックを インストールしてください([3 つの SSE イベント種別](#3-つの-sse-イベント種別)を参照)。 **アイランドを消費するページファイルを編集する。** ページとアイランドの両方がダーティです。 オーケストレーターはページを再レンダリングし、HTML はほぼ確実に変わり、`Page` イベントが 発火します。ブラウザは最新のサーバーレンダリング済み HTML で新鮮なフルページロードを得ます。 そのタブの同時並行の `Islands` イベントは、リロードによって無意味になります。 **CSS ファイルのみを編集する。** ページはダーティにならず、アイランドの再バンドルもありません。 CSS パイプラインが実行され、出力が変わっていれば `Css` イベントが発火します。ブラウザは スタイルシートをインプレースでスワップします — ドキュメントのリロードはなく、スクロール位置と クライアント側の状態はすべて保持されます。 ## 関連 - [Build pipeline](/concepts/build-pipeline) — CLI から `dist/` までの完全なパイプラインと、dev モードがその上にどう乗るか - [Incremental rebuild](/concepts/incremental-rebuild) — リビルドを影響を受けるページに限定する依存グラフと `DirtySet` - [Islands](/concepts/islands) — `"use client"` がどのようにコンポーネントをアイランドバンドルにオプトインさせるか - [SSR and Cloudflare Bindings](/guides/ssr-and-cloudflare-bindings) — dev における `prerender = false` ルートの再起動のみという制約を含む --- # Worker 上の SSR(アダプターモード) > Source: https://takazudomodular.com/pj/zudo-front-builder/ja/docs/concepts/ssr-on-a-worker `prerender = false` の zfb ページがプロダクションで実際に何を動かすのかというメンタルモデル。2 層のワーカー出力、リクエストが正しいハンドラへどうディスパッチされるか、そして `getCloudflareContext()` が prop-drilling なしに任意のページコンポーネントへ Cloudflare のバインディングを届ける方法を扱います。 このページはメンタルモデルです。実際のセットアップ(アダプターのインストール、`wrangler.toml` の配線、D1 へのクエリ、`compatibility_flags` の設定)については [SSR and Cloudflare Bindings](/guides/ssr-and-cloudflare-bindings) を読んでください。 また、このページは zfb が出力する `dist/_worker.js` に特化したものであり、wrangler で別途デプロイする外部の Worker についてではありません。 ## あなたのページはただの関数 フレームワークの語彙を取り去れば、`prerender = false` の zfb ページは `Response` を返すただの `async function` です。以下のスニペットでは、`renderToString`・`getUser`・`updateProfile`・`AccountLayout` はアプリ側のコードを表すプレースホルダーの識別子です。zfb や `@takazudo/zfb-adapter-cloudflare` のエクスポートではありません。この例で使われている唯一のフレームワーク提供のシンボルは `getCloudflareContext` です。 ```tsx // pages/account.tsx interface Env { DB: D1Database; SESSION_SECRET: string; } const { env, request } = getCloudflareContext(); if (request.method === "POST") { const form = await request.formData(); await updateProfile(env, form); return new Response(null, { status: 303, headers: { location: "/account" } }); } const user = await getUser(env, request); return new Response( renderToString(), { headers: { "content-type": "text/html" } }, ); } ``` この関数は引数を受け取りません。Cloudflare の `(request, env, ctx)` のタプルは、コールツリーのどこでも(レイアウト、ヘルパー、lib モジュール)`getCloudflareContext()` 経由で利用できます。同じページ上の `` からの `POST` は同じハンドラに到達します。`request.method` での分岐がミューテーションの経路です。 ここで起きていることに、手書きの Worker で起きないような特別なことは何もありません。このページの残りでは、`getCloudflareContext()` が実際にどう動くか、そしてそれを可能にするためにビルドが何を出力するかを説明します。 ## ビルドが実際に出力するもの `@takazudo/zfb-adapter-cloudflare` を設定した状態で `zfb build` を実行すると、`dist/` 配下に 2 つのファイルが生成されます。 **`dist/_worker.js`** — アダプターが書き出す小さな自動生成のスタブ。これは Cloudflare Pages のアドバンスドモードのエントリです。リクエストが到着したときに Cloudflare がロードするファイルです。その役割は、`(env, ctx, request)` のための `AsyncLocalStorage` スコープをセットアップし、すべてのリクエストを(静的アセットサーバーか内側のバンドルのいずれかへ)ディスパッチすることです。あなたのアプリケーションコードは含みません。 **`dist/_zfb_inner.mjs`** — 本物のアプリケーションバンドル。すべての `pages/*.tsx` ファイル、レイアウト、コンポーネント、lib ヘルパーを単一の Hono 形の ESM モジュールにコンパイルしたものです。これが実際にページをレンダリングします。 この 2 ファイル構成は、アダプター内での 2 回目の esbuild パスを回避します。Workerd のモジュールローダーは、アドバンスドモードの `_worker.js` ディレクトリ内の相対 ESM インポートを解決するため、`_worker.js` は単純に `import inner from "./_zfb_inner.mjs"` でき、両ファイルは独立したままです。 より広い 2 バンドルのメンタルモデル(ワーカーバンドル対 islands バンドル)については [Architecture overview](/concepts/architecture-overview) を読んでください。 ## ディスパッチフロー Cloudflare Pages サイトへのすべてのリクエストは、まず `_worker.js` に到達します。スタブは静的優先・動的が次という規則を適用します。 | Request method | Dispatch order | |---|---| | GET, HEAD | まず `env.ASSETS` を探る。内側のワーカーが呼ばれるのは、アセットサーバーが 404 を返した場合だけ。それ以外のステータス(200・308 など)はクライアントに直接返される。 | | POST, PUT, PATCH, DELETE | `env.ASSETS` を完全にスキップ。直接内側のワーカーへ進む。 | フォールスルーの規則は「404 のみ」であり「200 以外のみ」ではありません。これが重要なのは、Cloudflare Pages のアセットサーバーが、プリレンダリングされたルートの末尾スラッシュを正規化するために 308 リダイレクトを出すためです(例: `/docs/account` → 308 → `/docs/account/` → `dist/docs/account/index.html`)。ラッパーはこれらの 308 をクライアントへ届けて、ブラウザがそれを辿って index.html へ到達できるようにしなければなりません。`worker-wrapper.mjs` を見ると、ラッパーは `await env.ASSETS.fetch(request)` を呼び、`assetResponse.status !== 404` のときはレスポンスを手を加えずに返します。 **なぜ GET/HEAD は静的優先なのか:** アセットサーバーは上で説明した末尾スラッシュの正規化を担当し、さらに島のハイドレーションを機能させるビルド時の head 注入(``、`.js">`)も配信します。もし内側の Hono ルーターがプリレンダリングされたルートを先に処理すると、それらの注入されたアセットなしでリクエスト時に再レンダリングしてしまい、島はハイドレートされません。 **なぜ POST/PUT/PATCH/DELETE は直接通すのか:** アセットサーバーは定義上リードオンリーです。フォーム送信のためにそれを探っても、常に 404 か 405 を返すだけ — 利益のない不要な往復です。フォーム送信、JSON API 呼び出し、あらゆるミューテーションは直接内側のワーカーへ進みます。 その結果、静的ページは SSR のコストを払いません。`prerender = true` のページは `env.ASSETS` だけで配信され、内側のワーカーの `fetch` ハンドラは呼ばれません。(ただし `_worker.js` がモジュールスコープで内側のバンドルをインポートしているため、内側のバンドルはワーカー起動時に依然としてロードされ評価されます。正確な表現は [What is NOT in the worker output](#what-is-not-in-the-worker-output) を参照してください。) ## `getCloudflareContext()` のトリック Cloudflare Workers は、同じ V8 アイソレート内で複数のリクエストを並行してディスパッチできます。素朴な `globalThis.__env = env` への書き込みは、それらの並行リクエストをまたいで競合します。アダプターはこれを `node:async_hooks` の `AsyncLocalStorage` で解決します。 仕組みは 2 ステップです。 **ステップ 1 — ラッパーがコンテキストを保存する。** `inner.fetch(request)` を呼ぶ前に、生成された `_worker.js` スタブは次を実行します。 ```js als.run({ env, ctx, request }, () => inner.fetch(request)); ``` これはリクエストごとの async スコープを開きます。そのスコープ内のすべての `await`(レイアウト、ヘルパー、データベース呼び出しをまたいで)は、依然として同じ保存された値を見ます。 **ステップ 2 — ユーザーコードがコンテキストを読む。** `getCloudflareContext()` は、同じ `AsyncLocalStorage` インスタンスに対して `als.getStore()` を呼びます。アダプターモジュールがそのストレージインスタンスを安定したキーで `globalThis` に登録するため、ラッパーファイル(`_worker.js`)とユーザーバンドル(`_zfb_inner.mjs`)は、別々の ESM モジュールであっても同じインスタンスを共有します。`getStore()` は、ラッパーがこのリクエストのために保存した `{ env, ctx, request }` オブジェクトを返します。他の並行リクエストのものではありません。 **なぜこれが `compatibility_flags = ["nodejs_compat"]` を必須にするのか。** `AsyncLocalStorage` は `node:async_hooks` にあります。Workerd はデフォルトでこれを公開しません。次のように opt-in する必要があります。 ```toml # wrangler.toml compatibility_flags = ["nodejs_compat"] ``` このフラグがないと、Worker は起動に失敗します。エラーメッセージは欠けているモジュールとして `node:async_hooks` を名指しします。完全な `wrangler.toml` 設定については [SSR and Cloudflare Bindings guide](/guides/ssr-and-cloudflare-bindings) を参照してください。 **なぜ prop-drilling ではなく AsyncLocalStorage なのか。** ページハンドラは共有のレイアウトや lib ヘルパーを自由に組み合わせます。`env` を明示的なパラメータとして引き回すと、すべてのコンポーネントとヘルパーがそれを受け取ることを強いられます。Cloudflare 固有のインフラと汎用的な UI コードの間の漏れのある結合です。ALS はフレームワークの境界をきれいに保ちます。アダプターがストレージを所有し、ユーザーコードはそれが必要な場所でだけ読みます。 概念は次のようにマッピングされます。 | Concept | Next.js App Router | Remix | zfb adapter-mode | |---|---|---|---| | ファイルベースのルーティング | `app/account/page.tsx` | `routes/account.tsx` | `pages/account.tsx` | | サーバーでのデータ取得 | `async function Page()` | `loader` | `async function AccountPage()` | | ミューテーション | Server Actions | `action` | プレーンな `` | | レイアウト | `layout.tsx` | ネストしたルートレイアウト | `layouts/*.tsx`(インポート) | | 静的対動的 | `dynamic = 'force-static'` / `'force-dynamic'` | (常に動的) | `export const prerender = true` / `false` | 仕組みとしては Worker、使い勝手としては App Router スタイルの SSR、思想としては Remix ランタイムなしの Remix。RSC ストリーミングも Server Actions の抽象もなく、ただ 1 つのバンドルと Web の `fetch` ハンドラがあるだけです。 ## ワーカー出力に含まれないもの 2 つのカテゴリの出力が、`dist/_worker.js` と `dist/_zfb_inner.mjs` から意図的に欠けています。 **Islands** — `"use client"` でマークされたコンポーネントは、別の esbuild ステップによって **単一の結合された** ブラウザ配信バンドルへとコンパイルされます。dev ではこのバンドルは `dist/assets/islands.js`(安定したファイル名、ハッシュなし)に出力され、プロダクションでは `ProductionAssetPipeline` がそれを `dist/assets/islands-.js` として書き出し、レンダリングされた HTML 内のすべての参照をハッシュ付き URL に書き換えます。このバンドルは `dist/_worker.js` の一部でも `dist/_zfb_inner.mjs` の一部でもありません。サーバーで実行される Worker コードではなく、ブラウザ配信の JavaScript です。Worker は静的 HTML のシェルをレンダリングし、ブラウザが islands バンドルをダウンロードして、そのトップレベルのハイドレーションコードがページ上のすべての `[data-zfb-island]` 要素を走査します。完全な仕組みは [Islands](/concepts/islands) を参照してください。 **プリレンダリングされたページ** — `prerender = true` をエクスポートする(またはエクスポートを省略する。デフォルトは true)任意のページは、ビルド時に静的 HTML へレンダリングされます。リクエスト時には、Cloudflare Pages が `.html` ファイルを `env.ASSETS` から直接配信するため、それらのルートに対して内側の Worker の `fetch` ハンドラは **呼ばれません**。ただし、内側のバンドルはワーカー起動時に依然としてロードされ評価されます。`_worker.js` が静的な `import inner from "./_zfb_inner.mjs"` を行うため、最初のリクエストがどのルートに着地するかに関係なく、workerd は内側のモジュールグラフをメモリに引き込みます。この最適化は「静的ヒットは `inner.fetch()` をスキップする」であって、「静的のみのデプロイは内側のバンドルのロードをスキップする」ではありません。 ## 関連 - [Architecture overview](/concepts/architecture-overview) — 2 バンドルのモデルと、ワーカーバンドルの形がビルド時とプロダクションの間の安定したコントラクトであること。 - [Islands](/concepts/islands) — `"use client"` がコンポーネントを Worker の外に存在するブラウザ配信バンドルへどう opt-in させるか。 - [SSR and Cloudflare Bindings](/guides/ssr-and-cloudflare-bindings) — 実際のセットアップ向け: `wrangler.toml`、D1 クエリ、シークレット、`wrangler pages dev` でのローカル開発。 --- # プラグイン > Source: https://takazudomodular.com/pj/zudo-front-builder/ja/docs/concepts/plugins このページは英語版のスタブです。完全な翻訳は別の PR で行われます。 英語版: [Plugins](/concepts/plugins) ## 概要 zfb プラグインは、`ZfbPlugin` オブジェクトをデフォルトエクスポートする ES モジュールです。zfb の build / dev ホストは各プラグインモジュールを起動時に一度ロードし、4 つのライフサイクルフックを順次ディスパッチします。 ```ts name: "my-plugin", setup?(ctx) {}, // #255 — ホスト起動時に一度、preBuild の前 preBuild?(ctx) {}, // バンドラ / レンダラ前のファイル生成 postBuild?(ctx) {}, // dist/ への書き出し後の仕上げ処理 devMiddleware?(ctx) {},// zfb dev でのリクエスト単位 HTTP ハンドラ }); ``` ## 4 つのフック - **`setup(ctx)`** — `addAlias` / `addVirtualModule` / `injectRoute` で仮想モジュール、インポートエイリアス、開発専用の合成ルートを登録します。`ctx.command` が `"build"` か `"dev"` かで分岐できます。 - **`preBuild(ctx)`** — バンドル前にファイルを生成する用途。 - **`postBuild(ctx)`** — `dist/` 書き出し後の仕上げ。`ctx.routes` にビルドが生成した全 URL のルートマニフェストが渡されます(`preBuild` では `undefined`)。 - **`devMiddleware(ctx)`** — リクエストごとに `{ status, headers, body }` を返す HTTP ハンドラ。`injectRoute` がページレンダリングパイプラインを通すのに対し、こちらは JSX を介さない素の HTTP 応答です。 ## 設計上の制約 - `addAlias` のコントラクトは **完全一致のみ** ですが、現状 1 系統だけ振る舞いが異なります。SSR と `paths()` を実行する組み込み V8 ホストは完全一致を遵守します。一方、`"use client"` 島をバンドルする islands esbuild 経路は esbuild CLI の `--alias` フラグに依拠しており、これがプレフィックス + スラッシュ一致のため `@/foo` を登録すると `@/foo/bar` も書き換えてしまいます。両系統を完全一致に揃える作業は v1 フォローアップで追跡されます。当面は実際に import する specifier のみをエイリアス登録するのが安全です。 - `addVirtualModule(specifier, loader)` の `loader` は **完全な ESM ソーステキスト** を返します。loader はビルドごと(および dev ホスト起動ごと)に最初の `import` のタイミングで **1 回だけ** 実行され、結果はキャッシュされます。 - `injectRoute` は **dev 専用**。`zfb build` 中に呼ぶと `InjectRouteInBuildMode` で失敗します。なお v1 ではレジストリへの登録・衝突検出・パターンマッチングまでが対象で、マッチした entrypoint を実際にページレンダラで評価する処理は後続イシューで対応します(マッチ自体は `zfb_plugin` トレースに記録されます)。 - 同じキー(エイリアスの `from` / 仮想モジュールの `specifier` / 注入ルートの `pattern`)を 2 つのプラグインが異なる値で登録すると衝突エラーで両方のプラグイン名を含めて中断します。 - `SetupContext` には `addRemarkPlugin` などのマークダウン拡張面は **意図的に存在しません**。マークダウン関連は zfb 本体(Rust 側)に内包する方針です。 詳細は英語版を参照してください。 --- # 変更履歴 > Source: https://takazudomodular.com/pj/zudo-front-builder/ja/docs/changelog # 変更履歴 このセクションは `@takazudo/zfb` とそのロックステップなワークスペースパッケージ (`@takazudo/zfb-runtime`、`@takazudo/zfb-adapter-cloudflare`、`create-zfb`、および 5 つの プラットフォームバイナリ)のリリース済みバージョンを記録します。 各バージョンは以下に専用のページを持ち、新しい順に並んでいます。 ## リリースプロセス(内部向け) メンテナーは `/l-make-release` の Claude Code スキルを使ってバージョンを更新し、このセクションの エントリを書きます。フロー全体はスキルの SKILL.md を参照してください。 Mac x86_64 ビルドは `/l-make-mac-release-binary` スキルによる任意対応です。公開前にドラフトの GH Release へアーカイブを事前アップロードしておけば、`release.yml` は時間のかかる `macos-13` の CI レグをスキップします。 ## バージョン このページより下のページに、リリース済みの各バージョンが並びます。 --- # v0.1.0-next.5 > Source: https://takazudomodular.com/pj/zudo-front-builder/ja/docs/changelog/v0.1.0-next.5 # v0.1.0-next.5 リリース日: 2026-05-25 ## 機能 - feat(release): X9 trigger switch + A2 detect-mac-local job + docs update (17424b4) - feat(skills): add /l-make-release orchestrator skill (f14b26b) - feat(skills): add /l-make-mac-release-binary skill (2cf5228) - feat(zfb): stamp --version from ZFB_RELEASE_VERSION env at build time (c8f9bc3) - feat(scaffold): sync WORKSPACE_DEP_PLACEHOLDER via sync-platform-versions script (e3c931b) ## バグ修正 - fix(release): address codex review findings (f438a5d) - fix(release): create GH Release as draft in build-macos-x64-local.sh fallback (04cf463) - fix(bundler): re-gate --preserve-symlinks on opt-in field (0bc1d7f) - fix(content): store snapshot on globalThis so dual zfb/content instances share state (efabf06) - fix(release): publish platform packages via npm to preserve binary mode (d71c669) - fix(release): set CI=true for pnpm install in macos-x64 local build (cec7f03) - fix(release): resolve cargo target dir via metadata in macos-x64 local build (096b04e) ## その他の変更 - chore: deprecate l-version-* skills + changelog sortOrder + admonitions (f48db6f) - chore: gitignore per-platform native zfb binary (2e951c7) - tests(smoke): correct Sub #449 evidence — use dynamic paths() route (088e8e1) - test(smoke): add Wave 2 confirmation report for sub-452 (4379d62)