Skip to content

ADR-0005: Deploy via Cloudflare Pages git integration instead of GitHub Pages

Listen to this page4:11
Read the transcript

1. The subpath tax of GitHub Pages

Host: Welcome back to the show. Today we’re digging into ADR-0005, a decision about why the Principal AI Engineer Handbook packed up and left GitHub Pages for Cloudflare Pages. And it turns out the root cause is this sneaky little thing called a subpath, which I think a lot of people don’t realize is even a problem until it bites them.

Guest: Right, so GitHub Pages serves a project repo — one that isn’t named username.github.io — from a subpath, so the site was living at vins13pattar.github.io slash principal-ai-engineer-handbook. To make that work at all, astro.config.mjs had to set base to that same prefix. That part’s fine, Astro handles it for anything it generates itself, like sidebar links or pagination.

Host: But that’s not the whole story, is it — what about links people actually typed into the content?

Guest: Exactly the trap. Astro only auto-applies that base to framework-generated URLs, not to literal hrefs authors write inside MDX. So every single hand-written internal link, something like slash learn slash, had to be manually rewritten to include the full prefix, slash principal-ai-engineer-handbook slash learn slash. Miss one, and it’s a dead link in production.

2. Weighing fixes versus a root-relative rewrite

Host: So once you’ve found forty broken links, the obvious first fix is just to make the base path smarter, right? Rewrite it conditionally per environment so dev and prod get different prefixes automatically.

Guest: That handles the templated stuff, sure, but those forty links are hand-typed strings sitting inside MDX content, not generated by Astro’s routing. Making that work per environment means either maintaining two versions of the content tree or writing a build step whose entire job is rewriting hrefs — that’s a lot of infrastructure for a single-maintainer docs site. We also looked at just buying a custom domain and pointing it at GitHub Pages, so the site would live at the root and the subpath problem disappears entirely.

Host: That sounds simpler on paper — why not just do that instead of switching platforms?

Guest: It fixes the link problem but adds a domain purchase and a DNS dependency to something that’s supposed to be a zero-cost side project, and it still doesn’t get you PR previews, which GitHub Pages just doesn’t do without extra tooling bolted on. Moving to Cloudflare Pages solved both at once: serve from the root so every link is root-relative with zero prefix, and get per-PR preview deployments for free as part of the git integration.

3. What was gained, what still can’t be checked, and what outlived the mechanism

Host: So beyond just fixing the links, what did the team actually gain day to day once this landed? It sounds like the PR preview piece was the bigger unlock.

Guest: Right, every pull request against main now gets a real, browsable preview URL automatically, which GitHub Pages never gave them without extra tooling. The CI workflow still runs lint, typecheck, test, build, link-check, e2e as a gate, but it’s no longer the thing deploying anything — Cloudflare owns deployment end to end. The catch is that ownership is also a blind spot: a broken Cloudflare dashboard setting fails silently from GitHub’s perspective, there’s no workflow-based safety net, so you have to actually go check Cloudflare’s own dashboard to know the site is live.

Host: That’s a fair trade to note honestly rather than bury. So where does this decision stand now — I know we said it got superseded.

Guest: It did, by ADR-0007, because Cloudflare itself moved the ground underneath it — new git-connected projects now go through Workers Builds instead of the Pages flow this was written against, so the specific build settings here no longer apply. But the thing that actually mattered survived untouched: the site is still served from the root, still has no base path, every internal link is still root-relative. Only the deployment mechanism changed; the insight this ADR was really about didn’t.

Not covered

The planner wanted these and found nothing in the source to support them:

  • A deep dive into the Astro/Starlight framework choice itself (ADR-0001) beyond the base-path trade-off it documents
  • Full walkthrough of ADR-0007’s three Workers Builds breakages (Pages config rejection, monorepo auto-detection failure, missing CF_PAGES_URL) since the episode is scoped to ADR-0005’s own reasoning

Generated from this page by Claude Sonnet 5 on , spoken by Kokoro-82M running locally. Two synthetic voices, not a recorded conversation. Every claim is drawn from this page — where it differs from the text above, the text is correct.

ADR-0005supersededAugust 5, 2026

Superseded by ADR-0007. Cloudflare now routes new git-connected projects through Workers Builds rather than the Pages flow this decision was written against, so the build settings below no longer describe how the site deploys.

What survives unchanged is the part that shaped the content: the site is served from a domain root, has no base path, and every internal link is root-relative. Only the mechanism moved.

Originally accepted, and implemented by removing .github/workflows/deploy.yml, removing base from apps/handbook/astro.config.mjs, and adding wrangler.toml.

The platform originally deployed to GitHub Pages: a GitHub Actions workflow (.github/workflows/deploy.yml) built the site and pushed it to Pages. Because GitHub Pages serves a project repository (one not named <user>.github.io) from a subpath — https://vins13pattar.github.io/principal-ai-engineer-handbook/ — the site had to set base: "/principal-ai-engineer-handbook" in astro.config.mjs, and every hand-written internal link in MDX content had to repeat that prefix ([Learn](/principal-ai-engineer-handbook/learn/)), since Astro only applies base automatically to framework-generated URLs (sidebar, pagination, asset paths), not to literal hrefs authors write in content — see the trade-off called out in ADR-0001.

The project is moving to Cloudflare Pages, connected directly to the GitHub repository through Cloudflare’s own git integration (not a GitHub Actions workflow). Cloudflare Pages serves every deployment — production and per-branch previews — from a domain root (https://<project>.pages.dev or a custom domain), never from a subpath. A site built with a GitHub-Pages-shaped base would 404 on every internal link once deployed there.

  • Keep the GitHub Pages subpath base and rewrite it per environment. Astro’s base can be set conditionally at build time, but the ~40 literal internal links already baked into MDX content (not templated) would still need per-environment rewriting, which means either shipping two content trees or a build step that rewrites links — more moving parts than the problem justifies for a single-maintainer OSS site.
  • Keep GitHub Pages as the deployment target and give it a custom domain at the root, so both targets could share one root-relative link scheme. Rejected for now: it adds a domain purchase and DNS dependency to what should be a zero-cost documentation site, and Cloudflare Pages’ per-PR preview deployments are a real capability GitHub Pages doesn’t offer without extra tooling.
  • Move to Cloudflare Pages, serve from the root, and drop the base path entirely. Every internal link becomes root-relative with no prefix, matching how the site is actually served in every environment (local dev, Cloudflare preview, Cloudflare production).

Deploy exclusively through Cloudflare Pages’ git integration. Remove base from astro.config.mjs (Astro defaults to /), remove .github/workflows/deploy.yml, and strip the /principal-ai-engineer-handbook prefix from every internal link across the content tree. site is now process.env.CF_PAGES_URL when present — the exact URL Cloudflare’s build environment provides for the deployment being built, production or preview — falling back to a placeholder domain for local builds.

Cloudflare project settings (configured in the dashboard, not in this repository): build command pnpm build, build output directory apps/handbook/dist, root directory / (repository root, so the pnpm workspace resolves correctly). Node version comes from .nvmrc; the package manager is auto-detected from pnpm-lock.yaml.

  • Internal links are simpler and environment-agnostic (/learn/ works identically in dev, preview, and production) — no base-path bugs are possible because there’s no base path.
  • Every pull request against main gets a real, browsable Cloudflare Pages preview URL, which the previous GitHub Pages setup didn’t provide — .github/workflows/site-ci.yml’s build-artifact upload remains as a CI gate but is no longer the way to preview a change.
  • .github/workflows/site-ci.yml now only verifies the build (lint, typecheck, test, build, link-check, e2e); it no longer deploys anything. Cloudflare owns deployment end to end, which means a Cloudflare account/dashboard configuration this repository’s CI cannot verify — a broken Cloudflare project setting fails silently from GitHub’s perspective. There is no workflow-based safety net for that; the site’s live deployment status has to be checked in Cloudflare’s own dashboard.
  • Moving to a different static host again (or back to GitHub Pages under a subpath) would mean reintroducing a base and re-adding the prefix to every internal link — the same one-time cost paid here, in reverse.