Skip to main content
Resources / Case Studies / Outchurched
Case study · Publication-grade content site

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.

Vertical · Publishing · Editorial-tech · Long-form non-fictionStack · TypeScript · Astro 5 · MDX · Vitest · Playwright · Docker · TraefikTimeline · 34 days · 3 release tags · 122 merged PRs
The challenge

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.

The outcome

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.

Executive summary

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.

Context

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.

Why the alternatives fell short

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.

The build

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.

TypeScript (strict)Astro 5 (static output)MDX content collections + Zod schemasInter Variable webfont (self-hosted)VitestPlaywright (E2E against built image)Lighthouse CIDocker (multi-stage) · Docker ComposeTraefik (reverse proxy + TLS)pnpm · Makefile (container-first)Git submodules (content separation)JSON-LD (book / episodeBook / chapter)
tdd-implementation-engineer

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.

git-flow-manager

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-expert · per task

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.

docker · container-first

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.

What shipped

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.

outchurched.website

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.

outchurched.website/book/...

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.

outchurched.website/book/...

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.

The build journey

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.

Apr 18 → Apr 25 194 commits 8 active days · → v0.1.0-base

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.

Apr 26 → Apr 27 104 commits 2 active days · 2B/2C/2D content build

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.

Apr 28 → May 7 ~80 commits 7 active days · 2L design-token epic

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.

May 8 → May 16 ~170 commits 7 active days · → v0.3.0-launch

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.

May 17 → May 19 32 commits 3 active days · → v0.3.1

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.

The receipts · git metrics

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.

34days
start to v0.3.1 corrective release
28days
to production launch (v0.3.0-launch)
610
commits across all refs
122/122
PRs merged · 100% merge rate
6min
median PR time-to-merge
3
release tags (v0.1.0-base · v0.3.0-launch · v0.3.1)
213
WBS-tagged commits · ~35% story-tracked
3
CI gates · Vitest · Playwright · Lighthouse

Commits per active day — 30 of 53 calendar days

InfraSpecSprintReleasesPolish
Apr 18 May 10 May 17

Commit-type distribution · 346 commits

feat
220
fix
140
chore
90
merge
169
docs
25
refactor
15
test
20
revert
2

Quality signals

122/122 PRs merged · 100% merge rate · zero closed-unmerged, zero open
Median 6-minute PR time-to-merge across all 122 PRs · mean 67.4 min
213 of 610 commits carry an explicit WBS task identifier · ~35% story-tracked
Three CI gates — Vitest unit · Playwright E2E against the built image · Lighthouse
Container-first dev — the dev image serves the same built dist/ as production
Content submodule — manuscript and editorial specs version-controlled independently of code
Structured revert — four reviewable steps preserved the rollback narrative instead of squashing it
What we'd flag

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-basev0.3.0-launch was 21 days; v0.3.0v0.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.

Lessons

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.

Build yours

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.