claude.md
Internal reference, rendered read-only. Edit the real file in the repo — this page just displays it. Not indexed; it should sit behind auth once Clerk lands.
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project state
Live Astro site on Cloudflare Pages, auto-deploying on push to main. Phase 2 in progress: the CAP disease page renders fully data-driven from a typed content collection, and drug pages derive from that same collection (the drug-page work is the current iteration). Legacy Webflow/ holds the behavioral-reference files — CAP.html, otitis media - peds.js, sexual assault pep.js (three deliberately different disease shapes), plus the abx-*.js engine, abx-po.js calculator, abx.css, allergy-map.js, navbar.js. Commands: npm run dev (preview server), npm run build (production build — also validates every content-collection schema, so a bad record fails the build, not the user’s screen), npm run preview.
Full spec lives in PROJECT-CONTEXT.md — read it before any substantive work. It is the durable source of decisions; this file summarizes the parts that shape day-to-day coding choices.
What this product is
The mission is SPEED OF DECISION MAKING. Exploding-bacteria.com is a clinical decision-support platform built to make a clinician’s next decision faster. That is the through-line for every product on it — not “an antibiotic site that also has calculators.”
The platform is one site with three layers, and knowing which layer you are working in decides almost everything (gating, scope, voice, breadth):
| What it is | Status | Gated? | |
|---|---|---|---|
| The wedge | Free tools — the whole ED calculator bundle, EM-wide (chest pain, syncope, trauma, tox). Public, static, indexable. | Building now | No — free forever, this is the funnel |
| Product #1 — ABX | The paid antibiotic guide: diseases + drugs. A faster, more opinionated alternative to Sanford Guide — takes positions where guidelines hedge, eliminates false-precision numbers, cites primary literature over organizations. | Building now | Yes — $269/yr |
| Product #2 — ACT (Acute Care Tools) | The second paid product: acute-care content — seizures, chest pain, stroke, etc. Same disease-page machinery, different specialty. | Not yet — do not build ACT disease pages | Yes, later |
The tool bundle is NOT scope creep — it is the wedge, and it is deliberately EM-wide. A tool being non-antibiotic (Ottawa Ankle, NIHSS, a burn panel) is correct and expected: free tools bring the ED doc in the door, and the gated ID content is what they pay for. Do not “correct” a non-ID tool back toward antibiotics.
But ACT the PRODUCT is not started. The line to hold: an ACT tool (a chest-pain score) is in scope today as part of the free wedge; an ACT disease page (a seizure page with management content) is Product #2 and is not being built yet. When the free tools outgrow having no disease page to live on, that is a discovery problem to solve in navigation — not a licence to start Product #2 early.
Solo builder (an ED physician, not a programmer) reviews/verifies everything; Claude authors all code.
The long game — we are becoming the organization. The endgame is for Exploding Bacteria to be the trusted source: a team of ID docs setting positions that update in near-real-time, replacing the 50-page consensus PDFs that drop once a decade (one of the most inefficient things in medicine — a platform can update continuously; a decadal document cannot). This is why we never defer to IDSA/ATS/etc. as the authority in our own product — see the “never cite a professional organization as the authority” guardrail. It shapes content voice today, long before the team exists.
Marketing / positioning
The story — the angles that make this defensible rather than merely functional. Not code; the narrative we sell from and write from. Add to it as angles surface.
- “Positions we hold,” not “calculators.” The real-time-updates-vs-decadal-PDF thesis is the most interesting thing about this product. It reframes the tools from calculators into positions we hold — a much more defensible place to compete from than being a faster Sanford. Anyone can ship a calculator; a calculator is a commodity, and MDCalc already owns that shelf. A continuously-updated position — owned by named ID docs who will defend it and revise it the week the evidence moves — is not a commodity, and it is precisely the thing a 50-page decadal consensus PDF structurally cannot be. This is the through-line for the tools, the writing, and eventually the org itself (see “The long game” above). It’s also why the de-org rule exists: you cannot claim the position while citing someone else’s guideline as your authority.
The stack (locked)
- Astro (static-first, islands for interactivity), hosted on Cloudflare Pages
- git + GitHub for version control
- Auth: Clerk (locked). Billing provider: open, decided at Phase 6 — Clerk Billing (Stripe, we own global tax) vs. a Merchant-of-Record (Paddle/Lemon Squeezy, they own global tax). $269/yr + 3–7 day trial. Leaning MoR because we sell internationally and the solo builder won’t run a tax back-office. Auth is geography-independent; only billing/tax is the international question. Full rationale in PROJECT-CONTEXT.md → Business model & payments.
- Selling internationally: yes, with a “reflects US resistance patterns/availability/guidelines” scope banner — not geo-blocked (US-centric ≈ 97% useful abroad; localizing is a separate future project). Gated pages must be SSR’d at the edge (a static site leaks HTML if you gate client-side); free diseases + every tool stay static/public/indexable (the wedge — see the three layers above; tools are free forever and are the funnel, so never gate one).
- Pagefind for search, plus a drug alias/synonym map and failed-query logging
- React for interactive islands
- Writing/blog = MDX content collection (
src/content/writing), prose + embedded theme-aware diagram components; flagship posts are Claude-authored (“paste prose → build MDX + diagrams”); tool?buttons deep-link to the relevant post. Backup plan (deferred, not now): if self-serve small edits become annoying, add a git-based visual CMS — Keystatic (writes Markdown back to the repo, no DB, no lock-in). Do NOT reach for hosted DB-backed CMSs (Sanity/Contentful/Notion) — they break the static/git/no-DB model. - Algo-tool decision trees = our own generated SVG (one
AlgoToolisland renders any algo record; layout derived from the elimination data — linear→spine, branching→merged top-down). NOT Mermaid (too heavy, hard to theme) or D3. Backup plan (deferred, not now): if an algo’s graph ever gets too gnarly for the hand-rolled layout (many crossing branches, deep multi-parent merges), drop indagre(the layout engine Mermaid uses internally) for node positioning only and keep our own SVG rendering + theming + interactivity. A small, targeted dep — adopt only if complexity demands it. - No database. All clinical content is static, known at build time. A database (managed Postgres — Supabase or Neon, not Xano/Airtable) is only introduced later, and only for per-user runtime data (saved lists, annotations, history).
- The engine is rewritten from scratch, mined from the legacy vanilla-JS files as a behavioral spec — not ported line-for-line.
Architecture
Page composition: the page template/layout is a static component, not an island — it renders to plain HTML. Islands are the discrete interactive widgets embedded in it (PO calc, IV calc, acid-base tool, MRSA/Pseudo toggle, Adult/Peds filter). Do not nest islands — one appropriately-sized island with ordinary React children (e.g. the acid-base tool, “10 MDCalcs in one,” is a single island with sub-calculators as children). Islands are isolated by default; the Adult/Peds filter is the one exception that needs a shared store since it affects the rest of the page.
Drug pages are build-time-derived views, not runtime queries. A build function scans all content-collection entries for a drug and assembles its page (disease context, pairing, tier) from the same source records disease pages use. The scan happens at build time only.
THE THREE PAGE TYPES ARE THREE USER INTENTS — this decides what belongs where (builder, 2026-07-15):
| Page | The user is thinking | |
|---|---|---|
| Disease | ”I don’t know what to give.” | The home of drugs and doses — the decision lives here |
| Drug | ”I know what to give, but not the dose.” | Redirects up to Disease |
| Dosing | ”I know the drug AND the dose — I just don’t want to do the math.” | Redirects up to Drug and Disease |
Navigation points UP toward the disease, because that is where the decision is. Every page carries a full-width backlink to the page that owns it — the drug page up to its diseases, the dosing page up to its drug (top) and its diseases (“Used in”). Use the intent to decide placement: a dosing shortcut does NOT belong on a drug page, because that user hasn’t chosen a dose yet — they’re there to choose one. Adding it answers a question they haven’t asked.
DOSING pages are derived from the DISEASE, never from the config registry — and the direction is easy to get backwards. src/data/{po,iv}Dosing.json is a warehouse of ~127 configs parsed from the legacy CSV; most are waiting for a disease that hasn’t been ported. A config does not earn a page by existing — it earns one when a disease binds it (doseCard.poKey / ivKey). Build the disease and its dosing pages fall out (src/lib/dosing.ts → /dosing/[key]), along with the drug page’s dosing list and the search entries. They serve the user who wants none of the navigation — “I just want to calculate a cefdinir dose.” Gated (ABX, Product #1 — the dose is the product), so they are noindex and carry no SEO argument.
- Titles and search terms are derived too, including COLLISION handling. Two configs can produce the same title (levofloxacin 20 mg/kg/day exists at q12h and q24h; “Amoxicillin 90 mg/kg/day” will collide the day two diseases pull its two configs, whose maxes differ 54% at 30 kg). The builder detects the clash and appends the distinguishing frequency — never hand-name a dosing page. Search terms deliberately carry every max and frequency (“azithromycin single dose max 1500 mg”), because that is what separates two same-dose regimens in a long list.
- ASK THE BUILDER when a NEW DISEASE brings new calc bindings (his standing instruction, 2026-07-15 — this is the one dosing checkpoint): list the
poKey/ivKeys the disease introduces and the dosing pages they would create, and let him confirm the set and the names before they ship. It is a clinical/editorial call (is azithromycin 3-day/4-day/5-day/single four pages or one? — he ruled four), and it is the moment a naming collision gets caught.
Tool ↔ disease cross-links are DERIVED, never hand-maintained (same pattern). Only ONE direction is authored: a disease references its tools (disease.tools[].tool). A tool page inverts that at build time — the [slug] page scans all diseases to render (a) a full-width backlink to each disease that uses this tool, below the subtitle, and (b) a “Related tools” section of its siblings (the other tools those same diseases reference). Consequence: adding a tool to a disease auto-appears in that disease’s other tools’ Related lists — there is nothing per-tool to remember, and no related/diseases field on the tool record. When adding a new tool, just wire it into the relevant disease’s tools[]; the cross-links fall out for free. (Same rule as drug pages: one source of truth, invert by scanning.)
Frozen DOM contract (engine spec — do not deviate)
Canonical nesting: regimen > combo > group > drug > drug-card.
Structure is real nesting, never number fields. The regimen→combo→group→drug hierarchy exists as nested records in the typed schema, and the rendering component enforces exclusivity from that structure — a flat list with a “group number” field cannot substitute. (Legacy read this ancestry via DOM .closest; that mechanism is a legacy detail — keep the behavior, re-express it cleanly in the component, don’t port the DOM-soup.)
- One-open-per-group is automatic: opening a drug closes other open drugs in the same group.
- Combos close each other by default (unless the shell is marked multi).
- Exclusive combo: opening a drug in one group closes open drugs in other groups within that combo.
- Combination therapy (“one from Group A AND one from Group B”) = a
combowrapping twogroups — zero new code, this falls out of the nesting rules for free. - Primary + Alt regimen = two
combos inside oneregimen— cross-combo close already gives “select Alt → Primary closes” for free. - Same drug, different doses (e.g. azithro 5-day vs 3-day) = separate
drugaccordions. - Header is its own record with its own sort order — do not reproduce the legacy “header paired to first drug” pattern.
- PO calc: the engine builds the entire calculator from one
data-po-keyreference — never hand-build a calc. IV calc renders on weight entry, no separate click. Calc slots bind by key (data-po-key/data-iv-key) to global dosing configs. - Drug formulations/identity are looked up from the global drug DB by slug (
data-drug).
Both Phase-0 engine decisions are now resolved (2026-07-07; full detail in PROJECT-CONTEXT.md):
- Primary/Alt = auto-close-on-open — two
combos in one non-multiregimen, default cross-combo close; no new code. - MRSA/Pseudomonas —
data-riskis an informational risk-factor disclosure, NOT a coverage gate. It renders a labeled accordion (“Pseudomonas Risk Factors” / “MRSA Risk Factors”) that expands to show the risk-factor list fromABX_RISK_DATA; it never hides or reveals drugs. Coverage is driven by regimen structure instead: Pseudomonas is a disease-level flag (pseudomonas: true/false) surfaced as a scenario in theclassifyradio (the WITH-Pseudomonas scenario’s regimen carries antipseudomonal agents); MRSA is also a disease-level relevance flag (mrsa: true/false) — the MRSA group is authored only for diseases where MRSA is plausible (CAP yes; sinusitis no). When present it is an always-visible optional group at the regimen foot (“Consider ONE Anti-MRSA”), used or ignored (or a required/primary group when MRSA is primary, e.g. purulent SSTI). Coexistence is automatic — the pseudomonas regimen contains the MRSA group.data-riskaccordions are independent free disclosures outside the combo/group exclusivity tree. (The allergy filter pill-chips are a separate Phase 3 island.)
Data model
- Drug identity (formulations, class, safety, brand names): one global record per drug, referenced by slug, never duplicated per page.
- Dose-in-context (dose, route, frequency for a specific infection): many records, one per disease context — expected to number in the thousands, and that’s correct, since dosing is a per-infection clinical decision, not a shared global.
- Each regimen entry carries its own disease reference plus structural fields; disease pages and drug pages are both build-time-derived views filtered from the same collection.
- Naming: a display name (human-facing, can change freely) and a stable KEY (engine binding, encodes route/dose/freq, e.g.
cap-amox-po-1000-q8h). Always bind pages to the KEY, never the display name. - Structured fields (
drug,dose,route,frequency,tier,paired_with,population,duration) carry data the build needs to compare or gather. Editorial reasoning goes in a prosenotesfield that is never parsed, only displayed.tierandpaired_withmust be present from day one (retrofitting later across ~121 pages is expensive) — but treat them as clinical editorial judgments requiring the builder’s sign-off, same asdose, not as mechanical fields to auto-fill. - The disease schema must express every disease SHAPE, not just CAP’s — modelling it around one disease forces per-disease hacks (the mess we’re avoiding). Mining the three legacy exemplars, the schema must model up front: regimen TYPE (single decision-tree vs
multibundle/protocol you give all of — e.g. sexual-assault PEP: nausea + GC + trich + contraception + HepB + HBIG + HPV + HIV-PEP together); a combo tier ladder (preferred → alternative → failed → last-line, not just primary/alt); drug entries that vary — a single drug, a fixed combination (one row = two drugs,data-drug="ceftriaxone doxycycline"→ surfaces on both drug pages), or a non-linked treatment (vaccine, IUD, device — no drug-DB record, no drug page); per-option conditions (“no recent amox AND no prior failure”); interleaved info-blocks; age/condition-structured durations; weight-based calc bindings; and a rich-text story for doses/notes (bold, “OR” branches, links, mcg/mg) — Markdown or inline HTML, whichever renders it cleanest; the goal is expressiveness, not format purity. Design against all shapes, then build one template that renders whatever the record declares.
Engineering philosophy (Astro-native, lean — supersedes “port as-is”)
Legacy code is a behavioral spec, not source to import. Mine the old vanilla-JS/Webflow files for what the clinician sees and how the logic works, then re-express it in the cleanest form this stack allows. Do not carry over DOM-soup, jQuery-isms, Webflow scaffolding, or “just in case” config. Old thinking dies; we build in the modern idiom.
- One source of truth, always. Anything appearing in more than one place — design tokens, clinical prose, shared UI, behavior — lives in exactly one file/record. Duplication is a bug to fix, not a convenience.
- Data-driven, not hand-built. Structure lives in the typed content schema; components render whatever the data declares. One
[disease]/[drug]template each — never per-page copies. - Delete what isn’t needed — CODE only, NEVER clinical content. Dead code, unused legacy fluff, and Webflow scaffolding go. But medical content — doses, warnings, notes, drug facts, citations — is the product and is never removed as “cleanup.” When unsure whether something is cruft or content, keep it and ask. Lean-and-legible applies to the substrate, never the medicine.
- Interactivity: Astro components for everything static; React islands only where UI genuinely needs state (calculators, shared filters). New tools: React. Ported tools: re-express cleanly — do not wrap old DOM code.
- Visual hierarchy is carried by size + weight, largest-at-top to smallest-at-bottom. In any nested block, a parent/label must NEVER be smaller (or lighter) than the child content beneath it — a heading below its own body text is a bug (e.g. a drug-component name must not be smaller than its dose line). And never use muted / low-opacity text to signal rank: full contrast always; size and weight do the work (low-opacity body text is unreadable in dark mode).
- Correctness is still sacred. Lean ≠ loose. The clinical MATH and values are preserved exactly and remain the builder’s red-pen; modernizing the engine never changes the medicine. Prefer designs where a wrong value fails the build (loud) over ones where it renders silently (found by a user).
- All-or-nothing, never silent — a broken tool that does nothing beats one that shows a wrong dose. This is a hard safety rule, not a preference: a clinical tool must work 100% or not render at all. Missing, misconfigured, or half-wired data must fail the build (loud) — never silently omit a tool, never render a partial/degraded calculation a clinician could mistake for a real dose. Wrong output is the worst possible outcome; a build that breaks (or a tool that visibly refuses to render) is always preferable. Enforce with build-time guards (like the PO calc’s poKey→config→drug-data guard) so incomplete work can’t ship. When in doubt, break loudly.
Migration phases
Phase 0 (freeze spec) → Phase 1 (toolchain + hello-world deploy) → Phase 2 (one page at full parity — the real test, do not proceed past this until it’s genuinely done) → Phase 3 (port tools as islands) → Phase 4 (bulk content port, ~67 remaining pages with a mandatory visual confirm per page) → Phase 5 (search: Pagefind, alias map, query logging) → Phase 6 (Clerk auth + billing provider [Clerk Billing vs. MoR], SSR the gated routes, test payment flows hard) → Phase 7 (cutover: DNS, redirect all legacy Webflow paths, parity sweep, then kill Webflow).
Parity line (stopping condition): migration is done when all 68 existing pages render on Astro with working tools AND a brand-new disease page publishes end-to-end. At that point, infrastructure work stops and clinical content resumes — not “when it’s clean” or “when the engine is elegant.”
Guardrails
- TOOLS: author the subtitle + search terms yourself by default — don’t ask, don’t block. If a tool arrives without a subtitle (
blurb) or search terms (synonyms), write them. Category isED Decision Toolsunless it clearly isn’t. Surface what you wrote in the summary so it can be red-penned. (Standing instruction from the builder, 2026-07-15. This is TOOLS only — the disease-page ASK below still stands.) - Before building/porting any NEW disease page, ASK the builder (don’t guess): (1) the search category, (2) the synonym list, and (3) whether it gets the allergy filter (
allergyFilter: true/false). Not every page needs the filter — some are genuinely simple, and some bake allergy into the decision from the start (e.g. otitis media is a ladder built on the patient’s allergy status), so the standalone filter would be redundant. It is opt-in per disease; today only CAP has it. - The clinical verification pass (the builder’s red-pen review) is the bottleneck this migration does not speed up. Don’t imply otherwise.
- Flag medical-content errors the moment you notice them — never quietly “fix,” guess, or paper over clinical content. Surface it to the builder explicitly. If it isn’t being corrected right then, log it on the /roadmap (
src/data/roadmap.ts) so it can’t be lost. A flagged, tracked possible-error always beats a silently-carried one. (Applies to anything clinical you author with lower confidence too — e.g. a guessed MDT or formulation.) - Watch for substrate-polishing (perfecting the setup instead of shipping) — push back if it surfaces, especially during the blank-slate Astro build.
- NEVER cite or defer to a professional organization as the authority — we are becoming the authority. Do not name IDSA, ATS, GOLD, BTS, ACEP, or any medical society as the source of a recommendation in user-facing content — not in a title, subtitle, footer, teaching block, or even paraphrased in our own voice. CDC is the exception (including its committees, e.g. ACIP): a public-health authority and public domain — referencing it is fine. Everything else must be backed by primary literature (author · journal · year — “Lim WS, et al. Thorax. 2003”) or stated as our own position. Citing the study an org’s guideline was built on is correct; citing the guideline is not.
- Per-tool check, every time (alongside the copyright + FDA checks): does this tool derive its authority from an organization like IDSA? If so, FLAG it — don’t quietly ship it. Resolution is either (a) find the primary studies that actually back it and cite those, or (b) drop the tool. Some tools will not survive this, and that’s acceptable.
- Why: deferring to IDSA inside our own product cements them as the authority and us as the summarizer. The whole thesis is the opposite (see “The long game” above).
- The OPERATIONAL-MANDATE exception (Surviving Sepsis Campaign is the archetype). Some org output isn’t advice you may take or leave — it is wired into hospital-wide protocol, order sets, core measures, and billing (SEP-1). The clinician has to comply whether or not the evidence supports it, so a tool that pretends the mandate doesn’t exist is useless at the bedside. For these: name the org, but as the MANDATE, not the authority — “this is what your hospital’s protocol/billing requires,” never “this is what’s true because they said so.” Where our position differs, show BOTH, say plainly that we differ, and say why. Never silently diverge from a rule the user gets audited against — a clinician blindsided at an M&M or a denied claim is a clinician we failed.
- The test is not “is it a professional society?” — it is “does this bind the user operationally (billing / core measure / hospital-wide protocol) regardless of the evidence?” IDSA does not. Surviving Sepsis does. Being famous, or being in every residency lecture, is not the same as being mandated.
- Do not self-grant this exception. If it’s genuinely arguable (e.g. AHA/ASA via stroke-centre certification, SCCM/PADIS via ICU sedation protocol), FLAG it for the builder and default to writing in our own voice until he rules.
- Draft phase: flag + log to /roadmap; resolved at red-pen, not necessarily on the spot.
- Verify PMIDs before publishing citations. Public-domain US-government sources (CDC, NIH, FDA, other federal works) may be reproduced verbatim — and for high-legal-risk tools (e.g. PEP eligibility) preserving the source’s exact wording is the safer choice than paraphrasing; improve the delivery, not the message. Do NOT reproduce copyrighted guideline text from private organizations (IDSA, Sanford, ACEP, medical societies, journals) verbatim — mine it, cite primary literature over organizations, and write in our own voice. (Not legal advice; the builder owns final verification.)
- Legal + regulatory gut-check on ANYTHING new we build or reproduce — do it every time, surface the risk to the builder, never quietly ship past it. Two questions:
- Copyright / risk of legal action. Does this reproduce someone’s protected expression (a proprietary scale, guideline prose, a figure, branding)? Public-domain US-gov (CDC/NIH/FDA) is safe verbatim; private-org / journal prose is not — mine it, don’t copy. Standardized clinical instruments (scoring scales like CIWA-Ar, CURB-65, Wells) are the special case: verbatim reproduction is clinically required (paraphrasing invalidates the instrument) and usually defensible (the scoring system/facts aren’t copyrightable, plus the merger doctrine) — but a few scales are actively licensed/enforced (the classic example is the MMSE → PAR), so check per-instrument before reproducing. Always attribute the source; never imply we authored it or own it.
- FDA “medical device” (Clinical Decision Support) risk. A tool that informs a clinician who can independently review its basis (calculators, references, our decision content) is generally NOT a regulated device. A tool that directs a specific diagnosis/treatment, or that a clinician can’t see the reasoning behind and independently verify, can cross into FDA-regulated CDS. Keep tools in the “informs, shows its work, doesn’t dictate” lane; flag anything that drifts toward auto-directing care or hiding its basis. Not legal/regulatory advice — the builder owns final verification (and a real attorney for anything genuinely uncertain).
- External links open in a NEW TAB (
target="_blank" rel="noopener noreferrer") — research papers, other sites, anything off exploding-bacteria.com. We never navigate the user away from the site. In Markdown/MDX this is automatic (rehype-external-links in astro.config); for hand-written anchors and React components, add it explicitly. Internal links stay same-tab. - Permission mode: Accept Edits (auto-accept file edits so long tasks don’t stall while the builder steps away; riskier actions like shell commands still confirm). Keep secrets out of the repo (env vars, gitignored).
- One source of truth for anything that appears identically across contexts (drug identity, dosing configs, shared UI) — never duplicate.