Skip to main content
Christian Crawford

Case study

One platform for three pharmacy brands.

A team-built React and Next.js pharmacy platform. I've been one of its engineers since November 2023: I built its multi-brand system, made most of its accessibility fixes, and wrote its Playwright test suites.

Role
Senior Software Engineer
Since
November 2023
Focus
Architecture · Frontend · Accessibility
Stack
Next.js · React · GraphQL · Zustand · MUI

What changed

Three brands that stay one platform.

The homepages, September 2026. HealthWarehouse and SpringMeds are live and differ mostly by config. PharmcoRx is built but not launched yet; its header, footer, and homepage are per-brand components on the same platform.

A rule the team builds by

The other engineers follow the smallest-unit rule when they add a brand difference. It's how the platform grows, not just how I built it.

A new brand is a config file

PharmcoRx joined without a structural change, instead of starting as a copy of another brand's app.

A redesign stays in its brand

PharmcoRx got its own typefaces, header, footer, and homepage. Measured against a baseline, the other two brands came out unchanged.

The problem

Three brands, different in real ways.

Each brand has its own colors, typefaces, corners, features, and legal copy. The easy way to handle that is to copy a page and change it. Do that often enough and you have three apps.

HealthWarehouseSpringMedsPharmcoRx
Typefaces
Montserrat
Montserrat
Inter and Hepta Slab
Corners
8px
8px
16px cards, pill buttons
Feature flags on
14 of 18
3 of 18
4 of 18
Legal copy
Shared
Its own
Shared

The rule

Fork at the smallest unit.

A difference between brands goes to the smallest place that can hold it. Config holds data only. Anything that truly differs lives in the smallest per-brand component that can carry it.

  • A value

    A color, a feature flag, the pharmacist's hours

    goes to

    The brand's config

    Pharmacist hours: one shared component reads each brand's hours string.

  • Markup

    A different sentence, or a different structure

    goes to

    The smallest per-brand component

    Shipping copy: each brand writes its opening line; the sentence they share is its own file.

  • Behavior

    What a component does, when only its layout differs

    goes to

    Stays shared, in a hook

    The header: two layouts, one hook for sign-in, the mini-cart, and sign-out.

Where a difference between brands goes. Config and components are both picked when a brand is built, never while it runs.

Picked when a brand is built

Each brand is fixed at build time, for config and components alike. Nothing switches brands while the app runs.

Kept honest by audit

When I audited the per-brand files, two had drifted into byte-identical copies and a third differed by one value. They collapsed into one shared component and a config string.

Where it bent

The rule had boundaries.

PharmcoRx needed more than small forks could carry. These are the costs I accepted, and why.

Four forks are whole components

Of the seven per-brand components, four are large: the header, footer, homepage layout, and logo. PharmcoRx's design changed their structure, not just their values. To keep that fork to markup, I moved the header's behavior into a hook both headers share.

Every build carries every version

The helper that picks a brand's component imports all of its versions, so each brand ships the others' code. Resolving by filename at build time would drop them. I deferred it: more build setup than a handful of small components justified then. It's the next step now that headers fork.

Fonts one brand uses

PharmcoRx adds about 116 KB of fonts. HealthWarehouse and SpringMeds carry about 3 KB of font CSS they never use, because the font loader can't branch on the brand. Measured, and too small to fix yet.

One build per brand

Build-time brands keep runtime simple, but each brand is its own build and deploy. A stale build can pass for the wrong brand: one measurement ran against one, and checking the page title caught it.

The test

PharmcoRx was the test.

It arrived in June 2026 as a config file, env files, and build scripts, with no structural change. Its redesign in September needed its own typefaces, corners, header, footer, and homepage, and the other two brands had to come out unchanged.

  1. 0 · Baseline
  2. 1 · Design system
  3. 2 · Header and footer
  4. 3 · Homepage
  5. 4 · Interior pages
  6. 5 · Content
  7. 6 · Re-measure
CheckHealthWarehouse and SpringMedsPharmcoRx
Theme
Reproduces the previous theme exactly
Its own typefaces, corners, and colors
Header, footer, logo
Footer and logo code moved over byte-for-byte. The header's behavior moved into the shared hook, then sign-in, the mini-cart, and sign-out were exercised on running builds.
Its own versions, on the same hook
axe-core: 42 routes, 5 browser and device profiles
210 of 210 scans clean on HealthWarehouse. SpringMeds wasn't re-scanned.
210 of 210 scans clean
Lighthouse: 6 routes, desktop and mobile
No change beyond run-to-run noise
Meets its targets, except mobile homepage load

Big swings in the sweep were re-run on their own before I trusted them, and none held up. One gap was real, and it predated the redesign: in that measurement, PharmcoRx's mobile homepage took about 7 seconds to show its main image, against a 2.5-second target. The cause was placeholder photos, then still waiting on licensed ones.

Accessibility

What screen readers were actually told.

Audits kept finding markup that looked fine and told a screen reader something wrong. Two examples, from the fixes themselves.

The cart drawer, before and after

Before: four dialogs for one drawer

  • dialog“Shopping cart”

    • dialogno name

      • dialog“Your Shopping Cart”

      • dialognamed by a label that doesn’t exist

After: one dialog, named once

  • dialog“Shopping cart”

    • the cart’s heading and items

The shopping cart drawer, as a screen reader was told about it. The add-to-cart state, with this drawer open, is scanned by axe-core in five browser and device profiles.

The menu's instructions, second attempt

  1. Tried

    Announce it when the menu opens

    A live region spoke the keyboard instructions as the submenu opened.

  2. Why it failed

    Focus moved at the same moment

    Focus jumping into the submenu raced the announcement, and JAWS and NVDA dropped it.

  3. Shipped

    Describe each item on focus

    Each top-level menu item points to hidden instructions with aria-describedby, read on focus, before anything opens: “Press Enter or Space to open…”

Checked by tests, run by hand.

Playwright and axe-core scan every listed route, plus the states people reach through forms, dialogs, and checkout. The suites run by hand, not in CI, and the route script exits successfully even when a scan fails. Today they report problems; they don't block them.

The HealthWarehouse site also carries a third-party WCAG 2.2 accessibility seal.

  • 42routes scanned by axe-core, in five browser and device profiles
  • 210 / 210scans clean in the last recorded run, September 2026
  • 83routes in the SEO regression suite

Next

What I'd change now.

Each of these was a deliberate call at the time. With another pass:

  • Gate the suitesRun the accessibility suites in CI, and make the route script fail when a scan does.
  • Cover every brandSpringMeds has no accessibility script of its own yet.
  • Ship each brand only its own codeResolve per-brand components by filename at build time, the step deferred above.
  • Fix the landmarksThe header and footer sit inside the main content area, so no brand exposes them as banner and footer landmarks. Found while adding tests; left for its own change.

Role

What was mine

  • Builtthe brand system: the configs and the per-brand component helper.
  • Establishedthe smallest-unit rule, which the team now follows when adding brand differences.
  • RanPharmcoRx's redesign in seven measured phases.
  • Movedthe app from the Pages Router to the App Router.
  • Wrotethe Playwright and axe-core suites: routes, interaction flows, keyboard, and SEO.
  • Fixedmost of the accessibility issues audits found; the form-error announcements were shared work with two teammates.
  • Didmost of the frontend work on checkout and payments.
  • Designedgovernance for AI-assisted development: repository contracts, decision logs, lifecycle hooks, and approval checkpoints.