DEV Community

Cover image for A 147,000-word interactive book as a free SPA, and the stack behind it
Kenji Sakuramoto
Kenji Sakuramoto

Posted on Edited on Originally published at kensaur.us

A 147,000-word interactive book as a free SPA, and the stack behind it

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:

  1. Narrated chapters. ElevenLabs voices, with 12 narrator personas.
  2. A 3D knowledge graph of the concepts, people and chapters, built with react-three-fiber.
  3. 22 simulations you can play with: the Easterlin paradox, Minsky moments, r > g.
  4. Four languages, switchable while you read.
  5. 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.mjs signs a JWT with the App Store Connect API key, reads the listing copy from docs/store-listings.md, and pushes name, subtitle, promotional text, description and keywords for each locale.
  • scripts/play-update-listing.mjs does 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

  1. Pick one mobile shell (Capacitor or Tauri) and stay with it.
  2. Prerender before you write growth posts. The posts link to pages, and the pages should show their content to whatever opens them.
  3. Add error reporting on the first day, even for a personal project.
  4. Generate marketing images from the app's own code, so they match what readers see.
  5. 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)