the wanting mind is a free 147,000-word interactive book: no ads, no account, in English, Japanese, Chinese and Thai. It is a React 19 SPA with Supabase behind it and Capacitor for the mobile apps. ElevenLabs reads the chapters, fal.ai made the chapter art, Veo made the hero videos, and Claude translated the chapters. This post covers what worked, what I'd do again, and the stack.
What it is
A long-form book about why we're never satisfied: the economics, neuroscience and Buddhist philosophy of taṇhā ("thirst"). At 147,000 words, reading it cover to cover takes about eleven hours. Most people won't do that, so the app gives them other ways in:
- Narrated chapters. ElevenLabs voices, with 12 narrator personas.
-
A 3D knowledge graph of the concepts, people and chapters, built with
react-three-fiber. -
22 simulations you can play with: the Easterlin paradox, Minsky moments,
r > g. - Four languages, switchable while you read.
- An "essence" view: each chapter compressed to one quote over its chapter image, shareable as a card.
Read it: kensaur.us/the-wanting-mind
The stack
| Layer | Choice | Why |
|---|---|---|
| Framework | React 19 + Vite 8 | Fast dev loop; a prerender step after the build for SEO |
| Language | TypeScript 6 | The book's content lives in typed data files, so a typo in a chapter reference fails the build |
| Styling | Tailwind CSS 4 | Design tokens in one file, dark mode with dark:
|
| 3D | reagraph on react-three-fiber | The knowledge graph and the closing constellation scene |
| Mobile | Capacitor 8 | The same React build for iOS and Android |
| Auth + DB | Supabase | Accounts and cloud sync, with row-level security |
| Audio | ElevenLabs | English narration of all 36 chapters, about 20 hours |
| Images | fal.ai (nano-banana-2) | Chapter essence images, batch-generated |
| Video | Veo 3.1 | 35 looping hero videos and a cover video |
| Translation | Claude | Chapter by chapter, into Japanese, Chinese and Thai |
| Hosting | S3 + CloudFront | Static files; no server to keep running |
| Errors | Sentry | Errors on readers' devices reach me without anyone having to report them |
| Push | Firebase Cloud Messaging | Notifications to signed-in readers' devices |
Three things that mattered more than I expected
1. Prerendering chapter HTML
An SPA serves one HTML shell for every route. Google can render JavaScript, but other crawlers and link previews often read only that shell. So scripts/prerender-chapter-html.mjs runs after vite build. It copies dist/index.html once for every chapter and feature page in all four locales, and gives each copy its own <title>, description, canonical URL and OpenGraph tags. The deploy uploads each file to the matching S3 key, so a crawler or link preview asking for /ja/ch-4 gets that chapter's title and description in the first response. The chapter text itself still renders in the browser.
2. Quote cards drawn in the browser
Every chapter has an "essence", one sentence that sums it up. generateQuoteCardBlob in src/components/QuoteCard.tsx draws it as a 1080×1080 card on a Canvas: the chapter image as the backdrop, a translucent overlay, the quote, and the book's name. It runs in the browser with no server. Readers can save the card or share it with navigator.share() on mobile.
For social posts I call the same function from a Playwright script (scripts/generate-quote-cards.mjs), so the cards I post are the cards readers get.
3. Store listings from one file
The store listings exist in four locales on both stores, and editing them by hand is slow and easy to get wrong. Two scripts do it now:
-
scripts/asc-update-listing.mjssigns a JWT with the App Store Connect API key, reads the listing copy fromdocs/store-listings.md, and pushes name, subtitle, promotional text, description and keywords for each locale. -
scripts/play-update-listing.mjsdoes the same through the Google Play Publisher API with a service account.
docs/store-listings.md is the one place the listing copy is edited.
If you build something similar
- Pick one mobile shell (Capacitor or Tauri) and stay with it.
- Prerender before you write growth posts. The posts link to pages, and the pages should show their content to whatever opens them.
- Add error reporting on the first day, even for a personal project.
- Generate marketing images from the app's own code, so they match what readers see.
- Keep the content (chapters, essences, graph nodes) as plain TypeScript files in git. For a single author, that is enough of a CMS.
What's next
The source isn't public yet. If a translation reads wrong, the feedback button in the app reaches me directly.
I wrote this post with help from an AI assistant. The figures come from the project's repository.
Top comments (0)