Skip to content

The Build: One Repo, Nine Sites

Deploy is copying files; this page is about where those files come from. Before anything reaches Cloudflare, a build has to turn source into the dist/ folder the previous page called the one artifact that ships. In this repo that build is a small, disciplined system: one command produces nine sites, and it does so fast and reproducibly. Understanding it is understanding the “CI” in CI/CD applied to a static monorepo.

There are nine books — DevOps, Bitcoin, Rust, System Design, and more — each an independent Astro site under apps/. They could have been nine separate repositories. Instead they share one repo, one lockfile, one build tool, and one shared UI package (packages/ui). The payoff is the DRY argument at the repo scale:

  • A fix to the shared Starlight theme or a shared React component lands once and every book gets it.
  • One bun.lock pins the same dependency versions across all sites — no “works in Bitcoin, breaks in DevOps” version drift.
  • One CI config builds and checks all of them together, so a change that breaks another book is caught in the same pull request that made it.

The cost is that the build must be smart enough to not rebuild all nine when you only touched one. That is exactly what the two tools below buy.

Bun workspaces: one install, many packages

Section titled “Bun workspaces: one install, many packages”

The repo is a Bun workspace — a single bun install at the root resolves dependencies for every app and package at once, hoisting shared ones and symlinking the internal @first-principle/ui package so each book imports it like a normal dependency ("@first-principle/ui": "workspace:*"). The workspace:* protocol means “use the local copy in this repo,” so the shared UI is never a published package you have to version and re-release — it is just there, always current.

This is why the deployment docs insist the Cloudflare build command begins with bun install &&: Cloudflare’s build pipeline does not auto-install workspace dependencies, so without it the build fails with astro: command not found. The workspace is the thing that makes astro resolvable at all.

bunx turbo run build is the one command that builds everything. Turborepo is a task runner that understands the dependency graph between packages. Two properties matter for deploys:

  1. It builds in dependency order. packages/ui builds before the apps that import it, automatically, because Turbo reads the graph. You never hand-order the build.
  2. It caches by input hash. Turbo hashes each task’s inputs (source files, dependencies, env). If nothing that feeds a task changed, it replays the cached output instead of rebuilding. Touch only the DevOps book and the other eight are cache hits — near-instant.
bunx turbo run build
─────────────────────
packages/ui ─┐ (built once)
├─► apps/bitcoin ─► apps/bitcoin/dist
├─► apps/devops ─► apps/devops/dist
├─► apps/rust-* ─► …/dist
└─► … 9 apps total ─► one dist/ per app
changed only devops? → ui + 8 apps = CACHE HIT, only devops rebuilds

That cache is the same idea as a Docker layer cache from Part 3 — hash the inputs, reuse the output when they’re unchanged — applied to whole build tasks instead of image layers. It is what makes “build all nine on every push” cheap enough to actually do on every push.

For a single-app deploy you don’t want all nine. Turbo’s --filter scopes the build to one package and its dependencies:

Terminal window
# Build just this book (and packages/ui, because devops depends on it):
bunx turbo run build --filter=devops

Each app’s output lands in its own apps/<app>/dist, and each app’s package.json carries a deploy script that points wrangler at that folder and that Cloudflare project. Build is per-app, deploy is per-app, URL is per-app — the “one project per app” decision from the overview shows up here as clean, independent build targets.

For this book, apps/devops/build (Astro) renders every Markdown/MDX page under src/content/docs/ to HTML, bundles the interactive React/MUI components (the accordions, the error-budget calculator) into hashed JS in /_astro/, copies everything in public/ verbatim (including the crucial _headers file — see the next-but-one page), and generates the Pagefind search index. The result is a self-contained dist/ that needs no server to serve — exactly the frozen artifact the model requires.

  1. The nine sites could have been nine repos. Give two concrete benefits of the single-monorepo choice, and the one cost it imposes on the build system.
  2. What does workspace:* mean for @first-principle/ui, and why does it save you from publishing and versioning that package?
  3. The Cloudflare build command starts with bun install &&. What breaks without it, and why?
  4. Explain Turborepo’s caching in one sentence, then name the earlier-Part concept it most resembles.
  5. You edit one paragraph in the DevOps book and push. Roughly how much of the nine-site build actually re-runs, and why?
Show answers
  1. Benefits (any two): a shared fix (theme/component/dependency) lands once and reaches every book; one lockfile pins identical versions across all sites so there’s no cross-book drift; one CI run builds and checks all books together so cross-book breakage is caught in the same PR. Cost: the build tool must be smart enough to rebuild only what changed instead of all nine — solved by Turborepo’s graph + cache.
  2. workspace:* tells Bun to resolve @first-principle/ui to the local copy in this repo rather than a published registry version. Because it’s always the current local source, you never publish, version, or re-release it — edits are immediately live for every app that imports it.
  3. Cloudflare’s build pipeline does not auto-install the workspace’s dependencies, so astro (and every other dep) is absent; the build fails with astro: command not found. bun install at the root resolves the whole workspace so the build tools exist.
  4. Turbo hashes each task’s inputs and replays cached output when the inputs are unchanged, only rebuilding tasks whose inputs changed. It most resembles Docker image-layer caching (hash inputs, reuse the layer if unchanged) from Part 3.
  5. Only the DevOps app rebuilds (plus packages/ui if it wasn’t already cached). The other eight apps are cache hits because none of their inputs changed, so Turbo replays their prior dist/ instead of rebuilding them.