Skip to main content
Back to Projects
MedAtlas logo

MedAtlas

Educational Application

MedAtlas architecture diagram MedAtlas Veritabanı yok — içerik derleme anında doğrulanır, sayfalar statik üretilir Çalışma anında sunucu sorgusu yok; tüm okuma yolu build çıktısıdır. KAYNAK Yapısal veri src/data/*.json 58 kas · 25 kemik · 22 sinir 14 eklem · 6 bölge origo, insertio, innervasyon Anlatı içeriği src/content/**/*.mdx 96 dosya · klinik notlar ilişkiler ID ile değil slug ile DERLEME KAPISI Zod şema doğrulaması content:check — tek geçersiz kayıt exit 1 verir, dağıtım durur TypeScript tipleri şemadan türetilir ÇIKTI generateStaticParams 18 rota şablonu · ~330 statik sayfa /kaslar · /kemikler · /eklemler · /sinirler /bolgeler · /biyomekanik · /quiz · /pano Tarayıcıda çalışan kısım Fuse.js arama — ⌘K index istemcide kurulur, arama isteği sunucuya çıkmaz masaüstü modal ve mobil overlay aynı gövde Zustand — yalnız kişisel durum yer imi, ilerleme, quiz durumu içerik asla client store'a girmez içerik sunucudan gelir, kopyalanmaz Serwist — çevrimdışı service worker önbelleği bağlantı yoksa /offline sayfası telefona kurulabilir 11 birim testi + Playwright uçtan uca akışları; içerik doğrulaması testten önce, derleme kapısında çalışır. Yazma yolu yok: içerik yalnızca repoda değişir, panelden değil.
No database. Structured data lives in JSON files (58 muscles, 25 bones, 22 nerves, 14 joints, 6 regions) and narrative content in 96 MDX files; both are validated with Zod at the build gate, and a single invalid record stops the deployment. The output is roughly 330 static pages from 18 route templates. In the browser there is Fuse.js search, Zustand for personal state only, and a Serwist offline cache; there is no write path.

An anatomy atlas for medical and health-sciences students. It loads fast on a phone and works offline, with search and quizzes right in the browser. Every piece of content is checked automatically before it goes live.

Technologies Used

  • Next.js 16
  • TypeScript
  • Zod
  • MDX
  • Fuse.js
  • Zustand
  • Serwist
  • Playwright

Key Features

  • Fully static generation — no database query at runtime
  • Content is validated with Zod at the build gate; one invalid record stops the deployment
  • Search opens with ⌘K and runs in the browser; the query never leaves the device
  • Relations are built on slugs rather than identifiers
  • Personal state (bookmarks, progress, quizzes) stays on the client; content never enters the client store
  • Offline use and installability through a service worker

Development Time

2 months

Problem

Anatomy content is made of two different kinds of material: on one side purely structural data — a muscle's origin, insertion, nerve, action — and on the other free narrative, clinical notes, differential diagnosis. Keeping the first in a prose file makes the data unsearchable and inconsistent; squeezing the second into a database table makes it impossible to write. The real issue, though, is correctness: in educational content, a field left empty or a reference pointing at a structure that does not exist is a silent error for the student working from it. The page opens, the table renders, only one row is missing. That kind of error had to be caught before it reached production.

Users

One type of user but two distinct moments of use. The studying student: reads in long sessions, constantly jumps from one structure to another — muscle to nerve, nerve to region — and wants to open the search box from the keyboard. The revising student: before an exam, between classes, on a phone, often on a poor connection. That second mode makes offline operation and fast startup mandatory.

My Role

Single developer. Content schema and validation pipeline, data model, static generation structure, search, quiz flow, offline layer and tests.

Architecture

One decision sits at the centre of the system: there is no database. Structural data lives in the repository as JSON, narrative content as MDX; on a detail page the two are joined through a loader. Relations are built on slugs rather than identifiers — a muscle's nerve is not "nerve-42" but that nerve's readable name, which keeps both the addresses and the content files editable by a human. All detail pages are produced at build time through `generateStaticParams`; there is no read path to a server at runtime. The default rendering mode is Server Component; `"use client"` appears only where there is real interaction. Three things run in the browser: fuzzy search with Fuse.js — the index is built on the client, and the desktop ⌘K modal and the mobile full-screen overlay share the same body — personal state held in Zustand, and the service worker cache. There is no content in Zustand, only what belongs to the user: bookmarks, progress, quiz state.

Key Decisions

  1. No database — content in the repository, validation at build time

    This content does not change a hundred times a day; it changes a few times a term, and only after review. For content like that, a database brings runtime queries, connection management and a separate admin interface — and the freshness it buys is not needed. Instead the content is under version control: every change is a reviewable diff and can be reverted.

    Trade-offChanging content requires repository access and a new deployment; a domain expert cannot make a correction on their own. Even a typo waits for a build.

  2. Invalid content stops the deployment

    The validation script parses every data file against Zod schemas and exits non-zero on a single invalid record. TypeScript types are derived from the same schemas, so the schema is the single source: the type and the runtime validation cannot drift apart. In educational content this gate is the only mechanism that catches "the page opens but the information is missing" before it ships.

    Trade-offBecause the schema is strict, an unfinished piece of content cannot enter the repository; anyone wanting to work on a draft has to either add permission for an empty field to the schema or keep the content outside the repository. And loosening the schema directly lowers the value of the gate.

  3. Search runs in the browser, not on the server

    The searched dataset is small and fixed; sending an index that is already available at build time to the client is both faster and simpler than a round trip on every keystroke. Fuzzy matching was needed so that a Latin term and its Turkish equivalent can be found by the same query. The desktop modal and the mobile overlay share the same body and the same index.

    Trade-offThe index is downloaded to the client; as the content grows so does that cost, and at some point moving to server-side search will be necessary. No data is collected about which terms are searched either — good for privacy, a blind spot for prioritising content.

  4. Personal state stays on the client; content never enters it

    Bookmarks, progress and quiz state belong to the user and there is no reason to store them on a server — there is no account and no sync. Copying content into the client store, on the other hand, is forbidden: content is the single source coming from the server, and keeping a second copy makes the two drift over time.

    Trade-offWhen a user changes device their progress does not travel; clearing browser data loses it as well. That is the cost of working without an account, and it was accepted.

Stack Rationale

Next.js App Router is used here not as a server framework but as a static site generator: hundreds of detail pages come out at build time through `generateStaticParams`, and because Server Component is the default, the JavaScript sent to the client stays minimal. Zod does two jobs — validation gate and type source; having both come from one place guarantees that changing the schema changes the type too. MDX lets clinical narrative be written as prose while leaving component embedding open. Fuse.js is small enough to run on the client. Serwist sets up the offline layer without writing a service worker by hand. On the visual side, interactive SVG gives zoom and pan without falling into the blur of a raster image.

Security

The application has no write path: there is no form, session, account or user database. As a result there is no personal data stored either — progress and bookmarks never leave the user's device. The model calls used while producing content live only in scripts; no request goes to an external service at runtime. That reduces the attack surface to the correctness of the content — and that is guarded by the build gate.

Performance

There is no stored, date-stamped Lighthouse output for this project, so no page-speed score is written here. Structurally the measure is this: there is no server query on the read path, pages are served as build output. Because the search index and quiz state are on the client, interaction latency does not depend on a network round trip. The service worker keeps recently seen pages in cache; when the connection drops, the application shows its own offline page rather than a blank screen.

Results

Active development
2026-04 → 2026-06 · 12 commits
TypeScript source files
139
Test files
11 — unit tests and Playwright end-to-end flows
Content volume
96 MDX files · 58 muscles · 25 bones · 22 nerves · 14 joints · 6 regions
Routes
18 page templates — atlas modules, biomechanics, quiz, board and the offline page
Architectural guarantee
A single content record that does not match the schema stops the deployment
Architectural guarantee
Content is never copied into the client store; the single source is the build output

What I Would Do Differently

I put the validation gate in from the start, and it was the decision that paid off most in this project; I repeated the same pattern later in Rahmina as a content approval gate — there the rule was not "does it match the schema" but "has it been reviewed". That is exactly what is missing here: the schema verifies that a field is filled, not that the content is correct. Today I would add a review status to every content block and turn the release gate into "the build fails if unapproved content exists". The second point is search: shipping the index to the client is the right call at today's volume, but nothing measures when that call will stop being right. Today I would bound the index size with a threshold and make the build warn when it is exceeded — when a decision will expire belongs in the code just as much as the decision itself.

Prototype

Let's find where to start

Tell me what you want to build; I'll tell you up front how long it takes and what the sensible first step is.