zfb
GitHub リポジトリ

検索したい単語を入力

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

Astro からの移行

作成 2026年6月24日Takeshi Takatsudo

既存の 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 ファイルです。zfb.configframework フィールドで Preact か React のどちらかを選択し、レイアウト・ページ・コンポーネントのすべてで同じ JSX の流儀を使います。

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

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

コンテンツコレクション

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

import { getCollection } from "zfb/content";

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

返される形状も似ています。各エントリは slugdata(フロントマター)、body を公開します。全体の仕様については /api/get-collection を参照してください。

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

// 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" はなく、オプショナルを表す末尾の ? もありません)。スキーマは設定読み込み時に構造が検証されますが、フロントマターのフィールドはビルド時にスキーマに対して現状では強制されません。完全な形状については defineConfig を参照してください。

アイランド

Astro はインポートしたフレームワークコンポーネントの JSX 属性として client:loadclient:visible などを使います。zfb はその代わりにファイルレベルの "use client" ディレクティブを使います — Next.js App Router が広めたのと同じパターンです:

"use client";

export default function Counter() {
  const [n, setN] = useState(0);
  return <button onClick={() => setN(n + 1)}>{n}</button>;
}

"use client" ファイルからインポートされたものは、自動的にアイランドになります。/api/island を参照してください。

スロットとレイアウト

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

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

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

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

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

  • アイランド / クライアントディレクティブ"use client" ファイルディレクティブに 置き換えられています(Next.js App Router のパターン)。アイランドを参照してください。

現時点で対応物がない機能もあります。インテグレーションのマーケットプレイス、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 を参照してください。

プラグインライフサイクル: setupaddAliasaddVirtualModuleinjectRoute

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

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

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

addAlias は完全一致のインポート書き換えを行います(プレフィックス一致は将来の改訂で対応予定です)。addVirtualModule は指定子(specifier)によって合成 ESM モジュールを公開します。injectRoute は開発専用で、zfb build 中に呼び出すとエラーになります。完全な契約については プラグイン を参照してください。

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-tocrehype-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",
  },
});

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

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

zfb はいくつかの Astro 機能を意図的に省略しています。これは見落としではありません — これらのパターンは、純粋な JSX/Preact モデルにおいてエンジンのサポートを必要としないクリーンなユーザーランドの解決策を持っています。以下の各セクションでは、それぞれについて推奨されるレシピを示します。

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

Astro の client:media="(max-width: 768px)" は、メディアクエリがマッチするまでハイドレーションを遅延させます。zfb の "use client" ディレクティブは常にロード時にハイドレーションします。

Preact での同等物は、コンポーネント自身の内部での早期リターンです:

"use client";

import { useEffect, useState } from "preact/hooks";

interface Props {
  children: preact.ComponentChildren;
}

/** Renders children only on viewports that match the query. */
export default function MediaMount({ children }: Props) {
  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}</>;
}

これはラッパーとして使います: <MediaMount><MobileMenu /></MediaMount>。ラップされたコンポーネントはクエリがマッチしたときにのみマウントされ、クエリがマッチしなくなるとアンマウントされます。

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

Astro はページが <Fragment slot="head"> を介して任意の <head> 要素を注入できるようにします。zfb にはエンジンレベルでのスロット機構はありません。

Note

PageMeta は現在 unknown フィールドを拒否します。将来この制約が緩和されれば、このレシピはよりシンプルになります。

ユーザーランドのパターンは Preact コンテキストベースのヘルメット です。レイアウトがまずページボディをレンダリングし(コレクターを満たし)、その後収集されたヘッドノードを出力します。これにより 2 回目のレンダーパスを避けられます:

// components/head-context.tsx
import { createContext } from "preact";
import { useContext, useRef } from "preact/hooks";

interface HeadContextValue {
  nodes: preact.VNode[];
  add(node: preact.VNode): void;
}

export const HeadContext = createContext<HeadContextValue>({
  nodes: [],
  add() {},
});

export function useHead() {
  return useContext(HeadContext);
}
// components/head.tsx — drop inside a page to register head nodes
import { useHead } from "./head-context";

interface Props {
  children: preact.VNode | preact.VNode[];
}

export function Head({ children }: Props) {
  const { add } = useHead();
  const registered = useRef(false);
  if (!registered.current) {
    registered.current = true;
    const nodes = Array.isArray(children) ? children : [children];
    nodes.forEach(add);
  }
  return null; // renders nothing inline
}
// layouts/base.tsx
import { useState } from "preact/hooks";
import { HeadContext } from "../components/head-context";

export default function BaseLayout({ children }: { children: preact.ComponentChildren }) {
  const [nodes] = useState<preact.VNode[]>([]);
  const ctx = { nodes, add: (n: preact.VNode) => nodes.push(n) };

  // Render children first so Head calls populate `nodes` before we emit <head>.
  const body = <main>{children}</main>;

  return (
    <HeadContext.Provider value={ctx}>
      <html>
        <head>
          <meta charSet="utf-8" />
          {nodes}
        </head>
        {body}
      </html>
    </HeadContext.Provider>
  );
}
// pages/blog/[slug].tsx
import { Head } from "../../components/head";

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

SSR のレンダー順序

このパターンは、Preact が同じパス内で兄弟要素より先に children をレンダリングすることに依存しています。zfb の静的ビルド SSR(トップダウンのレンダリング、親の {nodes} 出力より先に children をレンダリング)では機能します。ストリーミング SSR アダプターを追加する場合は、ボディのレンダリングが完了した後にヘッドがフラッシュされることを確認してください。

Preact-compat エイリアス(@/components/svg@/components/responsive-image

一部の Astro プロジェクトは、Astro のコンポーネントモデルを回避するために @/components/svg@/components/responsive-image のようなエイリアスを登録します — 例えば .astro の SVG コンポーネントを JSX アイランドへ橋渡しするためです。

zfb ではすべてのコンポーネントが最初から .tsx であり、これらのエイリアスが提供していたブリッジレイヤーは存在しません。移行時には、これらのエイリアスを削除し、コンポーネントをファイルパスで直接インポートしてください(あるいは .tsx ファイルを直接指すよりシンプルなエイリアスを使ってください):

// 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 コンポーネント(.astro)だった場合は、先にそれを .tsx コンポーネントとして書き直す必要があります。

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

Astro のアイランドは、レンダリングされた HTML の隣にある <script type="application/json"> タグ内に、コンポーネントの props を JSON としてシリアライズします。一部のプロジェクトは、きめ細かな制御のためにこれをカスタムの data-island 属性で拡張します。

このパターンは zfb にバイト単位でそのまま移植できます — ハイドレーション戦略の全体を自分で所有します。マーカーをクエリして Preact の hydrate() を呼び出す、自前の <script> を用意してください:

"use client";

// components/island-hydrator.tsx — add to your root layout as a "use client" island
import { hydrate } from "preact";
import { useEffect } from "preact/hooks";
import MyWidget from "./my-widget";

const REGISTRY: Record<string, preact.ComponentType> = {
  "my-widget": MyWidget,
  // add more components here
};

export default function IslandHydrator() {
  useEffect(() => {
    document.querySelectorAll<HTMLElement>("[data-island]").forEach((el) => {
      const name = el.dataset.island!;
      const Component = REGISTRY[name];
      if (!Component) return;
      const props = JSON.parse(el.dataset.props ?? "{}");
      hydrate(<Component {...props} />, el);
    });
  }, []);
  return null;
}

その上で、サーバーレンダリングされたマークアップ内で <div data-island="my-widget" data-props='{"label":"Click me"}' /> を出力します。

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

Astro は astro.config.mjsremarkPlugins / 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

Takeshi Takatsudo作成: 2026-06-25T05:17:25+09:00更新: 2026-06-25T05:17:25+09:00

AI Assistant

Ask a question about the documentation.