zfbでは、動的ルートやキャッチオールページはすべて `paths()` 関数をエクスポートし、
ビルド時に出力すべき具体的なURLを指定します。
このページでは `paths()` の仕組み、内部で使用すべきもの、避けるべきことを説明します。

## paths()は同期的に実行される

`paths()`はビルド時に**miniflare**内で実行され、**同期的**であることが契約上の仕様です。
関数シグネチャは`async`ではなく`function paths(): PathEntry[]`です。

```tsx
// pages/docs/[[...slug]].tsx
import { getCollection, getEntry } from "zfb/content";

export function paths() {
  const entries = getCollection("docs");
  return entries.map((entry) => ({
    params: { slug: entry.slug.split("/") },
  }));
}

export default function DocsPage({ params }: { params: { slug: string[] } }) {
  const slugPath = params.slug.join("/");
  const entry = getEntry("docs", slugPath);
  if (!entry) return <p>Not found.</p>;
  return <entry.Content />;
}
```

## データアクセスにはgetCollectionとgetEntryを使用する

`paths()`内では、`zfb/content`から`getCollection(name)`と`getEntry(name, slug)`を使ってコンテンツを読み込みます。
どちらも同期的で、`await`は不要です。

```ts
import { getCollection } from "zfb/content";

export function paths() {
  const docs = getCollection("docs"); // synchronous — no await
  return docs
    .filter((entry) => !entry.data.draft)
    .map((entry) => ({
      params: { slug: entry.slug.split("/") },
      props: { title: entry.data.title },
    }));
}
```

これらのヘルパーは**ContentSnapshot**から読み取ります。ContentSnapshotとは、TypeScriptモジュールが実行される前にRustで構築された、全コンテンツコレクションのインメモリスナップショットです。
`paths()`が実行される時点で、全データはすでにメモリ上にあるため、呼び出し時にI/Oは発生しません。

[Dynamic content unavailable in text export]

ContentSnapshotはランタイム起動時にJSONにシリアライズされ、最初のTSXモジュールが評価される前に`globalThis.__zfb`に埋め込まれます。
`getCollection`と`getEntry`はそのインメモリマップから直接読み取るため、非同期の境界を越える必要がなく、呼び出し元に`await`を連鎖させる必要もありません。
Rust ↔ JSブリッジ契約の詳細は[ADR-004](https://takazudomodular.com/pj/zfb/architecture/adr-004-content-bridge)を参照してください。



## paths()内でライブデータをフェッチしないこと

`paths()`は技術的には`async`として書けますが、そうするとSSGの保証が破れます。

```tsx
// ❌ Anti-pattern — live fetch inside paths()
export async function paths() {
  const res = await fetch("https://api.example.com/items");
  const items = await res.json();
  return items.map((item) => ({ params: { slug: item.id } }));
}
```

このパターンにはいくつかの問題があります：

- **非決定論的** — リモートAPIはビルドごとに異なるデータを返す可能性がある
- **再現性の欠如** — 同じコミットから異なるHTML出力が生成される可能性がある
- **オフラインでの障害** — エアギャップ環境やネットワークのないCIでビルドが失敗する
- **ビルド遅延** — ルート列挙のたびにネットワークラウンドトリップが発生する

コンテンツレイヤーはまさにこれを避けるために存在します。
一度フェッチしてビルド時にスナップショットし、そのスナップショットから同期的にクエリします。

## コレクションベースのpaths()：最小限の例

以下のスニペットは、このパターンの最小限の例です — `getCollection`/`getEntry`から直接組み立てた
手書きの`paths()`です。これはzudo-doc自身のdocsキャッチオールルートが実際に行っていることでは
ありません。実際の実装は下記の[パッケージルートの注入とシャドーイング](https://zudo-doc.takazudomodular.com/ja/docs/develop/routing-conventions#%E3%83%91%E3%83%83%E3%82%B1%E3%83%BC%E3%82%B8%E3%83%AB%E3%83%BC%E3%83%88%E3%81%AE%E6%B3%A8%E5%85%A5%E3%81%A8%E3%82%B7%E3%83%A3%E3%83%89%E3%83%BC%E3%82%A4%E3%83%B3%E3%82%B0)を参照してください。
各エントリのスラグはURLセグメントの`string[]`にマッピングされます：

```tsx
// pages/docs/[[...slug]].tsx
import { getCollection, getEntry } from "zfb/content";

export function paths() {
  const docs = getCollection("docs");
  return docs
    .filter((entry) => !entry.data.draft)
    .map((entry) => ({
      params: { slug: entry.slug.split("/") },
    }));
}

export default function DocsPage({ params }: { params: { slug: string[] } }) {
  const slugPath = params.slug.join("/");
  const entry = getEntry("docs", slugPath);
  if (!entry) return <p>Not found.</p>;
  return <entry.Content />;
}
```

`/docs/guides/configuration`は`params.slug === ["guides", "configuration"]`でマッチします。
`slug.join("/")`で再結合して`getEntry`のルックアップキーを再構築します。

## パッケージルートの注入とシャドーイング

実際の`pages/docs/[[...slug]].tsx`ドキュメントルートスタブは、上記の最小限の例のように
`getCollection`/`getEntry`を手書きしません。`routeCtx.buildDocRouteEntries()`と`createChrome()`
でルートを再構築しており、生成されたプロジェクトは通常このファイル自体を持ちません — 詳細は以下を参照してください。

`packageOwnedRoutes`（デフォルトで`true`）が有効な場合、
`@takazudo/zudo-doc`のroutesプラグインはビルド時にドキュメントルートを注入します。
これには`/docs/[[...slug]]`、そのロケール・バージョン別のバリアント、タグとバージョンのルートが含まれます。
したがって、生成されたプロジェクト自身が持つ`pages/`ファイルはほとんどありません。

同じパスでは、プロジェクトのルートが注入されたルートより常に優先されます。
この場合、ビルドログには次のように表示されます。

```text
shadowed by a user pages/ route (user wins); skipping
```

ユーザーファイルが使用され、パッケージルートは注入されません。プロジェクト固有の実装が必要なルートでは、これを意図的に使用します。

`/`ルートは異なります。zfbは`/`の注入を許可しないため、スキャフォールドの`pages/index.tsx`は
`@takazudo/zudo-doc/routes/index`を再エクスポートする1行のファイルです。
これはホームページを提供する仕組みであり、ルートのシャドーイング例ではありません。

スキャフォールド内の自己完結型ドキュメントルートスタブは、明示的なホスト所有のシームとして残っています。
注入された動的ルートが dev で失敗するためではありません。zfb 2.13.1 は注入ルートを
dev と build の両方でレンダリングします。`/` だけは zfb が `/` の注入を許可しない別の例外です。

注入されたルートをシャドーするドキュメントルートスタブをカスタマイズする場合は、
`virtual:zudo-doc-chrome-bindings`から`chromeBindings`もインポートし、`createChrome`へ渡す必要があります。
そうしなければ、ドキュメントページでは`chromeBindingsModule`チャネルが切断されます。
完全なスタブ配線パターンについては[カスタムコンポーネント](https://zudo-doc.takazudomodular.com/ja/docs/guides/custom-components)を参照してください。

## AstroのgetStaticPathsからの移行

Astroからプロジェクトを移行する場合、`paths()`が`getStaticPaths()`の代わりになります。
主な違いは、`paths()`が同期的であり、ビルド実行前にすべてのデータをコンテンツコレクションに
事前ロードしておく必要があることです。

Astroの`getStaticPaths()`は任意の非同期操作を受け付けていました。
次のようなパターンがよく見られました：

```ts
// ❌ Astro — before migration
// pages/docs/tags/[tag].tsx
export async function getStaticPaths() {
  const res = await fetch("https://api.example.com/tags");
  const tags = await res.json();
  return tags.map((tag: { slug: string }) => ({
    params: { tag: tag.slug },
  }));
}
```

zfbでは、これを2ステップのプロセスに変換します。

**ステップ1** — フェッチをビルド時ジェネレータースクリプトに移動します。
`zfb build`の前に実行し、その結果をコンテンツコレクションまたは静的JSONファイルに書き込みます：

```ts
// scripts/fetch-tags.ts — run as a pre-build step
import { writeFileSync, mkdirSync } from "fs";

const res = await fetch("https://api.example.com/tags");
const tags = await res.json();

mkdirSync("src/content/tags", { recursive: true });
for (const tag of tags) {
  writeFileSync(
    `src/content/tags/${tag.slug}.json`,
    JSON.stringify(tag),
  );
}
```

**ステップ2** — `getCollection`を使って`paths()`内でプリフェッチ済みデータを読み込みます：

```tsx
// ✅ zfb — after migration
// pages/tags/[tag].tsx
import { getCollection, getEntry } from "zfb/content";

export function paths() {
  const tags = getCollection("tags"); // reads the pre-generated content
  return tags.map((tag) => ({
    params: { tag: tag.slug },
    props: { label: tag.data.label },
  }));
}

export default function TagPage({
  params,
  props,
}: {
  params: { tag: string };
  props: { label: string };
}) {
  return <h1>{props.label}</h1>;
}
```

デプロイ間で変わらないデータ（例：リポジトリにコミットされた初期データセット）の場合は、
ジェネレータースクリプトを省略して、JSONファイルを直接`src/content/`に配置することもできます。

### 簡易比較

| | Astro `getStaticPaths` | zfb `paths` |
| --- | --- | --- |
| **シグネチャ** | `async function getStaticPaths()` | `function paths()` |
| **ランタイム** | Node.js | miniflare |
| **データアクセス** | `await getCollection(...)` または `await fetch(...)` | `getCollection(...)` / `getEntry(...)` (同期) |
| **ライブHTTPフェッチ** | 許可（SSGでは非推奨） | ビルド前ステップに移動 |
| **返り値の形状** | `{ params, props }[]` | `{ params, props }[]` |
| **下書き除外** | `getStaticPaths`内でフィルタリング | `paths()`内でフィルタリング |

## 関連ページ

- [国際化（i18n）](https://zudo-doc.takazudomodular.com/ja/docs/guides/i18n) — `paths()`を使ったロケール別ルート列挙
- [バージョニング](https://zudo-doc.takazudomodular.com/ja/docs/guides/versioning) — バージョン管理されたdocsとそのルート規約