Careers
The careers board ships fourteen open roles, all of them in src/config/careers/roles.ts, and all fourteen build a page.
One file, not fourteen
src/config/doctors/ and src/config/services/ split one entry per file because each of those carries a hundred and fifty lines of profile or specialty detail. A job posting is about thirty-five lines, and fourteen of them read better as one list than as fourteen imports and a barrel.
The rule to apply if you extend it: split the day a posting grows a section.
All fourteen build a page
This is the largest decision on this page group, and it is the opposite of the one the services index takes.
The design draws eight rows and a “See all fourteen” button pointing at a group careers site that does not exist. This codebase drops a control that cannot move anything, so it had two ways out: build the button’s destination, or publish the six missing roles. It published them.
The reasoning is that a directory card is the thing it names, so a listing that withholds six of its own jobs behind an off-site link is a listing that cannot reach its own contents. The fourteen are all here, the filter narrows all fourteen, and the “see all” foot became a real fallback rather than a dead link.
A specialty card on /services/ may still honestly point at a route nothing has built — because a signpost is not a listing. The distinction is the reason the two page groups differ.
Eight are clinical
The listing heading says so, and careersData.test.ts holds the sentence to the clinical: true flag on the roles — so changing the mix without changing the heading fails the build. The design’s throwaway line about “six more roles in finance, IT and facilities” became those six roles rather than an excuse not to publish them.
The role shape
Each RoleProps carries a slug and title, a department, the clinics it is based at, a contract (Full time, Part time, Nights, 0.6 FTE), the clinical flag, a salary band of { min, max, unit }, hours, a closing date, a one-line summary, a reportsTo clinician slug, and the prose: about, responsibilities, and the rest of the advert.
Two fields are checked rather than trusted. Every clinic named must be one the practice has — the four building names are declared as constants at the top of the file and spelled exactly as homeData.clinics spells them, and the check fails on a name the practice does not have, which is the drift no screenshot could show. Every reportsTo must be a clinician on the roster.
Published salary bands
The bands are real numbers rather than “competitive”, which is a deliberate design choice made concrete. They are also invented — internally consistent rather than Portland market data — and they must be replaced before anyone advertises a real job with them. The closing dates and hours are placeholder in the same way.
The facets
The filter narrows by department, clinic and contract, and the option lists are derived from the roles rather than hand-written:
- Department — Allied health, Cardiology, Imaging, Nursing, Operations, Primary care
- Clinic — Beaverton, Lloyd Center, Pearl District, Sellwood
- Contract — 0.6 FTE, Full time, Nights, Part time
facetOptions is exported from careersData.json.ts, so adding a role in a new department adds the option automatically. relatedRoles, roleHref, roleRoute, roleWhere, roleManager and roleHaystack are exported from the same module — the derived answers, computed once rather than in each consumer.
The rest of the page
careersData.json.ts holds the chrome around the listing: the hero, the benefits band, the gallery, the team band, the hiring-process steps and the newsletter panel. The listing itself — heading, lede, empty state and foot — is a small block there; the roles are the separate file.
Adding a role
Add an entry to the array in src/config/careers/roles.ts. Use one of the four clinic constants at the top of the file rather than typing a building name, and set reportsTo to a slug that exists in src/config/doctors/.
The listing row, the detail page, the facet options and the related-roles strip all follow. If the role is clinical, check that the listing heading in careersData.json.ts still says the right number — the test will tell you if it does not.