
Educational Application
An anatomy atlas for medical and health-sciences students. There is no database: the content lives in the repository, is validated against a schema at build time, and every page is statically generated. Search, quizzes and offline use all run in the browser.
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.
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.
Single developer. Content schema and validation pipeline, data model, static generation structure, search, quiz flow, offline layer and tests.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Tell me what you want to build; I'll tell you up front how long it takes and where to start.