ルーティング規約
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に埋め込まれます。getCollectionとgetEntryはそのインメモリマップから直接読み取るため、非同期の境界を越える必要がなく、呼び出し元に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 />;
}/はparams.slug === ["guides", "configuration"]でマッチします。
slug.で再結合して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),
);
}ステップ2 — getCollectionを使って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 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()内でフィルタリング |