Doctors
src/config/doctors/ holds nine clinician profiles, one file each, and it is quietly the most load-bearing folder in the theme. Three other systems read it: the health library derives its author list from it, the services detail pages name their teams by slug from it, and the booking endpoint derives a booking’s clinic from it.
That is why a clinician leaving the roster breaks the build rather than leaving a trail of blank spaces.
Two halves of a profile
Each file exports a DoctorProps. The top half is what the directory card needs:
| Field | Notes |
|---|---|
slug |
the URL and the reference other files use |
name, credentials |
printed as one line — credentials is the post-nominals, e.g. MD, FACP |
specialty |
must resolve against the specialty list servicesData owns |
clinic, clinics |
clinic is clinics[0] by construction; the card prints it |
languages, gender |
facets |
rating |
{ score, count } |
nextSlot |
the next-availability line |
photo |
an ImageKey, resolved by siteImages.ts |
tags |
the quick-filter chips this clinician answers to |
The detail half is the profile page: the lede, the photograph and its alt text, the fact pills, the philosophy prose with its “at a glance” table, what they see most, training, publications and availability.
src/config/doctors/sarah-lindqvist.ts is the reference file — every other clinician’s is that one’s shape with their own facts.
Tags are strings, not booleans
tags carries values like "Accepting new patients" and "Telehealth", and they are strings the search box can find rather than boolean fields.
The mechanism is worth knowing because it recurs: a shortcut chip types its own label into the search box. So a tenth chip needs no new field on the profile, no new predicate in the filter, and no new column anywhere — just the string on the clinicians it applies to. The specialty grid works the same way.
The specialty field has to give
One detail in the reference file is instructive. Dr. Lindqvist practises family medicine, and the design’s card says so — but the site’s taxonomy calls that department “Primary care”, because servicesData owns the specialty list and the facet has to resolve against it. So specialty is "Primary care" and the prose below calls the discipline family medicine.
When a card’s copy and a filter’s vocabulary disagree, the filter wins and the prose absorbs the difference. A facet that offers a value no card carries is a filter that returns nothing.
The filter
The directory uses the shared listing filter over cards already in the DOM: a search box, selects for specialty, clinic, language and gender, the shortcut chips, a live count and an empty state. src/components/Sections/Global/_listing.ts is the pure half, checked by _listing.test.ts; _listingFilter.ts is the half that touches the document.
It moved to Global/ on its second user — the doctors node draws the same band as the services node, and only the facets differ. Everything else was about to be a second copy of a hundred lines of subtle logic that no screenshot can check.
The three consumers
The health library. doctorAuthorSlugs is derived from this roster, and content.config.ts refines a post’s author against it. A post naming nobody fails the build; a clinician who leaves breaks the build on every article they wrote. Their byline — credentials · specialty · clinic — and their photograph are read from here at build time, so neither can go stale.
The services pages. A specialty’s team is a list of slugs, so a specialty cannot list a clinician the directory does not have.
The booking endpoint. The clinic on a booking email is derived server-side from the chosen clinician’s roster entry with clinicOf(), and whatever the request claimed is replaced. A hidden input used to carry it, kept in step by the form’s script — which made the row spoofable by a crafted POST and absent whenever JavaScript was off. Deriving it deleted the input, the script that maintained it, and the difference between the two paths. See Booking Form.
The founder check
Dr. Lindqvist is the practice’s founder, and three pages already assert things about her: the homepage hero portrait, the About story, and the fourth clinician card. doctorsData.test.ts fails if the founder here stops being the founder there.
That check is the model for the rest of the config layer’s tests — it does not verify that the data is correct, which no test can, but that two files that must agree still do.
Schema
A clinician profile emits Physician JSON-LD via getPhysicianSchema in src/js/schema.ts, and deliberately no aggregateRating — review rich results are the one type search engines penalise for unverifiable self-published numbers. The rating still renders on the page as a fact about the practice’s own data; it just does not claim to be a structured review aggregate.
Placeholder warning
The training, licence numbers, papers, awards, review counts and open slots are invented. Reed College, OHSU, the ABFM and the AAFP are real bodies; the claims made about them are not. The photographs recur across the site under different names.
Adding a clinician
Add src/config/doctors/<slug>.ts following the reference file’s shape and import it into doctorsData.json.ts. The directory card, the profile page and the facet options follow, and the slug immediately becomes valid as a blog author, as a member of a specialty team, and as a booking destination.