zudo-doc
GitHub リポジトリ

検索したい単語を入力

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

ルーティング規約

作成 2026年4月28日更新 2026年6月20日Takeshi Takatsudo

zfbにおけるpaths()のルート列挙方法 — 同期的、コレクションベース、SSGセーフ。

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

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

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

// 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は不要です。

import { getCollection } from "zfb/content";

export function paths() {
  const docs = getCollection("docs"); // 同期的 — 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は発生しません。

なぜ同期的なのか?

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

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

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

// ❌ アンチパターン — 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():完全なdocsパターン

zudo-docで最も一般的なpaths()の使い方は、docsキャッチオールルートです。 各エントリのスラグはURLセグメントのstring[]にマッピングされます:

// 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/configurationparams.slug === ["guides", "configuration"]でマッチします。 slug.join("/")で再結合してgetEntryのルックアップキーを再構築します。

AstroのgetStaticPathsからの移行

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

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

// ❌ Astro — 移行前
// 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ファイルに書き込みます:

// scripts/fetch-tags.ts — ビルド前のステップとして実行
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),
  );
}

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

// ✅ zfb — 移行後
// pages/tags/[tag].tsx
import { getCollection, getEntry } from "zfb/content";

export function paths() {
  const tags = getCollection("tags"); // 事前生成されたコンテンツを読み込む
  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 getStaticPathszfb paths
シグネチャasync function getStaticPaths()function paths()
ランタイムNode.jsminiflare
データアクセスawait getCollection(...) または await fetch(...)getCollection(...) / getEntry(...) (同期)
ライブHTTPフェッチ許可(SSGでは非推奨)ビルド前ステップに移動
返り値の形状{ params, props }[]{ params, props }[]
下書き除外getStaticPaths内でフィルタリングpaths()内でフィルタリング

関連ページ

Revision History

Takeshi Takatsudo作成: 2026-04-28T20:38:07+09:00更新: 2026-06-20T07:20:58Z

AI Assistant

Ask a question about the documentation.