DEV Community

Cover image for I shipped a 147,000-word interactive book as a free SPA — here's the stack
Kenji Sakuramoto
Kenji Sakuramoto

Posted on • Originally published at kensaur.us

I shipped a 147,000-word interactive book as a free SPA — here's the stack

TL;DR — I just shipped The Wanting Mind, a 147,000-word interactive book that reads like Wikipedia + a podcast + a graph. It's free, no signup, all four languages (EN/JA/ZH/TH). The whole thing is a React 19 SPA backed by Supabase, Capacitor for the mobile apps, and a chunk of AI (ElevenLabs for narration, fal.ai for chapter art, Gemini for video, Claude for translation). This post is what worked, what broke, and the stack at the bottom for fellow indie devs.

The pitch (60 seconds)

It's a long-form book about why we're never satisfied — the economics, neuroscience and Buddhist philosophy of taṇhā (literally "thirst"). Reading it cover-to-cover takes ~10 hours. Most people won't do that, so the app does five things to keep you in:

  1. A podcast version of every chapter, AI-narrated by ElevenLabs (the same voices, locale-aware)
  2. An interactive 3D knowledge graph of every concept, person, and chapter — react-three-fiber + force-graph-3d
  3. Playable simulations — Easterlin paradox, Minsky moments, r > g, all the charts you can poke at
  4. Four languages — full translations, switchable mid-sentence
  5. An "essence" view — every chapter compressed to one quote on top of a generated image, shareable as a card

Free forever. The only paid thing is a $9 lifetime "supporter" tier that unlocks a couple of nice-to-haves (offline downloads, custom voices) — basically a tip jar for people who want to thank me.

Read it: kensaur.us/the-wanting-mind
Source: github.com/kensaurus/the-wanting-mind

The stack

Indie-developer-grade, deliberately. Nothing exotic.

Layer Choice Why
Framework React 19 + Vite 8 Fast dev loop, easy SSG-ish prerender for SEO
Language TypeScript 5.9 The book has 200+ data files; types catch every typo
Styling Tailwind CSS 4 Single-file design tokens, dark mode via dark:
3D react-three-fiber + drei Knowledge graph + simulation visuals
Mobile Capacitor 8 Same React build → iOS + Android, one codebase
Auth + DB Supabase RLS-only, no server code I have to babysit
Audio ElevenLabs Multilingual TTS that doesn't sound robotic
Images fal.ai (Flux) Chapter essence images, batch-generated
Video Gemini Veo 3.1 Trailer + 30-sec teaser per chapter
Translation Claude (Anthropic) Chapter-by-chapter, with a human review pass
Hosting S3 + CloudFront $3/month for the entire site at current scale
Errors Sentry I cannot afford to babysit a hundred users by hand
Push Firebase FCM Web push for new chapter releases
Tracing Langfuse Every LLM call has a trace + cost line

Three things that turned out to matter way more than I expected

1. Pre-rendering chapter HTML for SEO

A 25-chapter SPA gives you one indexed page (the shell) by default. Googlebot doesn't run your React. So I added a tiny script — scripts/prerender-chapter-html.mjs — that uses Puppeteer to navigate to every /{locale}/{chapterId} route, wait for the content to render, and dump the resulting HTML to dist/prerender/. The deploy then uploads each prerendered HTML to the matching S3 key, so Googlebot hitting /en/ch-4 gets the chapter content rendered, in English, with the right <title> and <meta description>.

Writing this took an afternoon. The day after I shipped it, indexed-page count went from 1 to ~150 across the four locales. If you have an SPA and you care about SEO at all, do this before you do any keyword research.

2. Sharable quote cards generated in-browser via Canvas

Every chapter has an "essence" — one sentence that compresses the chapter. I render those as 1080x1080 cards using HTML Canvas: the chapter's essence image as the backdrop, a translucent overlay, the quote in a clean serif, the book title at the bottom.

The function lives in src/components/QuoteCard.tsx as generateQuoteCardBlob. It runs entirely in the browser, no server. Users can right-click → save, or hit a "share" button that uses navigator.share() on mobile. For the launch, I drove this exact same function from a Playwright script to generate 10 hero cards for socials — zero divergence between what users see and what I post on Bluesky.

3. Idempotent, programmable App Store + Play Store updates

Updating store listing copy by hand across en-US, ja-JP, zh-Hans, th-TH for both Apple and Google is a soul-crushing afternoon. So I wrote two scripts:

  • scripts/asc-update-listing.mjs — talks to the App Store Connect API, JWT-signs with a .p8 key, parses the canonical listing copy from docs/store-listings.md, and pushes the localized name/subtitle/promo/description/keywords for each locale.
  • scripts/play-update-listing.mjs — same idea, but Google Play Publisher API with a service account JSON.

Now my "marketing" repo has a single source of truth (docs/store-listings.md) and one command per store updates everything. This was the single highest-leverage tool I built post-launch.

What didn't work

  • Tried to inline the entire knowledge graph data into the bundle. Initial JS payload went from 380KB → 2.1MB. Reverted to lazy-loading on graph mount.
  • First pass at audio used streaming generation per request. Burned $40 in API costs in two days from Bingbot. Switched to pre-generated MP3s served from Supabase Storage.
  • Pretended "I'll add Stripe later." Adding a payment link in week 1 cost me 30 minutes; trying to retrofit a paid tier into an existing user base would have cost weeks.

If you're going to build something similar

  1. Pick one mobile shell (Capacitor or Tauri). Don't double up. The feature regression cost of maintaining two shells is brutal.
  2. Pre-render for SEO before you write a single growth post. Otherwise Google doesn't know you exist.
  3. Wire up Sentry on day 1, even on a personal project. You will not notice the bug your one user hit yesterday until they tell their friends about it.
  4. Generate marketing assets from your app code, not separately. The branding stays consistent and you can regenerate at will.
  5. Keep the entire content layer (book chapters, essences, knowledge graph nodes) as plain TypeScript files. No CMS. The file system + git is the best CMS ever made for a single author.

What's next

I'm going to write one essay a week riffing on a chapter — Substack + Medium + cross-posts here. The first one is "Happiness stopped rising in 1972 — here's what happened" (the Easterlin paradox). If that interests you, the book is here and the chapter on it is here.

If you want to see the actual code, the repo is open and PRs are welcome — especially translation fixes from native speakers.

Thanks for reading. If this helped, an emoji reaction is enough. ❤️

Top comments (0)