Case Study — Multi-Site Specialty Practice

We inherited their stack.
We left them a site that publishes itself.

The practice came to us as someone else's codebase. Nothing was broken, which is the difficult kind of inheritance — everything was simply fragile, and every fragile part was load-bearing. Today the practice publishes its own content, the site is static files on a CDN, and a broken build cannot reach production.

At A Glance ● live
352 pages rendered at build,
across 58 routes
Sector
Healthcare — multi-site practice
Stack
Astro · Wagtail · Django 5.2 LTS
Fetched at runtime
Nothing — the API can be down
Publishing
Editors publish, the site rebuilds itself
Rollback
One environment variable
What Changed

Same site.
Nothing underneath it is the same.

The practice never asked for a redesign, and never got one. Every rebuild was measured against the live site it replaced. What changed is everything a patient doesn't see and an editor feels every day.

× Every URL served the same empty shell — search engines saw nothing until JavaScript ran
352 real HTML pages, rendered at build time and served complete
× Every visit re-fetched every word of content from the API
Nothing is fetched at runtime — the site stands up even when the API doesn't
× Editors worked in a database admin: no drafts, no preview, no scheduling, no media library
A real CMS — drafts, revisions, preview, scheduled publishing, editor roles
× Publishing a change meant asking a developer to run a deploy script
Hitting publish rebuilds the site on its own
× A production key in the repository and a dependency list that couldn't be rebuilt twice the same way
Pinned dependencies, monitored errors, and framework versions with years of support left

Three things we were
actually trying to fix.

01

Static, all the way down

The site is HTML on a CDN. Content is pulled once, at build time, by a single typed data layer — not by the browser, on every visit, for every visitor. Interactive pieces load as isolated islands; everything else ships no JavaScript at all.

02

A CMS editors can actually use

Wagtail replaced the Django admin panel that had been standing in for one. Fifteen content types, roughly thirty models and 38 raw HTML fields moved to a real authoring surface, with the page tree, previews and permissions that come with it.

03

A build that can refuse to deploy

Every push builds in CI before production moves. Publishing content triggers the same build. A change that breaks the site fails in the pipeline rather than on the site — which was not true of the shell script we inherited.

We built a small CMS inside the Django admin. That is when we knew we needed a real one.

By mid-2026 we had hand-written draft states, a preview mode, and a registry of pages the editors couldn't reach — all of it custom code we would then own and maintain, badly reimplementing what a CMS gives you on day one. The patches were the argument.

How It Got There

Three years,
five moves.

None of these were rewrites for their own sake. Each one had something forcing it: no search visibility, a framework going unmaintained, an end-of-support date, or editors who couldn't publish without us.

Aug 2023

Handover

A Create React App deployed to S3 by a one-line shell script, talking to a Django 3.2 API whose admin panel was the CMS. The first month produced no features — it went on making someone else's system runnable, deployable and reproducible.
Oct 2023

Rebuilt on Gatsby

Prerendered HTML, real metadata, an image pipeline, a build that could fail before it shipped. It landed, was backed out five days later, and re-landed at the end of the month. It did not solve everything: content was still fetched twice, once at build and again in the browser.
2024 – 2026

Two and a half years of quiet work

Framework upgrades, pinned dependencies, error monitoring, automatic rebuilds on publish. The content model kept growing with the practice. And we kept hand-building editor features — draft states, a preview mode, a page registry — into an admin panel that was never meant to have them.
Mar 2026

Rebuilt on Astro

Specified before a line was written, with three rules: as little browser JavaScript as possible, nothing fetched at runtime, and pixel parity with the live site. No redesign, no new features — both explicit non-goals.
Jun 2026

Moved to Wagtail

Django 4.2 reached end of support in April 2026, so the framework upgrade was happening regardless. Rather than pay for two migrations, we bundled the CMS replacement with it — shipping the upgrade as its own release first, so its fallout stayed separate.
How We De-Risked It

Every scary step
had one move back.

Replacing the CMS under a live medical practice is the kind of work that goes wrong quietly. So the cutover was built to be undone: rehearsed on a full copy of production first, and reversible at every step after.

The new API is wrong
The old one stayed mounted and frozen — nothing was removed to make the new one work
The site breaks on the new API
Rollback is flipping a single environment variable
The content move loses something
It copies, never deletes, and re-runs safely to pick up anything that changed since
A row can't be traced to its original
Every migrated record carries an indexed pointer back to the row it came from
Something surfaces a week later
The old tables were kept a month past cutover, alongside a database snapshot
— And then we went looking for what broke

The first full build on the new CMS produced 351 pages with zero errors. That is not the same as the site being right, so we diffed it field by field against the old one and walked it screen by screen.

It found headings being silently stripped out of location pages, fields that had started arriving as objects and rendering as [object Object], formatting collapsed out of older hand-written HTML, and a review widget quietly showing blank cards because the vendor had changed and the new one returns star-ratings with no text. None of those are build errors. All of them are things a patient would have seen.

— We audit our own migrations

The same thing happened a rebuild earlier. Our own post-migration audit of the Astro build, written before launch, opened with a homepage hero shipped as a plain blue gradient with its animation missing, a providers page that had lost its location filter, and one page containing the literal words PATIENTS PAGE and nothing else.

Triaged, fixed, then launched. A migration reported to have gone perfectly is usually one nobody checked closely enough — and parity was only auditable because we refused to redesign anything at the same time.

The shape of this work is legacy system modernization; the site it produced is web application development, and the practice it serves is healthcare software.

Inherited Something?

Someone else's code is still your code.

Send us the repository and the thing that worries you most about it. We will tell you what is fragile, what is urgent, what can wait, and what it costs — before anyone commits to a rebuild.