Services
The services layer is the largest piece of config in Medice and the one most other pages read from. It ships eighteen specialties, of which nine carry a full detail page.
src/config/servicesData.json.ts owns the specialty list for the whole site. That is worth stating plainly, because several other pages draw specialties and none of them keeps its own copy.
Eighteen entries, nine pages
Every specialty is a ServiceProps: a slug, title, description, an icon name, and the list of clinics that run it. Nine of them additionally carry a detail, imported from one file each in src/config/services/ — cardiology, dermatology, endocrinology, gastroenterology, neurology, orthopedics, pulmonology, behavioral health and nutrition & dietetics.
Those nine are exactly the ones the design cross-links: the symptom router routes to eight of them, and the featured card and the related strip name the rest. So every link the page actually draws resolves to a page that exists.
The other nine render as cards on the index and point at routes that do not exist yet — flagged the same way navData’s planned routes are. A specialty card on /services/ may honestly point at a route nothing has built, because a signpost is not a listing. That distinction is drawn deliberately, and it is why the careers board takes the opposite decision and builds all fourteen of its pages.
“Has a page” is exported once
hasPage() and servicePages are exported from servicesData.json.ts and are the single answer to whether a specialty has a detail route. That condition used to be re-derived as detail !== undefined in seven places — the route’s getStaticPaths, its type predicate, two build-time guards, the grid’s data-detail attribute and twice in the check — which is the shape of a missing model rather than a repeated condition.
If you add a detail file for a tenth specialty, getStaticPaths picks it up with no other edit.
No entry stores an href
It is /services/${slug}/ for every one of the eighteen, and serviceHref() derives it once. A field that restates what another field already determines is a field that can disagree with it.
The same reasoning shapes the detail page’s “Available at” row: it is derived from the clinics array rather than stored beside it, because “3 of 4 clinics” typed by hand is a number that can disagree with the list it counts.
The homepage reads this file
The homepage’s specialty band draws six cards with the identical title, description and icon the services index draws — so homeData reads specialties from servicesData rather than restating six entries in a second file. Change a specialty’s description once and both pages follow.
servicesData.test.ts holds the copies honest, including that a clinician the homepage also names keeps the homepage’s face.
The symptom router
symptomRouter maps a complaint in the patient’s words to a specialty slug — eight entries, each { symptom, slug }. It is the band that lets someone who does not know they need a cardiologist find one, and all eight of its destinations are among the nine specialties that have pages, which is not a coincidence.
Adding a route is one entry. Point it at a slug with no detail page and you have shipped a dead link, so add the detail file first.
Filtering the index
The specialty grid uses the shared listing filter — search box, selects, shortcut chips, live count, empty state — over cards already in the DOM. The chips are strings the search box can find rather than booleans, which is why a tenth chip needs no new field and no new predicate: a chip types its own label into the search box.
The machinery is src/components/Sections/Global/_listing.ts, shared with the clinician directory and the health library.
What a detail page contains
Open src/config/services/cardiology.ts for the reference shape. A detail carries the hero and its pills, the prose sections, the conditions treated, the team (clinician slugs, resolved against the roster), the pricing rows, and the related specialties.
Clinician references are slugs, not names, so a specialty team cannot list a clinician the directory does not have.
Placeholder warning
Every clinician, wait time, price, turnaround and headcount in this layer is invented, and the numbers are internally consistent rather than true.
The portraits are worse than placeholder: there are six photographs for twenty-seven slots, so a face recurs under different names across departments. Replace them with real photography before launch. servicesData.test.ts at least pins that a name the homepage also uses keeps the homepage’s face, so the two pages agree while the photographs are still wrong.
Adding a specialty
Add an entry to the specialties array in servicesData.json.ts — slug, title, description, an icon name from the registry, and its clinics. It appears on the index, in the filter’s options and in the homepage band if it lands in the first six.
To give it a page, add src/config/services/<slug>.ts and import it as that entry’s detail. The route builds it, hasPage() returns true, and every guard that reads servicePages follows.