A publication-grade book site, three CI gates, one Docker image — shipped in 34 days.
Outchurched is an editorial book-series publication site for a long-form non-fiction project. We took it from an empty repository to a production launch on <code style="font-family:var(--font-mono);color:var(--accent-400)">outchurched.website</code> in 34 days — Astro 5 static output, MDX content collections behind a content submodule, three CI gates (unit · E2E against the built image · Lighthouse), and a single-image Docker deploy. 610 commits, 122 merged PRs, 100% merge rate, median 6-minute time-to-merge.
Publishers and authors who want a custom editorial home for a book have three bad choices: a Substack/Medium tenancy they don't own, a WordPress install that's hostile to long-form typography and version-controlled prose, or a generic SSG theme that wasn't designed for chapter-by-chapter reading. None of them treat the manuscript as version-controlled content with a typed schema, a chapter reader as a first-class UI surface, or a deploy story that an editorial team can reason about. The work is to build an opinionated publication-grade content site — typography, JSON-LD, RSS, chapter navigation, build-time validation — without inventing a CMS.
A production publication site on <code style="font-family:var(--font-mono);color:var(--accent-400)">outchurched.website</code>. Astro 5 static output. MDX chapters behind a content submodule so editorial work is a separate review surface from code. Seven typed MDX call-out components. A chapter-reader route (<code style="font-family:var(--font-mono);color:var(--accent-400)">/book/[episode]/[chapter]</code>) with per-episode RSS feeds and book/episode/chapter JSON-LD. Three CI gates: Vitest unit, Playwright E2E against the built production image, and Lighthouse. One Docker image deployed via Traefik with runtime config via build-args. First release tag on day 7, production launch on day 28, corrective patch on day 31.
What we built, and how fast
Outchurched is an editorial book-series publication site built on Astro 5 with MDX content collections. The manuscript and editorial specs live behind ./content/ as a git submodule; the website owns Astro code and the design system. Seven typed MDX call-out components (scripture, pull-quote, aside, key-passage, etc.) give editors structured prose without writing markup. A three-route reader — /book landing, /book/[episode], /book/[episode]/[chapter] — turns long-form non-fiction into a chapter-by-chapter reading surface, with per-episode RSS and book/episodeBook/chapter JSON-LD baked in at build time.
The build itself is the receipt. From first commit fcb2a2f on 2026-04-18 to the v0.3.1 corrective release on 2026-05-19 was 34 days. 610 commits across all refs, 122 merged PRs, 100% merge rate, median 6-minute time-to-merge. 213 of 610 commits carry an explicit WBS task identifier — story-tracking discipline visible in the log. Three CI gates kept the production image honest: Vitest unit, Playwright E2E against the built image, and Lighthouse. Two reverts, one of them a deliberate, structured four-step rollback that preserved the narrative instead of squashing it.
Who it's for, and what was at stake
The audience is publishers, authors, and editorial-tech buyers — people shipping a book or serialized non-fiction who want an opinionated publication home rather than a tenancy on Substack or a generic WordPress theme. The constraint is that the manuscript is the product. It needs to live in version control, pass schema validation at build time, render with publication-grade typography, and be deployable without an editorial team learning a custom CMS. The submodule split — code in the website repo, manuscript and editorial specs in ./content/ — is what makes the editorial review loop independent of the engineering review loop.
Three constraints shaped the build. Astro 5 static output meant every chapter had to validate against a typed Zod schema at build time — no half-formed MDX could ship. The dev container intentionally serves the production-built dist/ rather than running astro dev, so what developers preview locally is the same artifact that goes to production. And the deploy story had to be one image, runtime-configurable via build-args, behind Traefik — small enough for a single editorial-tech operator to reason about end to end.
Two tools, neither finishing the job
Substack, Medium, or a hosted newsletter platform
The fast path. The author writes; the platform hosts; readers subscribe. It works for serialized columns and short-form posts. It collapses for a book: tenancy means you don't own the canonical URL, typography is the platform's house style, structured cross-references between chapters aren't expressible, JSON-LD and per-episode RSS aren't yours to control, and the editorial workflow lives in a web form rather than version control.
WordPress with a long-form theme
The default for self-hosted publishing. Mature plugin ecosystem, full ownership of the domain. The cost is a runtime database where there should be content files, a plugin surface that has to be patched against vulnerabilities the editorial team didn't choose to take on, and a typography stack that fights long-form reading rather than helping it. The manuscript can't be code-reviewed; the deploy story is "log in to wp-admin."
A generic static-site generator with a book template
Hugo, Eleventy, or Jekyll with a community theme gets the typography right and keeps the manuscript in version control. The gap is the editorial surface: typed MDX call-outs, a chapter-reader route as a first-class UI, build-time schema validation of every chapter, per-episode RSS and book/chapter JSON-LD — all of that has to be rebuilt per project, every time. There's no opinionated publication-grade starter that treats those as the floor.
Story-numbered PRs against a WBS, gated by three CIs against the built image
Every PR carried an explicit WBS task identifier — 1B.2, 2B.1, 2L.5, 99S — and merged through three CI gates: Vitest unit, Playwright E2E against the production-built Docker image, and Lighthouse. The dev container serves the same built dist/ as production, so local preview and CI preview are the same artifact.
All code written under a red/green/refactor TDD cycle — failing test first, minimal code to pass, refactor. The receipt: 610 commits and 122 merged PRs in 34 days, with no merged PR shipping without its accompanying unit test. The build-time MDX schema is itself a test surface: a malformed chapter fails the build before it can fail CI.
Owned every branch, merge, PR, and release. The receipt: 122 of 122 PRs merged (100% merge rate), median 6 minutes from PR-open to merge, three clean release tags (v0.1.0-base, v0.3.0-launch, v0.3.1), and a structured four-step home-revert epic rather than a squashed rollback.
Astro 5 content collections, MDX integrations, route file conventions, and image-pipeline rules were invoked as a dedicated skill — framework conventions stayed consistent across the 2B/2C/2D content-build window without the human re-deciding them per PR.
make dev serves the production-built dist/ via nginx; make tools-shell drops into a Node + pnpm container. Host needs only Docker and Compose v2. The dev image and the deploy image share their build stage — what runs locally is what ships.
An editorial book site, not a CMS
Astro 5 static output on <code style="font-family:var(--font-mono);color:var(--accent-400)">outchurched.website</code>. MDX chapters behind a content submodule. A chapter-reader route with per-section background transitions and editorial call-outs. Here is what the v0.3.1 surface looks like.
Editorial typography as the entry point. The landing hero leads with the book's voice — large-format serif typography, a strike-through editorial flourish, and a single italic sub-line — rather than a generic SaaS lede. Astro 5 static output, self-hosted Inter Variable, design tokens driving every dimension. The eyebrow and masthead share a typography contract enforced by tokens.ts.
The chapter-reader route. A three-rail layout: chapter index on the left, prose in the centre column, metadata and cross-references on the right. Editorial call-outs (scripture cards, companion-book asides) are typed MDX components, not raw HTML — so every chapter validates against a Zod schema at build time. Dark theme variant; per-episode RSS feeds shipped alongside.
Per-section background transitions and editorial pull-quotes. The reader supports a lighter section variant in the same chapter — subhead "The Announcement" visible — so the page rhythm tracks the manuscript rather than fighting it. Same left-rail chapter index, same MDX call-out grammar, different visual register. The editorial design system is one set of tokens, two themes.
Four inflection points, two quiet weeks, one structured revert
Commit clustering reveals four inflection points and two zero-or-near-zero commit windows — the gaps correspond to external infra (DNS, VPS, registry) that doesn't leave a commit footprint.
Bootstrap & Cycle 1
Day 1 cycles through two false starts on the content submodule and a schema reorg before settling the shape that still holds: website owns Astro code, manuscript lives behind <code style="font-family:var(--font-mono);color:var(--accent-400)">./content/</code>. Baselines land (<code style="font-family:var(--font-mono);color:var(--accent-400)">.nvmrc</code>, strict tsconfig, pnpm, Astro install). PRs are tiny, story-numbered (WBS 1B.*), merged in 0–17 minutes. <strong>194 commits</strong> get to <code style="font-family:var(--font-mono);color:var(--accent-400)">v0.1.0-base</code> on day 7 via PR #49, with a post-release housekeeping PR (#52) that becomes the canonical release ritual.
The content-build window
The two highest-velocity days in the project — 50 and 54 commits respectively. PRs #57–#74 ship in batches of WBS-tagged tasks: KeyPassage MDX through 2B.7 (barrel + alias + chapters collection), the four book routes (<code style="font-family:var(--font-mono);color:var(--accent-400)">/book</code>, <code style="font-family:var(--font-mono);color:var(--accent-400)">/book/[episode]</code>, <code style="font-family:var(--font-mono);color:var(--accent-400)">/book/[episode]/[chapter]</code>, per-episode RSS), and JSON-LD builders for book/episodeBook/chapter. Cleanest stretch in the history — stories independently mergeable, tests gate behavior, WBS numbers map 1:1 onto PR titles.
Design tokens & the first quiet week
The 2L epic ships in one day (Apr 28) — six PRs merge to <strong>master</strong> rather than develop (the first visible violation of the branching rule, acknowledged and corrected as "Option C"). Token system (#94), chrome (#95), Inter Variable webfont (#97), MDX call-outs (#98), episode landing (#99), chapter reader (#103). Then a six-day quiet window (May 1 → May 7, 1 commit) — almost certainly external infra work (DNS, VPS, registry) that doesn't leave a commit footprint.
The Cycle 99 deploy sprint
May 8 erupts with 11 master-targeting PRs (#132–#139) — a pre-deploy shakedown of real-content layout bugs. <code style="font-family:var(--font-mono);color:var(--accent-400)">fc93c24 — chore(merge): sync develop with master — bring in 30+ fix/* commits</code> is the signal that develop had drifted. The deploy mechanics land next: Epic 99S (Traefik container), Epic 99D (image registry + <code style="font-family:var(--font-mono);color:var(--accent-400)">deploy.yml</code>), Epic 99E (runtime config via build-args). <code style="font-family:var(--font-mono);color:var(--accent-400)">v0.3.0-launch</code> tagged 2026-05-16 at 575 commits on master — 381 past v0.1.0-base.
Home-revert corrective cycle
Two days after launch, the only multi-commit revert epic in the build. Four feature branches and four merge commits structure a deliberate rollback: <code style="font-family:var(--font-mono);color:var(--accent-400)">429d486</code> (offending nine-move home redesign) → <code style="font-family:var(--font-mono);color:var(--accent-400)">bddb9b7</code> (docs scope correction) → <code style="font-family:var(--font-mono);color:var(--accent-400)">8594cde</code> (the revert) → <code style="font-family:var(--font-mono);color:var(--accent-400)">17ea1d1</code> (selective regraft). Could have been one squashed commit; was kept as four reviewable steps. First time master branch protection actually gated CI — the absolute-path test bug in <code style="font-family:var(--font-mono);color:var(--accent-400)">tests/unit/404.astro.test.ts</code> that <code style="font-family:var(--font-mono);color:var(--accent-400)">3abd86e</code> fixed had been latent for weeks.
Story-tracked velocity, gated by three CIs
Every figure below is drawn from git and gh history, read-only, anchored on the 610 commits across all refs in the 34-day window.
Commits per active day — 30 of 53 calendar days
Commit-type distribution · 346 commits
Quality signals
dist/ as production Where the workflow needed a human
A case study that only lists wins isn't evidence. The same git history that proves the pace also marks exactly where judgment had to stay with the human.
The "PRs target develop" rule was breached during 2L
Six PRs merged to master in one day (2026-04-28) during the 2L design-token epic — the first visible violation of the project's branching rule. The recovery was acknowledged in the project memory as "Option C." The breach pattern reappeared briefly on 2026-05-08 with seven more master-targeting fix(...) PRs during the pre-deploy shakedown. The rule stuck after — but the log carries the lesson.
Develop drifted from master through April
fc93c24 — chore(merge): sync develop with master — bring in 30+ fix/* commits is the signal: the branching model worked, but only after a forced sync. Two recovery commits (fc93c24 develop ← master sync and 9836e93 recovery PR #20) are visible in the log. Without master branch protection turned on earlier, the drift was easy to ignore.
One-person bottleneck
Effectively a single human contributor across all 591 human commits (two git identities, same email). PRs ship fast because there's nobody else to review them; CI is the only gate. The May 19 absolute-path test bug — five hardcoded /home/c/ paths that had been latent for weeks — is the predictable failure mode of self-review. Master branch protection finally caught it; it shouldn't have needed to.
Irregular release cadence · no v0.2.x
v0.1.0-base → v0.3.0-launch was 21 days; v0.3.0 → v0.3.1 was 3 days. No v0.2.x ever existed. The numbering jumps from Cycle 1 to Cycle 99 directly — the version string doesn't match the cycle pace, which makes it hard to read release health from the tag history alone.
What this build teaches the next one
Story-numbered PRs are the cheapest project-tracking discipline that exists
213 of 610 commits carry an explicit WBS task identifier; PR titles map 1:1 to stories. That's the reason 122 PRs were mergeable in 34 days without a backlog of stale branches. The cost is one line in each commit message; the payback is a log you can read as a project ledger. If the WBS numbering goes missing for a stretch, that stretch is the one to audit later.
Container-first dev kills "works on my machine" before it can start
make dev serves the production-built dist/; the host needs only Docker and Compose v2. Across 34 days: zero environment-drift incidents, zero "but it builds locally" surprises, zero CI failures rooted in host Node/pnpm versions. For a publication-grade build with a typography + image pipeline at the bottom, this is the difference between shipping and not.
Three CI gates beat one comprehensive one
Vitest unit catches schema and component logic; Playwright E2E runs against the actual production-built Docker image (not astro dev); Lighthouse catches the regressions a unit test can't see. Each gate has a different question and a different failure mode. The 2026-05-19 absolute-path bug was caught by master branch protection invoking all three — once gating was on, the latent bug surfaced in 24 hours.
A structured revert is worth more than a clean log
The home-revert corrective cycle could have been one squashed commit. Instead: four feature branches, four merge commits, four reviewable steps. The rollback narrative is preserved in the log — every step is independently auditable. That's the discipline that lets the next person on the codebase trust the history; squashed reverts forget why.
Editorial and engineering reviews need separate review surfaces
The content submodule (./content/) is what makes the editorial review loop independent of the code review loop. Manuscript edits don't churn the website repo; website edits don't fight the manuscript. The 23 submodule-pin bumps in content (most-churned file) are the visible proof the boundary held — editorial work flowed continuously without touching Astro code.
Have a book or serialized non-fiction that deserves its own home on the web?
Publication-grade Astro + MDX, three CI gates, single-image Docker deploy. Fixed price, fixed timeline, source-code handoff. We'll tell you on a free 30-minute call whether we're the right fit.