Case Study — Ambulatory Endoscopy Center

Staff still edit WordPress.
Patients never load it.

An outpatient endoscopy center was running its public site as a full WordPress install, with PHP, database and plugins serving every patient request. Staff liked editing it. Nobody liked how it loaded. WordPress stayed exactly where it was. Everything in front of it changed.

At A Glance ● live
Sector
Healthcare — outpatient endoscopy
Audience
Patients, caregivers, referring physicians
Stack
Astro 7 · WPGraphQL · Tailwind 4 · Appwrite Sites
CMS at request time
None — the site is static
Editing
WordPress, exactly as before
What Changed

Nothing changed for staff.
Everything changed for visitors.

Replacing a CMS is usually the expensive way to fix a slow site, and it costs the staff everything they know. Keeping the CMS and moving it out of the request path gets the same result without asking anyone to relearn their job.

× A full WordPress site, with PHP, a database and a plugin surface in the path of every patient request
→ Static HTML served from Appwrite Sites on infrastructure we run; WordPress is never in the request path
× Every edit rendered live through PHP on every visit, and so did every theme and plugin update
→ Pressing publish triggers one static rebuild, and a burst of ten edits produces one build, not ten
× A slow or unreachable CMS took the public site down with it
→ The pages are already built, so a CMS outage is a staff problem, not a patient one
× Images left over from the old site quietly 404'd, and shifted the layout as they loaded
→ Legacy URLs rewritten at build, with dimensions and lazy loading injected into CMS-authored images
× Meta descriptions written by hand, when they were written at all
→ Derived from the page's own content at build time, so editors get correct tags by doing nothing

Three rules the
build runs on.

01

The CMS is a build-time dependency

Staff keep the editorial surface they already know. WordPress exposes its content over GraphQL, the site fetches all of it while building (pages, providers, procedures, and the navigation menu itself) and ships plain HTML. Patients never touch WordPress. Every other decision here follows from that one.

02

A build that refuses to guess

Every response from the CMS is validated before it is used, so a renamed field stops the build loudly instead of quietly rendering a page that is wrong. Editor HTML is treated as untrusted on the way in. And a production build never invents content: if a fetch fails, the result is a not-found page, not a placeholder that looks real.

03

Publishing is a build, not a request

An editor pressing publish is the only thing that changes the public site. A small WordPress plugin notices the change, waits out a short quiet period so a burst of edits becomes one job, and asks the host for a fresh build. Nothing about that requires an editor to know it is happening.

The CMS could be down all week. Patients would never know.

That is what fetching content at build time actually buys. The pages a patient loads were finished the last time an editor pressed publish, so a plugin update, a slow database or an expiring certificate on the editorial side is a staff inconvenience rather than an outage on the public site. The honest flip side: a build does need WordPress reachable while it runs. When it isn't, the build fails and the previous site stays up, which is the failure mode you want.

Inside The Build

Making a build-time
dependency safe.

Moving the CMS out of the request path moves the risk into the build. A naive version of this architecture ships a broken site the first time WordPress is slow. Most of the engineering here is about that.

Publish once, build once

A small WordPress plugin we wrote calls the hosting API when published content changes. Edits are debounced through a 90-second scheduled event, so a burst of ten saves produces one build rather than ten. A system cron keeps that scheduler ticking even when no editor has the admin open.

Validating what the CMS sends

WordPress schemas drift: fields get renamed, field groups get restructured, and IDs come back inconsistently typed. Every GraphQL response is checked against a schema at build time, so a shape change becomes a loud, catchable failure instead of a page that quietly renders wrong.

Treating CMS HTML as untrusted

Editor HTML is injected into pages, which makes it a real attack surface if the CMS trust boundary ever moves. A sanitizing pass strips scripts, frames, forms, event handlers and javascript URLs. While it is in there, it also rewrites broken legacy upload URLs and adds lazy loading to images. It is pattern-based rather than a full HTML parser, which is a deliberate trade-off for a single-tenant site with trusted editors.

One way in, with a timeout

Every CMS query routes through a single client with a ten-second timeout and two retries on a backoff. Content fetching had started out duplicated inline across a dozen page files; consolidating it into typed services is what made timeouts, retries and validation a single change rather than twelve, and it later made a major framework upgrade a non-event.

A CI build that doesn't need the CMS

Continuous integration runs a full build against a local mock GraphQL server on every push. Query and schema breakage is caught without depending on the live CMS being reachable from a build runner, and a broken build never reaches the site.

Failure states that stay out of the index

Not-found and fallback states carry noindex, so a bad build cannot leave a stub page in search results. Every page gets a meta description generated from its own content. Fonts are self-hosted, the hero image is prioritised for first paint, and every image carries explicit dimensions so nothing shifts as it loads.

From Publish To Live

What happens when
an editor hits save.

The whole point of keeping WordPress was that staff would not have to learn anything new. So the pipeline between their publish button and the public site has to be invisible to them, and safe when something goes wrong.

01

An editor presses publish

In wp-admin, exactly as before. It could be a page, a provider bio, a procedure, or a menu item; the plugin only cares that published content changed.
02

The plugin waits ninety seconds

A scheduled WP-Cron event collapses a flurry of saves into one job. A system cron keeps that scheduler ticking when no editor has the admin open, so nothing waits for the next visitor to wp-admin.
03

The host builds the site

Appwrite Sites pulls every page, provider, procedure and the menu over GraphQL, validates each response against its schema, sanitizes editor HTML, and writes plain files. A bad response fails the build here, not in a patient's browser.
04

The old site stays up until the new one is ready

A failed build changes nothing. A successful one swaps in a complete set of pages. Either way, the CMS could go offline the next minute and the public site would not notice.
How It Got There

Six moves,
one decision.

The shape of the site was decided in the first month and never revisited. What the rest of the time bought was the discipline around it: validation, sanitization, and a deploy that no longer depends on anyone remembering to press it.

Out from under WordPress

The site was a full WordPress install, themed and served by PHP on every request. Homepage and facility page rebuilt as static pages reading from the same WordPress content, hosted on Appwrite Sites from day one. The shape of the site (static output, CMS at build time only) was fixed here and never revisited.

The whole site over GraphQL

Providers, procedures and administrators modelled as custom post types and pulled through WPGraphQL. Home, contact, quality and facility pages parsed from Gutenberg blocks. Navigation generated from the WordPress menu, so editors keep control of it.

Hardening sprint

Content fetching, which had grown up duplicated across page files, consolidated into typed services with timeouts and retries. Schema validation, HTML sanitization, centralised configuration, environment-aware logging and a mocked-CMS CI build all landed here, alongside self-hosted fonts, image dimensions and generated meta descriptions.

Accessibility pass

Contrast, a working skip link, reduced-motion guards, iframe titles, and heading and landmark structure, with a browser-driven test suite for keyboard navigation and reflow left in the repository so it stays that way.

Astro 7

Major framework upgrade, plus redirect and broken-link fixes. The earlier consolidation is what made this uneventful.

Publish-to-build

The WordPress plugin went live, so an editor's publish is what builds the site, with no developer or deploy button involved. A nondiscrimination notice was added.

Built with Astro 7, Tailwind CSS 4, Alpine.js, TypeScript, graphql-request, zod, WordPress with WPGraphQL and WPGraphQL Gutenberg, Appwrite Sites, and Node 22. This is website redesign and WordPress migration work for healthcare, and the reason it keeps working is ongoing maintenance.

Keep The CMS. Lose The Weight.

Your editors are not the problem.

Tell us what your team edits today and what your site has to do for the people who visit it. We will tell you whether the CMS needs replacing or just needs getting out of the way, and what either one costs.