Stack: React 18 + Vite + TypeScript. Two apps, no shared packages.
- Vite SPA, served via nginx (SSG first, SPA fallback)
- Pages: Home, Books, Authors, Genres, Reader, Library, Search, About, Privacy, Terms, Contact
- i18n:
/:lang/prefix on all routes. Languages:en,uk. JSON files insrc/locales/.
- Vite SPA on port 81 (
textstack.dev) - English only, no i18n
- Password-based JWT auth, sidebar layout
- Pages: Books, Authors, Genres, Editions, Chapters, Ingestion Jobs, Tools, Stats
React Context only — no Redux/Zustand.
Provider hierarchy in App.tsx:
SiteProvider → AuthProvider → DownloadProvider → LanguageProvider → {children}
- SiteProvider: fetches
/api/site/context, providessiteto all children - AuthProvider: Google Sign-In, auto-refresh token, skips Google for bots
- DownloadProvider: IndexedDB offline downloads, progress tracking
- LanguageProvider: extracts
langfrom URL, providesswitchLanguage(),getLocalizedPath()
Language-prefixed routes: /:lang/books, /:lang/authors/:slug, etc.
Root / redirects to /en. Legacy /authors/* → 301 to /en/authors/*.
useApi() hook → createApi(language) → typed methods (getBooks(), getBook(slug), etc.).
Uses fetchJsonWithRetry() with retry on 5xx/429.
ReaderPage.tsx — Kindle-like reader with:
- Settings: font size, line height, width, theme (light/sepia/dark), font family, alignment
- Navigation: TOC drawer, chapter prev/next, keyboard shortcuts, mobile swipe
- Offline: cache-first from IndexedDB
- Text selection: highlights, translate (LibreTranslate), dictionary
- Progress: autosave to server (authenticated) or localStorage (anonymous)
- SSG: Puppeteer prerenders book/author/genre pages to static HTML
- nginx serves SSG → SPA fallback (check
X-SEO-Renderheader) SeoHeadcomponent: title, description, canonical, robots, OG tags, JSON-LD
apps/web/src/
├── pages/ # Route components
├── components/ # Shared UI
├── contexts/ # React Context providers
├── hooks/ # useApi, useTranslation, etc.
├── locales/ # en.json, uk.json
├── styles/ # Global CSS
└── utils/ # Helpers