feat: add ssr

This commit is contained in:
gosangam
2026-04-17 22:51:58 +05:30
parent d88ababd76
commit 0aab1d23ec
97 changed files with 29891 additions and 68 deletions
+428
View File
@@ -0,0 +1,428 @@
# Prabhupāda Says — Project Context
## What this project is
A Next.js app that converts legacy RTF files containing Vedic scriptures (Bhagavad-gītā, Śrīmad-Bhāgavatam, Caitanya-caritāmṛta, Kṛṣṇa Book, letters, magazines, etc.) into structured, browsable content with proper Unicode Sanskrit.
The original RTF files use **custom font encodings** (Indevr, ScaGoudy, Inbenr/Inbeni, RM Devanagari, Balaram, etc.) where ASCII characters map to Devanagari/Bengali glyphs and IAST diacritical marks.
---
## Tech Stack
- **Next.js 16** with Turbopack, React 19, TypeScript
- **Tailwind CSS v4** with `@tailwindcss/postcss`
- **shadcn/ui** (base-nova style, neutral base color, lucide icons) — components: Button, Card, Badge, Tooltip, Input
- **next-themes** for dark/light mode
- **Prisma v6** with MongoDB (provider `prisma-client-js`)
- **MongoDB** native driver (`mongodb` package) for seeding (Prisma requires replica set for writes)
- **Python 3** with BeautifulSoup4 + `indic-transliteration` for HTML extraction scripts
- **Database**: MongoDB at `DATABASE_URL` env variable with `?authSource=admin`
---
## Project Structure
```
spsays-training/
├── app/
│ ├── page.tsx → "Prabhupāda Says" landing page (Books, Glossary, RTFs)
│ ├── layout.tsx → ThemeProvider + TooltipProvider wrapper
│ ├── globals.css → shadcn/Tailwind v4 CSS with dark mode
│ ├── books/
│ │ ├── page.tsx → Books index (BG, SB, CC, SB1962, KB)
│ │ ├── chapter-viewer.tsx → Shared client component for verse-based books
│ │ ├── extracted-verse-count.tsx → (i) tooltip for verse counts
│ │ ├── bg/ → Bhagavad-gītā pages
│ │ │ ├── page.tsx → Chapter list
│ │ │ └── [chapter]/
│ │ │ ├── page.tsx → Redirects to first verse
│ │ │ └── [verse]/page.tsx → Single verse viewer
│ │ ├── sb/ → Śrīmad-Bhāgavatam pages
│ │ │ ├── page.tsx → Canto list
│ │ │ └── [canto]/
│ │ │ ├── page.tsx → Chapter list
│ │ │ └── [chapter]/
│ │ │ ├── page.tsx → Redirects to first verse
│ │ │ └── [verse]/page.tsx → Single verse viewer
│ │ ├── cc/ → Caitanya-caritāmṛta pages (same pattern, [lila]/[chapter]/[verse])
│ │ ├── sb1962/ → 1962 SB First Canto (same pattern as BG, [chapter]/[verse])
│ │ └── kb/ → Kṛṣṇa Book (prose, no verses)
│ │ ├── page.tsx → Volume list
│ │ └── [volume]/
│ │ ├── page.tsx → Chapter list + intro segments
│ │ └── [slug]/page.tsx → Reading page with sidebar
│ ├── glossary/
│ │ ├── page.tsx → Server component wrapper
│ │ └── glossary-client.tsx → Searchable client component
│ ├── rtfs/
│ │ ├── page.tsx → RTF file listing (gated by ALLOW_RTF env)
│ │ └── [slug]/page.tsx → RTF iframe viewer (gated by ALLOW_RTF env)
│ └── api/
│ ├── books/
│ │ ├── route.ts → GET all book metas
│ │ └── [book]/
│ │ ├── route.ts → GET single book meta
│ │ └── verses/route.ts → GET verses (?chapter=&verse=&division=&indexOnly=&page=&limit=)
│ └── glossary/route.ts → GET all glossary entries
├── components/
│ ├── ui/ → shadcn components (button, card, badge, tooltip, input)
│ ├── page-header.tsx → Shared header with ThemeToggle
│ ├── theme-provider.tsx → next-themes wrapper
│ └── theme-toggle.tsx → Sun/moon toggle
├── lib/
│ ├── db.ts → Prisma client singleton
│ ├── api.ts → Data access layer (getAllBooks, getBookMeta, getChapterVerses, etc.)
│ ├── utils.ts → cn() helper
│ └── jsons/books/ → Extracted JSON data
│ ├── bg/ → BG chapters/verses/*.json + intro.json
│ ├── sb/ → SB cantos/chapters/verses/*.json + intro.json
│ ├── cc/ → CC lilas/chapters/verses/*.json + intro.json
│ ├── sb-1962/ → 1962 SB chapters/verses/*.json + intro.json
│ ├── kb/ → Kṛṣṇa Book
│ │ ├── intro.json → Book metadata + chapter listing (89 chapters)
│ │ ├── vol-1/intro/ → 5 intro segments (flap, dedication, George Harrison, preface, intro)
│ │ ├── vol-1/chapters/ → 54 chapter JSONs
│ │ └── vol-2/chapters/ → 35 chapter JSONs
│ └── glossary/index.json → 2,749 glossary entries
├── prisma/
│ └── schema.prisma → Verse, BookMeta, ProseChapter, GlossaryEntry models
├── scripts/
│ ├── bg_extract.py → BG extraction from bg.html
│ ├── sb_extract.py → SB extraction from sb.html
│ ├── cc_extract.py → CC extraction from cc.html
│ ├── sb1962_extract.py → 1962 SB extraction from others-before-1977.html
│ ├── match_sb1962.py → Match 1962 SB transliterations with standard SB IAST
│ ├── glossary_extract.py → Glossary extraction from glossary.html
│ ├── kb_extract_intro.py → KB book-level intro + chapter listing
│ ├── kb_extract_vol1_intro.py → KB Vol 1 front matter segments
│ ├── kb_extract_vol1_chapters.py → KB Vol 1 chapters
│ ├── kb_extract_vol2_chapters.py → KB Vol 2 chapters
│ ├── fix-bengali-encoding.py → Fix Bengali font encoding in cc.html
│ ├── inject-fonts.py → Inject @font-face rules into converted HTML
│ ├── seed-db.ts → Main MongoDB seeder (all books + glossary)
│ └── seed-kb-vol2.ts → KB Vol 2 seeder (uses $push workaround)
└── public/
├── rtfs/ → Original RTF files (~430MB, 17 files)
├── html/ → HTML converted from RTF (textutil + @font-face injection)
└── fonts/ → Custom TTF fonts (Balaram, ScaGoudy, Indevr, Inbenr, etc.)
```
---
## Database Schema (Prisma + MongoDB)
```prisma
type Synonym {
word String
meanings String[]
}
model Verse {
id String @id @default(auto()) @map("_id") @db.ObjectId
book String // "bg" | "sb" | "cc" | "sb1962"
division String? // null for BG/SB1962; canto for SB; lila for CC
chapter Int
verse Int
verseEnd Int?
reference String
bookTitle String
devanagari String
bengali String? // only CC has Bengali
transliteration String
synonyms Synonym[]
translation String
purport String[]
@@unique([book, division, chapter, verse])
@@index([book])
@@index([book, division])
@@index([book, division, chapter])
@@map("verses")
}
model BookMeta {
id String @id @default(auto()) @map("_id") @db.ObjectId
book String @unique // "bg" | "sb" | "cc" | "sb1962" | "kb"
data Json
@@map("book_meta")
}
model ProseChapter {
id String @id @default(auto()) @map("_id") @db.ObjectId
book String // "kb"
volume Int
chapter Int? // null for intro segments
title String
type String // "chapter" | "intro-segment"
slug String
order Int
paragraphs String[]
@@unique([book, volume, slug])
@@index([book])
@@index([book, volume])
@@index([book, volume, type])
@@map("prose_chapters")
}
model GlossaryEntry {
id String @id @default(auto()) @map("_id") @db.ObjectId
term String
definition String
definitionHtml String
@@unique([term])
@@map("glossary")
}
```
---
## Books Extracted
| Book | Code | Type | Verses/Chapters | Division |
|------|------|------|-----------------|----------|
| Bhagavad-gītā As It Is | `bg` | verse | 653 verses, 18 chapters | — |
| Śrīmad-Bhāgavatam | `sb` | verse | 8,260 verses, 213 chapters | 10 cantos |
| Śrī Caitanya-caritāmṛta | `cc` | verse | 11,348 verses, 62 chapters | 3 līlās (adi/madhya/antya) |
| SB First Canto (1962 Edition) | `sb1962` | verse | 762 verses, 19 chapters | — |
| Kṛṣṇa Book | `kb` | prose | 89 chapters (54+35), 2 volumes | 2 volumes |
| Glossary | — | glossary | 2,749 entries | — |
---
## Font Encoding System
### ScaGoudy → IAST mapping (used in BG, SB, CC, glossary)
```
ä→ā Ä→Ā å→ṛ Å→Ṛ ñ→ṣ Ñ→Ṣ ö→ṭ Ö→Ṭ ë→ṇ Ë→Ṇ ò→ḍ Ò→Ḍ
ç→ś Ç→Ś ì→ṅ ï→ñ ü→ū Ü→Ū é→ī É→Ī à→ṁ ù→ḥ è→ṝ û→ṁl ÿ→ḷ
```
### Bengali fonts (Inbenr/Inbeni in CC)
Custom-encoded fonts where ASCII codepoints render as Bengali script. The cc.html had **Apple Symbol encoding corruption** — `textutil` mapped bytes through Symbol encoding instead of PUA. Fixed by `fix-bengali-encoding.py` which remaps 356,396 characters back to PUA range U+F0xx.
### Devanagari generation
IAST transliteration → Devanagari via `indic-transliteration` library. Key fixes:
- `join_virama_words()` — joins virāma + consonant AND virāma + vowel (fixed 12,000+ rendering issues in SB)
- `normalize_devanagari_anusvara()` — ङ् → ं before velars, ञ् → ं before palatals
---
## Verse Navigation (verse-based books)
- **One verse per page** with path-based URLs: `/books/bg/1/5`, `/books/sb/6/6/1`, `/books/cc/adi/1/3`
- **ChapterViewer** (`app/books/chapter-viewer.tsx`): Client component with:
- Sidebar showing ALL verse numbers (from lightweight index query)
- Prev/Next buttons at top and bottom
- Keyboard navigation (← →)
- Client-side verse caching + prefetching adjacent verses
- Click any verse in sidebar → fetches from API and navigates
- **API route** supports: `?verse=N` (single), `?indexOnly=true` (lightweight index), `?page=&limit=` (paginated)
- Server component SSRs the first verse, client handles navigation
---
## KB (Kṛṣṇa Book) — Prose Book
Different from verse books — chapters are prose paragraphs, no verse structure:
- `ProseChapter` model with `paragraphs: String[]`
- 5 intro segments per volume (flap, dedication, George Harrison intro, preface, introduction)
- Sanskrit verses within prose are grouped into `<div style="text-align:center"><i>...</i></div>` blocks with `<br>` tags
- Reading page has left sidebar with chapter index, prev/next navigation
---
## Data Access Layer (`lib/api.ts`)
```typescript
// Book meta
getAllBooks(): Promise<BookMeta[]>
getBookMeta(book: string): Promise<BookMeta | null>
// Verse index (lightweight — for sidebar)
getChapterVerseIndex(book, division, chapter): Promise<VerseIndexEntry[]>
// Single verse
getSingleVerse(book, division, chapter, verse): Promise<VerseRecord | null>
// Paginated verses
getChapterVerses(book, division, chapter, page?, limit?): Promise<PaginatedVerses>
// Prose chapters (for KB)
getProseChapters(book, volume, type?): Promise<ProseChapterRecord[]>
getProseChapter(book, volume, slug): Promise<ProseChapterRecord | null>
// Glossary
getAllGlossaryEntries(): Promise<GlossaryRecord[]>
```
The data access layer includes `normalizeProse()` which handles mixed storage (some chapters stored with native arrays, some with stringified JSON due to BSON corruption workaround). It also includes `sanitizeHtml()` which strips `<script>`, `<style>`, `<iframe>`, event handlers from paragraphs.
---
## Known Issues
1. **MongoDB BSON wire corruption**: The MongoDB server (7.0.31) behind a proxy/tunnel occasionally corrupts large BSON documents during transmission. Workaround: seed using `insertOne` + `$push` for paragraphs one at a time. The `insertMany` and large `insertOne` operations fail non-deterministically with "Invalid UTF-8 string" or "Unrecognized BSON type" errors. Retrying usually works.
2. **SB verse numbering**: The HTML had 5 duplicate `p20` verse reference tags. Fixed by using the `p21` TEXT N header as the authoritative verse number (corrects the wrong `p20` reference).
3. **SB 1962 chapter 15**: Verses 41-45 have different numbering between the 1962 edition and standard SB. The matching script skips these 5 verses (keeps original plain English transliteration).
4. **RTF routes gated**: `/rtfs` and `/rtfs/[slug]` only accessible when `ALLOW_RTF=true` env variable is set.
5. **`scripts/` excluded from tsconfig**: Added to `"exclude"` array to prevent TypeScript build errors from seed scripts.
6. **MongoDB requires no replica set for reads** but requires replica set for Prisma writes. All writes use the native MongoDB driver instead of Prisma.
---
## Environment Variables
```
DATABASE_URL=mongodb://user:pass@host:port/spsays?authSource=admin
ALLOW_RTF=true # optional — enables RTF viewer routes
```
---
## How to Run
```bash
# Development
npm run dev # Start dev server
# Prisma
npx prisma generate # Regenerate Prisma client after schema changes
# Extraction scripts (Python 3 + beautifulsoup4 + indic-transliteration)
python3 scripts/bg_extract.py # Extract BG → lib/jsons/books/bg/
python3 scripts/sb_extract.py # Extract SB → lib/jsons/books/sb/
python3 scripts/cc_extract.py # Extract CC → lib/jsons/books/cc/
python3 scripts/glossary_extract.py # Extract glossary → lib/jsons/glossary/
python3 scripts/sb1962_extract.py # Extract 1962 SB → lib/jsons/books/sb-1962/
python3 scripts/match_sb1962.py # Match 1962 SB with standard SB IAST
python3 scripts/kb_extract_intro.py # Extract KB intro → lib/jsons/books/kb/intro.json
python3 scripts/kb_extract_vol1_intro.py # KB Vol 1 front matter
python3 scripts/kb_extract_vol1_chapters.py # KB Vol 1 chapters
python3 scripts/kb_extract_vol2_chapters.py # KB Vol 2 chapters
# Database seeding (TypeScript + tsx)
npx tsx scripts/seed-db.ts # Seed all data to MongoDB
npx tsx scripts/seed-kb-vol2.ts # Seed KB Vol 2 (separate due to BSON issues)
# Build
npx next build # Production build
```
---
## URL Routes
| Route | Description |
|-------|-------------|
| `/` | Landing page — Books, Glossary, RTF Viewer cards |
| `/books` | All books index |
| `/books/bg` | BG chapter list |
| `/books/bg/[chapter]` | Redirects to first verse |
| `/books/bg/[chapter]/[verse]` | Single verse viewer |
| `/books/sb` | SB canto list |
| `/books/sb/[canto]` | Chapter list for a canto |
| `/books/sb/[canto]/[chapter]` | Redirects to first verse |
| `/books/sb/[canto]/[chapter]/[verse]` | Single verse viewer |
| `/books/cc` | CC līlā list |
| `/books/cc/[lila]` | Chapter list for a līlā |
| `/books/cc/[lila]/[chapter]` | Redirects to first verse |
| `/books/cc/[lila]/[chapter]/[verse]` | Single verse viewer |
| `/books/sb1962` | 1962 SB chapter list |
| `/books/sb1962/[chapter]` | Redirects to first verse |
| `/books/sb1962/[chapter]/[verse]` | Single verse viewer |
| `/books/kb` | KB volume list |
| `/books/kb/[volume]` | Chapter list + intro segments |
| `/books/kb/[volume]/[slug]` | Prose reading page with sidebar |
| `/glossary` | Searchable glossary (2,749 entries) |
| `/rtfs` | RTF file listing (requires ALLOW_RTF=true) |
| `/rtfs/[slug]` | RTF iframe viewer (requires ALLOW_RTF=true) |
| `/api/books` | GET all book metas |
| `/api/books/[book]` | GET single book meta |
| `/api/books/[book]/verses` | GET verses with query params |
| `/api/glossary` | GET all glossary entries |
---
## Verse JSON Schema (BG/SB/CC/SB1962)
```json
{
"book_title": "Śrīmad-Bhāgavatam",
"canto": 1,
"chapter": 1,
"verse": 1,
"verse_end": null,
"reference": "SB 1.1.1",
"devanagari": "ॐ नमो भगवते वासुदेवाय\nजन्माद्यस्य यतो ...",
"bengali": null,
"transliteration": "om namo bhagavate vāsudevāya\njanmādy asya yato ...",
"synonyms": [
{ "word": "om", "meanings": ["O my Lord"] },
{ "word": "namaḥ", "meanings": ["offering my obeisances"] }
],
"translation": "<b>I offer my obeisances unto Lord Śrī Kṛṣṇa...</b>",
"purport": ["Obeisances unto the Personality of Godhead...", "paragraph 2..."]
}
```
## Prose Chapter JSON Schema (KB)
```json
{
"book": "kb",
"volume": 1,
"chapter": 1,
"title": "Advent of Lord Kṛṣṇa",
"type": "chapter",
"paragraphs": [
"<b>1 /Advent of Lord Kṛṣṇa</b>",
"Once the world was overburdened...",
"<div style=\"text-align:center\"><i>verse line 1<br>verse line 2</i></div>",
"<i>Thus ends the Bhaktivedanta purport of the First Chapter of </i>Kṛṣṇa..."
]
}
```
## Intro Segment JSON Schema (KB)
```json
{
"slug": "words-from-apple",
"title": "Words from Apple — by George Harrison",
"type": "intro-segment",
"book": "kb",
"volume": 1,
"order": 3,
"paragraphs": ["<b>Words From Apple</b>", "Everybody is looking for KṚṢṆA.", "..."]
}
```
## Glossary JSON Schema
```json
{
"title": "Glossary of Vedic Terms",
"description": "A comprehensive glossary of Sanskrit and Vedic terms...",
"totalEntries": 2749,
"letters": ["A", "B", "C", ...],
"entries": [
{
"term": "Ācārya",
"definition": "a spiritual master who teaches by his own example...",
"definition_html": "a spiritual master who teaches by his own example..."
}
]
}
```