zfb

Type to search...

to open search from anywhere

Build Pipeline

CreatedJun 1, 2026Takeshi Takatsudo

An end-to-end tour of how zfb turns your project into a built site.

zfb is built as a set of small Rust crates that each own one job. This page is a tour of the whole pipeline at the level of “which crate does what, and in what order” — enough to give you a mental map without diving into any single piece.

The crates, in order

  1. CLIcrates/zfb. Parses command-line args, loads zfb.config.{ts,json}, and dispatches to the dev, build, or preview command.
  2. Routercrates/zfb-router. Scans pages/ and produces the route table. See Routing.
  3. Watchercrates/zfb-watcher. Dev mode only. Observes pages/, content/, components/, layouts/, styles/, data/, public/, and zfb.config.ts, and emits a stream of change events.
  4. Dependency graphcrates/zfb-graph. Tracks page-to-source dependencies. When the watcher reports a change, the graph answers a single question: which pages need to be rebuilt?
  5. Build orchestratorcrates/zfb-build. Coordinates the per-page work and writes outputs atomically via atomic_write_string, so a partial write can never leave dist/ in a half-broken state.
  6. Renderercrates/zfb-render. Compiles TSX to JS with SWC, evaluates the resulting ES module via the JS runtime host, and calls the page’s default() export to produce an HTML string. Content collection entries (.md / .mdx) are compiled through the same pipeline as JSX modules — the renderer resolves mdx://<collection>/<slug> specifiers and surfaces them to user pages as entry.Content (see MDX Components).
  7. CSS pipelinecrates/zfb-css. Runs Tailwind v4 and PostCSS as needed. See Styling.
  8. Islands pipelinecrates/zfb-islands. Bundles each "use client" component as a separate ESM module via esbuild. See Islands.
  9. Servercrates/zfb-server. Dev mode only. An axum-based HTTP server that serves the page cache as static files, plus an SSE channel at /__zfb/reload for browser reload events (see Dev mode lifecycle for the full event-type breakdown).

Production: zfb build

zfb build runs the pipeline once and writes a complete site to outDir:

CLI → Router → Graph → Build orchestrator → Renderer → CSS pipeline → Islands pipeline.

The watcher and dev server are not involved. Every page is rendered, every CSS bundle is produced, every island is bundled, and the result lands in dist/ (or wherever outDir points). Atomic writes mean an interrupted build leaves the previous dist/ intact rather than a partially overwritten mess. For prerender = false routes, the adapter emits a worker bundle instead of static HTML — see SSR on a Worker (adapter mode) for how that output is structured.

Development: zfb dev

zfb dev runs the same pipeline as a long-lived loop, plus the watcher and server:

CLI → Router → Watcher → Build orchestrator → Renderer → CSS pipeline → Islands pipeline → Server.

When the watcher reports a change, the dependency graph picks the affected pages, the orchestrator rebuilds only those, and the server broadcasts a reload event over the SSE channel. The browser reconnects automatically and reloads.

Preview: zfb preview

zfb preview is the simplest command of the three: it serves the existing dist/ directory as static files on :4321. There is no rendering, no watching, no rebuilding — it exists so you can sanity-check a production build locally before deploying it.

Why the split

The crate boundaries are not accidental. Routing is a different problem from rendering, which is different from watching, which is different from serving. Splitting them means each crate stays small enough to test in isolation, and means the dev loop can swap in optimisations (incremental rebuilds, page caching, partial CSS extraction) without touching the production path. The deeper reasoning lives in /architecture/build-engine.

Revision History