bible-viewer
A Bible reader that puts two Korean translations side by side
PersonalSole designer, developer, and operator2026.06 — Present
ReactTypeScriptViteTailwindFirebaseFuse.js
Why I built it
I often compare two Korean Bible translations verse by verse, but every reader I tried made switching translations a tab change. Reading both at once meant opening two windows. I wanted a viewer that lines the two texts up against the same verse.
What I did
Everything — data pipeline, search indexing, the column layout, Firestore sync, deployment.
Data pipeline
The two translations come from different sources, so collection had to split too. The older translation
is available from Bolls.life as JSON with verse numbers already attached, but the
newer one isn’t on Bolls at all — I had to scrape holybible.or.kr’s raw EUC-KR HTML and parse verse numbers
myself from <ol start> values. Both share the same versification lineage, so alignment mostly just works,
but the newer translation merges a handful of verses the older one keeps separate — for those I filled the
reference key with an empty string rather than dropping it.
Scraping all 66 books / 1,189 chapters is cached per chapter so an interrupted run can resume, and a separate verification script checks book/chapter/verse counts and cross-translation alignment automatically.
What I learned
Importing a 30MB scripture JSON through resolveJsonModule kills tsc with a heap OOM: the type checker
expands the entire literal into a type. Switching to ?url plus a runtime fetch fixed it.
I also spent a while deciding whether translations belonged in static JSON or Firestore. Scripture is immutable and large, so it stayed static; only per-user state (bookmarks, reading position) went to Firestore. That is where I learned that Firestore’s last-write-wins semantics will resurrect a deleted document from a stale write on another device unless deletions are modeled as tombstones.
The interesting part
I built column drag-and-reorder with raw Pointer Events, no dnd-kit. The layout is a row-based grid, so a
“column” is not one DOM node. I laid a single absolutely-positioned pillar card behind transparent cells
and wrote CSS custom properties (--drag-x) straight onto an ancestor, so a pointermove triggers zero
re-renders. The other columns yield with translateX(±pitch). It ends up feeling like rearranging icons
on an iPhone home screen.
What I’d do differently
To run integration tests without a browser I executed the Firestore logic in Node through Vite’s
ssrLoadModule. Honest as that is, it still does not cover IndexedDB persistence in a real browser.