zfb
GitHub リポジトリ

検索したい単語を入力

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

Astro からの移行

既存の Astro 静的サイトを zfb に移行するための概念対応マップ。

すでに Astro サイトを運用している方なら、zfb の大部分には馴染みを感じるはずです。どちらのプロジェクトもファイルベースのルーティング、コンテンツコレクション、パーシャルハイドレーションを備えています。違いは主にスコープにあります。zfb はより小さく、より意見が強く(opinionated)、エンド・ツー・エンドで Rust に支えられています。

ディレクトリ構成

Astro はすべてを src/ の下に配置します。zfb はそうしません。

Astrozfb
src/pages/pages/
src/layouts/layouts/
src/components/components/
src/content/content/
astro.config.mjszfb.config.ts

このフラットな構成は、より小さな表面積に合わせたものです。覚えておくべき src/ という名前空間は存在しません。

コンポーネント: モデルは 1 つ、2 つではない

Astro の目玉機能は .astro ファイル — サーバー専用のテンプレート言語であり、オプションでフレームワークコードのアイランドを含められます。zfb はこれを単一のコンポーネントモデルに統合します。すべてのコンポーネントは .tsx ファイルです。すべての .tsx コンポーネントは独自の zudo-react ランタイムを使います。レイアウト、ページ、コンポーネントで同じ JSX 方言を使い、インタラクティブな状態はレンダリング時の hooks ではなく signal に置きます。モデルと制限は zudo-react の概要を参照してください。

export default function MarketingPage() {
  return <main><h1>Hello</h1></main>;
}

別途学ぶべきテンプレート構文はありませんが、コンポーネントの外でのトップレベル await のような用途で .astro に依存していた場合は、その処理を paths() エクスポートやデータコレクションに移す必要があります。

コンテンツコレクション

astro:content の getCollection("blog") には、zfb でほぼ直接の対応物があります:

import { getCollection } from "zfb/content";

const posts = getCollection("blog");  // synchronous — no await

返される形状も似ています。各エントリは slug、data(frontmatter)、body を公開します。全体の仕様については getCollection を参照してください。

スキーマはインラインの Zod から宣言的な設定へと移ります。コレクションは zfb.config.ts で defineConfig を使って宣言します:

// zfb.config.ts
import { defineConfig } from "zfb/config";

export default defineConfig({
  collections: [
    { name: "blog", path: "content/blog" },
  ],
});

Zod はありません。スキーマの検証には最小限の JSON-Schema 方言を使います。schema フィールドは type キーや properties キーを持つ JSON オブジェクトを受け付けます。type に指定できる値は "string"、"number"、"integer"、"boolean"、"array"、"object"、"null" です("date" はなく、オプショナルを表す末尾の ? もありません)。スキーマは設定読み込み時に構造が検証されますが、frontmatter フィールドはビルド時には強制されません。完全な形状については defineConfig を参照してください。

宣言したスキーマに対して collection frontmatter を検証するには zfb check を実行してください。CI ではこれが強制の関門です。

アイランド

Astro はインポートしたフレームワークコンポーネントの JSX 属性として client:load、client:visible などを使います。zfb はその代わりにファイルレベルの "use client" ディレクティブと明示的な <Island> 境界を使います:

"use client";

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

export default function Counter() {
  const n = signal(0);
  return <button on:click={() => { n.value += 1; }}>{n}</button>;
}

"use client" はクライアント境界の対象を示しますが、それだけで島は起動しません。対象コンポーネントを明示的な <Island> で包んでください。セットアップは一度実行され、signal のバインディングが島の DOM を更新します。Island を参照してください。

MDX のコンポーネントに残った client:load、client:idle、client:visible、client:media、client:only は ZB014 の警告でファイルと行が示されます。コンポーネントがこの props を無視すると起動タイミングには影響しません。DOM 要素へ転送すると ZR_ATTRIBUTE でレンダリングに失敗する場合があります。明示的な <Island> を使ってください。

スロットとレイアウト

Astro の <slot /> は素の JSX children になります:

export default function DocsLayout({ children }) {
  return <div class="prose">{children}</div>;
}

zfb が異なる形で提供するもの

いくつかの Astro 機能には、別の名前で zfb の直接的な対応物があります:

  • ビュートランジション — @takazudo/zfb-runtime の <ClientRouter /> を使います (Astro の <ClientRouter /> が提供するのと同じ、ビュートランジションアニメーション付きの SPA ルーターです)。

  • アイランド / クライアントディレクティブ — "use client" と明示的な <Island> に 置き換えられています。アイランドを参照してください。

現時点で対応物がない機能もあります。インテグレーションのマーケットプレイス、Astro DB、サーバーエンドポイントなどです。サイトがこれらに依存している場合は、移植に踏み切る前に、zfb のプラグインシステムやアダプターがユースケースをカバーできるか評価してください。

v0 以降のエンジン追加機能

以下の機能は zfb の初期リリースには含まれていませんでしたが、現在では利用可能です。Astro サイトで同等のパターンを使っていた場合、zfb での対応箇所は次のとおりです:

サイトの正規 URL(site)

Astro の site 設定キーは、zfb のトップレベル site オプションに直接対応します:

// zfb.config.ts
export default defineConfig({
  site: "https://example.com",
});

設定すると、レンダリング時に globalThis.__zfb.site が利用可能になり、レイアウトで正規 <link> タグや OpenGraph メタを構築できます。defineConfig を参照してください。

プラグインライフサイクル: setup、addAlias、addVirtualModule、injectRoute、addClientEntry

Astro インテグレーションは astro:config:setup フック中に updateConfig、addWatchFile、injectRoute を公開します。zfb での同等物は、プラグインライフサイクルの setup フックです:

import { definePlugin } from "@takazudo/zfb/plugins";

export default definePlugin({
  name: "my-plugin",
  setup({ command, addAlias, addVirtualModule, injectRoute, addClientEntry }) {
    addAlias("@/components/foo", "./src/components/foo.tsx");
    addVirtualModule("virtual:my-data", () =>
      `export default ${JSON.stringify({ key: "value" })}`,
    );
    injectRoute("/preset-page", "./pages/preset-page.tsx");
    addClientEntry("./client/analytics.client.ts");
    if (command === "dev") {
      injectRoute("/api/dev/x", "./scripts/dev-x.ts");
    }
  },
});

addAlias は完全一致のインポート書き換えを行います(プレフィックス一致は将来の改訂で対応予定です)。addVirtualModule は specifier によって合成 ESM モジュールを公開します。injectRoute は dev 専用ではありません。zfb build 中は package-owned route が build overlay に materialize され、通常の route pipeline で prerender されます。zfb dev 中は dev renderer が配信する合成ルートになります。dev だけに存在させたいルートのときだけ、command === "dev" でガードしてください。addClientEntry は client bundle 向けの package-owned *.client.{ts,tsx,js,jsx} side-effect entry を登録します。完全な契約については プラグイン を参照してください。

postBuild でのルートマニフェスト(ctx.routes)

Astro は injectRoute + astro:build:done のパターンでビルド後の処理を公開します。zfb では、postBuild プラグインが ctx.routes を受け取ります — これはビルドが出力したすべての URL の完全なマニフェストです:

postBuild({ outDir, routes }) {
  const htmlRoutes = routes.routes.filter((r) => r.extension === "html");
  // write sitemap, feed, etc.
},

ZfbRouteManifest の完全な形状と、サイトマップの実例については プラグイン のページを参照してください。

Markdown: 目次(Table of Contents)、外部リンク、CJK フレンドリーな強調

Astro ユーザーはしばしば remark-toc、rehype-external-links、CJK パッチをインテグレーションとして導入します。zfb では、これらは markdown 設定ブロック内の組み込みのオプトインとして提供されています:

// zfb.config.ts
export default defineConfig({
  markdown: {
    toc: { heading: "TOC", maxDepth: 2 },
    externalLinks: { target: "_blank", rel: ["noopener", "noreferrer"] },
    cjkFriendly: true, // on by default
  },
});

詳細は Markdown のカスタマイズ を参照してください。

シンタックスハイライト用のカスタムテーマファイル(codeHighlight.themesDir)

Astro の shiki インテグレーションは任意の Shiki テーマ名を受け付けます。zfb は syntect(Sublime Text 互換の .tmTheme ファイル)を使います。codeHighlight.themesDir に .tmTheme ファイルを含むディレクトリを指定してください:

export default defineConfig({
  codeHighlight: {
    themesDir: "./themes",
    theme: "Dracula",
  },
});

シンタックスハイライトを参照してください。

ユーザースペースに移行するパターン

Astro の機能の一部には、zfb エンジンの一対一の API がありません。以下では、zudo-react または通常のルートとレイアウトの合成で、近い振る舞いを記述する方法を示します。

client:media — メディアクエリによる条件付きハイドレーション

Astro の client:media="(max-width: 768px)" は、クエリが一致するまで島の起動を遅らせます。zfb では島の境界に when="media" と必須のクエリを指定して、同じタイミングを表します。

import { Island } from "@takazudo/zfb";
import MobileMenu from "./MobileMenu";

export default function Header() {
  return <Island when="media" media="(max-width: 768px)"><MobileMenu /></Island>;
}

クエリが最初に一致したときに島が起動します。このクエリは起動をスケジュールするものであり、その後の変化に応じてコンポーネントを繰り返しマウント、アンマウントしません。

ページごとの <head> 追加(Astro の <Fragment slot="head">)

Astro はページから <Fragment slot="head"> を使って任意の <head> 要素を追加できます。zfb エンジンにはスロット機構がありません。

ランタイムには context や子からレイアウトへの登録機構もありません。head の記述をレイアウトに明示的に渡せば、データの流れがレンダリング順序に依存しません。

// layouts/base.tsx
import type { Child } from "@takazudo/zfb/zudo-react";

export default function BaseLayout({ children, head }: { children: Child; head?: Child }) {
  return (
    <html>
      <head><meta charset="utf-8" />{head}</head>
      <body><main>{children}</main></body>
    </html>
  );
}
// pages/blog/[slug].tsx
import BaseLayout from "../../layouts/base";

export default function BlogPost({ post }) {
  return (
    <BaseLayout head={<><title>{post.data.title}</title><meta name="description" content={post.data.description} /></>}>
      <article>{/* ... */}</article>
    </BaseLayout>
  );
}

コレクションデータからメタデータを得るルートでは、ルート内で値を計算し、結果の記述を head コンテンツとして渡します。

Astro コンポーネント用ブリッジエイリアスの除去

Astro プロジェクトでは、.astro コンポーネントを JSX の島に橋渡しするため、@/components/svg や @/components/responsive-image のようなエイリアスを登録することがあります。

zfb ではコンポーネントは .tsx です。橋渡しされていた各コンポーネントを書き換え、その .tsx 実装を import してください。中間のエイリアスは通常、削除または簡略化できます。

// Before (Astro workaround — remove):
// addAlias("@/components/svg", "./src/components/astro-svg-bridge");

// After (zfb — import directly or use a simple alias):
addAlias("@/svg", "./components/svg.tsx");

エイリアス先が .astro ファイルなら、先に .tsx コンポーネントとして書き換えます。

data-island マーカーによるカスタムハイドレーション

Astro の島は、レンダリング済み HTML の隣にある <script type="application/json"> に props を JSON として保存します。独自の data-island 属性を足すプロジェクトもあります。

カスタムマーカーは別のウィジェットシステムに属せますが、zfb の島境界ではありません。zfb のクライアントランタイムが所有するコンポーネントには <Island> ヘルパーを使います。ラッパーの識別子、厳格な JSON props の受け渡し、スケジュール、クライアントエントリが用意されます。

// pages/blog/[slug].tsx
import { Island } from "@takazudo/zfb";
import MyWidget from "./my-widget";

export default function BlogPost() {
  return (
    <article>
      <Island when="visible"><MyWidget label="Click me" /></Island>
    </article>
  );
}

島の子は登録済みの同期コンポーネント 1 つに限られ、props は厳格な JSON データでなければなりません。不完全な data-zfb-island ラッパーを手書きしないでください。永続化ラッパーには Island APIに記載された完全な transport メタデータが必要です。

カスタム remark / rehype プラグイン

Astro は astro.config.mjs の remarkPlugins / rehypePlugins を介して任意の remark プラグインや rehype プラグインを受け付けます。

zfb の Markdown パイプラインは Rust バックエンドです。remark や rehype 向けの npm レベルのプラグインローダーはありません。Markdown レベルのカスタマイズは、バイナリにコンパイルされる in-tree の Rust ビジター — MdastVisitor または HastVisitor の実装 — として提供します。

ほとんどのサイトにとって、これは移行の妨げにはなりません。よく使われる組み込みの remark プラグイン(TOC、外部リンク、CJK 修正)は、すでに zfb.config.ts のファーストクラスのオプションとして利用可能です。より固有のことのためにカスタムプラグインに依存していた場合は、in-tree Rust ビジターの道筋について Markdown パイプラインの拡張 を参照してください。

Revision History

作成更新