diff --git a/kjvstudy_org/routes/__init__.py b/kjvstudy_org/routes/__init__.py
index 9e05dbf..1482c01 100644
--- a/kjvstudy_org/routes/__init__.py
+++ b/kjvstudy_org/routes/__init__.py
@@ -8,6 +8,14 @@ from .study_guides import router as study_guides_router, init_templates as init_
from .commentary import router as commentary_router, init_templates as init_commentary_templates
from .stories import router as stories_router, init_templates as init_stories_templates
from .utility import router as utility_router
+from .bible import router as bible_router, init_bible_templates, init_commentary_functions as init_bible_commentary
+from .reading_plans import router as reading_plans_router, init_templates as init_reading_plans_templates
+from .topics import router as topics_router, init_templates as init_topics_templates
+from .strongs import router as strongs_router, init_templates as init_strongs_templates
+from .timeline import router as timeline_router, init_templates as init_timeline_templates
+from .about import router as about_router, init_templates as init_about_templates
+from .main import router as main_router, init_templates as init_main_templates
+from .misc import router as misc_router, init_templates as init_misc_templates, init_search_family_tree
__all__ = [
'api_router', 'init_api_templates',
@@ -17,4 +25,12 @@ __all__ = [
'commentary_router', 'init_commentary_templates',
'stories_router', 'init_stories_templates',
'utility_router',
+ 'bible_router', 'init_bible_templates', 'init_bible_commentary',
+ 'reading_plans_router', 'init_reading_plans_templates',
+ 'topics_router', 'init_topics_templates',
+ 'strongs_router', 'init_strongs_templates',
+ 'timeline_router', 'init_timeline_templates',
+ 'about_router', 'init_about_templates',
+ 'main_router', 'init_main_templates',
+ 'misc_router', 'init_misc_templates', 'init_search_family_tree',
]
diff --git a/kjvstudy_org/routes/about.py b/kjvstudy_org/routes/about.py
new file mode 100644
index 0000000..e6468de
--- /dev/null
+++ b/kjvstudy_org/routes/about.py
@@ -0,0 +1,269 @@
+"""About routes - stats, cross-references index, and about page."""
+import json
+import re
+from collections import defaultdict
+from pathlib import Path
+
+from fastapi import APIRouter, Request
+from fastapi.responses import HTMLResponse
+from fastapi.templating import Jinja2Templates
+
+from ..kjv import bible
+from ..utils.books import OT_BOOKS, NT_BOOKS
+
+router = APIRouter()
+templates = None
+
+
+def init_templates(t: Jinja2Templates):
+ """Initialize templates for about routes."""
+ global templates
+ templates = t
+
+
+# =============================================================================
+# Routes
+# =============================================================================
+
+@router.get("/about/stats", response_class=HTMLResponse)
+async def stats(request: Request):
+ """Hidden statistics page - comprehensive site metrics"""
+ data_dir = Path(__file__).parent.parent / "data"
+
+ # Bible statistics
+ total_verses = bible.get_verse_count()
+ total_books = len(bible.get_books())
+ total_chapters = len(bible.get_chapters())
+
+ # Calculate words in Bible
+ total_words = sum(len(verse.text.split()) for verse in bible.iter_verses())
+
+ # Count unique book types
+ ot_books = len(OT_BOOKS)
+ nt_books = len(NT_BOOKS)
+
+ # Data file statistics
+ total_json_files = len(list(data_dir.glob('**/*.json')))
+
+ # Verse commentary statistics
+ verse_commentary_files = len(list((data_dir / 'verse_commentary').glob('*.json')))
+ total_commentary_verses = 0
+ total_commentary_words = 0
+ for file in (data_dir / 'verse_commentary').glob('*.json'):
+ data = json.load(open(file))
+ commentary = data.get('commentary', {})
+ for chapter in commentary.values():
+ for verse_data in chapter.values():
+ total_commentary_verses += 1
+ # Count words in analysis + historical
+ analysis = verse_data.get('analysis', '')
+ historical = verse_data.get('historical', '')
+ # Strip HTML tags for accurate word count
+ clean_analysis = re.sub(r'<[^>]+>', '', analysis)
+ clean_historical = re.sub(r'<[^>]+>', '', historical)
+ total_commentary_words += len(clean_analysis.split()) + len(clean_historical.split())
+
+ # Cross-reference statistics
+ cross_reference_files = len(list((data_dir / 'cross_references').glob('*.json')))
+ total_cross_refs = 0
+ verses_with_cross_refs = 0
+ for file in (data_dir / 'cross_references').glob('*.json'):
+ data = json.load(open(file))
+ verses_with_cross_refs += len(data)
+ for verse_refs in data.values():
+ total_cross_refs += len(verse_refs)
+
+ # Red letter statistics
+ red_letter_data = json.load(open(data_dir / 'red_letter_verses.json'))
+ total_red_letter_verses = len(red_letter_data['verses'])
+
+ # Study resources
+ study_guide_files = len(list((data_dir / 'study_guides').glob('*.json')))
+ topic_files = len(list((data_dir / 'topics').glob('*.json')))
+ resource_files = len(list((data_dir / 'resources').glob('*.json')))
+ story_files = len(list((data_dir / 'stories').glob('*.json')))
+
+ # Interlinear data size
+ interlinear_file = data_dir / 'interlinear.json.gz'
+ interlinear_size_mb = interlinear_file.stat().st_size / 1024 / 1024 if interlinear_file.exists() else 0
+
+ # Calculate total data directory size
+ total_data_size = sum(f.stat().st_size for f in data_dir.glob('**/*') if f.is_file())
+ total_data_size_mb = total_data_size / 1024 / 1024
+
+ # Book abbreviations
+ bible_metadata_file = data_dir / 'bible_metadata.json'
+ bible_metadata = json.load(open(bible_metadata_file))
+ total_abbreviations = len(bible_metadata.get('book_abbreviations', {}))
+
+ # Biographies
+ bio_data = json.load(open(data_dir / 'biographies.json'))
+ total_biographies = len(bio_data.get('biographies', {}))
+
+ # Reading plans
+ reading_plan_files = len(list((data_dir / 'reading_plans').glob('*.json')))
+
+ # Strong's concordance
+ strongs_dir = data_dir / 'strongs'
+ if strongs_dir.exists():
+ hebrew_data = json.load(open(strongs_dir / 'hebrew.json'))
+ greek_data = json.load(open(strongs_dir / 'greek.json'))
+ total_hebrew_entries = len(hebrew_data)
+ total_greek_entries = len(greek_data)
+ else:
+ total_hebrew_entries = 0
+ total_greek_entries = 0
+
+ stats_data = {
+ 'bible': {
+ 'total_verses': total_verses,
+ 'total_books': total_books,
+ 'ot_books': ot_books,
+ 'nt_books': nt_books,
+ 'total_chapters': total_chapters,
+ 'total_words': total_words,
+ 'avg_words_per_verse': round(total_words / total_verses, 1),
+ 'avg_verses_per_chapter': round(total_verses / total_chapters, 1),
+ },
+ 'commentary': {
+ 'files': verse_commentary_files,
+ 'verses_covered': total_commentary_verses,
+ 'total_words': total_commentary_words,
+ 'avg_words_per_verse': round(total_commentary_words / total_commentary_verses, 1) if total_commentary_verses > 0 else 0,
+ 'coverage_percent': round((total_commentary_verses / total_verses) * 100, 1),
+ },
+ 'cross_references': {
+ 'files': cross_reference_files,
+ 'verses_with_refs': verses_with_cross_refs,
+ 'total_references': total_cross_refs,
+ 'avg_refs_per_verse': round(total_cross_refs / verses_with_cross_refs, 1) if verses_with_cross_refs > 0 else 0,
+ 'coverage_percent': round((verses_with_cross_refs / total_verses) * 100, 1),
+ },
+ 'red_letter': {
+ 'total_verses': total_red_letter_verses,
+ 'percent_of_bible': round((total_red_letter_verses / total_verses) * 100, 1),
+ },
+ 'study_resources': {
+ 'study_guides': study_guide_files,
+ 'topics': topic_files,
+ 'resources': resource_files,
+ 'stories': story_files,
+ 'biographies': total_biographies,
+ 'reading_plans': reading_plan_files,
+ },
+ 'language_tools': {
+ 'hebrew_entries': total_hebrew_entries,
+ 'greek_entries': total_greek_entries,
+ 'total_strongs': total_hebrew_entries + total_greek_entries,
+ 'interlinear_size_mb': round(interlinear_size_mb, 1),
+ },
+ 'data': {
+ 'total_json_files': total_json_files,
+ 'total_size_mb': round(total_data_size_mb, 1),
+ 'book_abbreviations': total_abbreviations,
+ }
+ }
+
+ books = bible.get_books()
+ breadcrumbs = [
+ {"text": "Home", "url": "/"},
+ {"text": "About", "url": "/about"},
+ {"text": "Statistics", "url": None}
+ ]
+
+ return templates.TemplateResponse(
+ "stats.html",
+ {
+ "request": request,
+ "books": books,
+ "stats": stats_data,
+ "breadcrumbs": breadcrumbs,
+ }
+ )
+
+
+@router.get("/about/cross-references", response_class=HTMLResponse)
+async def cross_references_index(request: Request):
+ """Cross-references index - list all verses with cross-references"""
+ data_dir = Path(__file__).parent.parent / "data" / "cross_references"
+
+ # Build index of all verses with cross-references, grouped by book
+ crossref_index = defaultdict(lambda: defaultdict(list))
+
+ for file in sorted(data_dir.glob('*.json')):
+ with open(file, 'r') as f:
+ data = json.load(f)
+
+ for verse_key, refs in data.items():
+ # Parse verse key: "Book:Chapter:Verse"
+ parts = verse_key.split(':')
+ if len(parts) == 3:
+ book, chapter, verse = parts
+ crossref_index[book][int(chapter)].append({
+ 'verse': int(verse),
+ 'ref_count': len(refs)
+ })
+
+ # Sort books in biblical order (OT then NT)
+ biblical_order = OT_BOOKS + NT_BOOKS
+ book_order = {book: i for i, book in enumerate(biblical_order)}
+
+ # Convert to regular dict and sort
+ crossref_index = {
+ book: {
+ chapter: sorted(verses, key=lambda x: x['verse'])
+ for chapter, verses in sorted(chapters.items())
+ }
+ for book, chapters in sorted(crossref_index.items(), key=lambda x: book_order.get(x[0], 999))
+ }
+
+ # Calculate statistics
+ total_books = len(crossref_index)
+ total_verses = sum(
+ len(verses)
+ for chapters in crossref_index.values()
+ for verses in chapters.values()
+ )
+ total_refs = sum(
+ sum(v['ref_count'] for v in verses)
+ for chapters in crossref_index.values()
+ for verses in chapters.values()
+ )
+
+ books = bible.get_books()
+ breadcrumbs = [
+ {"text": "Home", "url": "/"},
+ {"text": "About", "url": "/about"},
+ {"text": "Cross-References Index", "url": None}
+ ]
+
+ return templates.TemplateResponse(
+ "cross_references_index.html",
+ {
+ "request": request,
+ "books": books,
+ "crossref_index": crossref_index,
+ "total_books": total_books,
+ "total_verses": total_verses,
+ "total_refs": total_refs,
+ "breadcrumbs": breadcrumbs,
+ }
+ )
+
+
+@router.get("/about", response_class=HTMLResponse)
+async def about(request: Request):
+ """About page - site information, creator, data sources, theological approach"""
+ books = bible.get_books()
+ breadcrumbs = [
+ {"text": "Home", "url": "/"},
+ {"text": "About", "url": None}
+ ]
+ return templates.TemplateResponse(
+ "about.html",
+ {
+ "request": request,
+ "books": books,
+ "breadcrumbs": breadcrumbs,
+ }
+ )
diff --git a/kjvstudy_org/routes/main.py b/kjvstudy_org/routes/main.py
new file mode 100644
index 0000000..0dba5c3
--- /dev/null
+++ b/kjvstudy_org/routes/main.py
@@ -0,0 +1,624 @@
+"""Main page routes - homepage, books browser, and resources."""
+import hashlib
+import re
+from datetime import datetime
+
+from fastapi import APIRouter, Request
+from fastapi.responses import HTMLResponse
+from fastapi.templating import Jinja2Templates
+
+from ..kjv import bible
+
+router = APIRouter()
+templates = None
+
+
+def init_templates(t: Jinja2Templates):
+ """Initialize templates for main routes."""
+ global templates
+ templates = t
+
+
+# =============================================================================
+# Helper Functions
+# =============================================================================
+
+def verse_reference_to_url(reference: str):
+ """Convert a verse reference to a URL path.
+
+ Examples:
+ "John 3:16" -> "/book/John/chapter/3#verse-16"
+ "Romans 8:38-39" -> "/book/Romans/chapter/8#verse-38-39"
+ "Ephesians 2:8-9" -> "/book/Ephesians/chapter/2#verse-8-9"
+ """
+ # Pattern: Book Chapter:Verse or Book Chapter:Verse-Verse
+ match = re.match(r'^(.+?)\s+(\d+):(\d+)(?:-(\d+))?$', reference.strip())
+ if not match:
+ return None
+
+ book = match.group(1).strip()
+ chapter = match.group(2)
+ verse_start = match.group(3)
+ verse_end = match.group(4)
+
+ if verse_end:
+ # Verse range - link to chapter with anchor
+ return f"/book/{book}/chapter/{chapter}#verse-{verse_start}-{verse_end}"
+ else:
+ # Single verse - link to chapter with anchor
+ return f"/book/{book}/chapter/{chapter}#verse-{verse_start}"
+
+
+def get_daily_verse(date_str=None):
+ """Get the verse of the day based on a specific date (or current date if not provided)"""
+ # Use date as seed for consistent daily verse
+ if date_str is None:
+ date_str = datetime.now().strftime("%Y-%m-%d")
+ seed = int(hashlib.md5(date_str.encode()).hexdigest(), 16) % 1000000
+
+ # Featured verses for rotation
+ featured_verses = [
+ ("John", 3, 16),
+ ("Jeremiah", 29, 11),
+ ("Philippians", 4, 13),
+ ("Romans", 8, 28),
+ ("Proverbs", 3, 5),
+ ("Isaiah", 41, 10),
+ ("Matthew", 11, 28),
+ ("1 John", 4, 19),
+ ("Psalms", 23, 1),
+ ("2 Corinthians", 5, 17),
+ ("Ephesians", 2, 8),
+ ("Romans", 10, 9),
+ ("1 Peter", 5, 7),
+ ("James", 1, 5),
+ ("Philippians", 4, 19),
+ ("Psalms", 119, 105),
+ ("Matthew", 6, 33),
+ ("Romans", 12, 2),
+ ("1 Corinthians", 13, 13),
+ ("Galatians", 5, 22),
+ ("Hebrews", 11, 1),
+ ("1 Thessalonians", 5, 18),
+ ("Psalms", 46, 1),
+ ("Isaiah", 40, 31),
+ ("Matthew", 5, 16),
+ ("Romans", 15, 13),
+ ("Colossians", 3, 23),
+ ("1 John", 1, 9),
+ ("Psalms", 37, 4),
+ ("Proverbs", 27, 17)
+ ]
+
+ # Select verse based on seed
+ verse_index = seed % len(featured_verses)
+ book, chapter, verse = featured_verses[verse_index]
+
+ verse_text = bible.get_verse_text(book, chapter, verse)
+ if not verse_text:
+ # Fallback to John 3:16
+ book, chapter, verse = "John", 3, 16
+ verse_text = bible.get_verse_text(book, chapter, verse)
+
+ return {
+ "book": book,
+ "chapter": chapter,
+ "verse": verse,
+ "text": verse_text,
+ "reference": f"{book} {chapter}:{verse}",
+ "date": date_str
+ }
+
+
+# =============================================================================
+# Routes
+# =============================================================================
+
+@router.get("/", response_class=HTMLResponse)
+async def read_root(request: Request):
+ books = bible.get_books()
+ daily_verse = get_daily_verse()
+
+ # Define study guide categories
+ study_guides = {
+ "Foundational Studies": [
+ {
+ "title": "New Believer's Guide",
+ "description": "Essential truths for new Christians",
+ "slug": "new-believer",
+ "verses": ["John 3:16", "Romans 10:9", "1 John 1:9", "2 Corinthians 5:17"]
+ },
+ {
+ "title": "Salvation by Grace",
+ "description": "Understanding God's gift of salvation",
+ "slug": "salvation",
+ "verses": ["Ephesians 2:8-9", "Romans 3:23", "Romans 6:23", "Titus 3:5"]
+ },
+ {
+ "title": "The Gospel Message",
+ "description": "The good news of Jesus Christ",
+ "slug": "gospel",
+ "verses": ["1 Corinthians 15:3-4", "Romans 1:16", "Mark 16:15", "Acts 4:12"]
+ }
+ ],
+ "Character & Living": [
+ {
+ "title": "Fruits of the Spirit",
+ "description": "Developing Christian character",
+ "slug": "fruits-spirit",
+ "verses": ["Galatians 5:22-23", "1 Corinthians 13:4-7", "Philippians 4:8", "Colossians 3:12-14"]
+ },
+ {
+ "title": "Prayer & Faith",
+ "description": "Growing in prayer and trust",
+ "slug": "prayer-faith",
+ "verses": ["Matthew 6:9-13", "1 Thessalonians 5:17", "Hebrews 11:1", "James 1:6"]
+ },
+ {
+ "title": "Christian Living",
+ "description": "Walking as followers of Christ",
+ "slug": "christian-living",
+ "verses": ["Romans 12:1-2", "1 Peter 2:9", "Matthew 5:14-16", "Philippians 2:14-16"]
+ }
+ ],
+ "Biblical Themes": [
+ {
+ "title": "God's Love",
+ "description": "Understanding the depth of God's love",
+ "slug": "gods-love",
+ "verses": ["1 John 4:8", "John 3:16", "Romans 8:38-39", "1 John 3:1"]
+ },
+ {
+ "title": "Hope & Comfort",
+ "description": "Finding hope in difficult times",
+ "slug": "hope-comfort",
+ "verses": ["Romans 15:13", "2 Corinthians 1:3-4", "Psalm 23:4", "Isaiah 41:10"]
+ },
+ {
+ "title": "Wisdom & Guidance",
+ "description": "Seeking God's wisdom for life",
+ "slug": "wisdom-guidance",
+ "verses": ["Proverbs 3:5-6", "James 1:5", "Psalm 119:105", "Proverbs 27:17"]
+ }
+ ],
+ "Doctrinal Studies": [
+ {
+ "title": "The Trinity",
+ "description": "Understanding God as Father, Son, and Holy Spirit",
+ "slug": "trinity",
+ "verses": ["Matthew 28:19", "2 Corinthians 13:14", "1 Peter 1:2", "John 14:16-17"]
+ },
+ {
+ "title": "The Resurrection",
+ "description": "Christ's victory over death and our hope",
+ "slug": "resurrection",
+ "verses": ["1 Corinthians 15:20-22", "Romans 6:4-5", "John 11:25-26", "1 Thessalonians 4:16-17"]
+ },
+ {
+ "title": "Heaven & Eternity",
+ "description": "Our eternal home with God",
+ "slug": "heaven-eternity",
+ "verses": ["Revelation 21:1-4", "John 14:2-3", "Philippians 3:20-21", "1 Corinthians 2:9"]
+ }
+ ],
+ "Family & Relationships": [
+ {
+ "title": "Biblical Marriage",
+ "description": "God's design for marriage",
+ "slug": "biblical-marriage",
+ "verses": ["Ephesians 5:22-33", "Genesis 2:24", "1 Corinthians 7:3-5", "Hebrews 13:4"]
+ },
+ {
+ "title": "Raising Children",
+ "description": "Biblical principles for parenting",
+ "slug": "raising-children",
+ "verses": ["Proverbs 22:6", "Ephesians 6:4", "Deuteronomy 6:6-7", "Colossians 3:21"]
+ },
+ {
+ "title": "Money & Stewardship",
+ "description": "Biblical wisdom on finances",
+ "slug": "money-stewardship",
+ "verses": ["Malachi 3:10", "Luke 16:10-11", "1 Timothy 6:10", "Proverbs 3:9-10"]
+ }
+ ]
+ }
+
+ # Process verse references to add URLs
+ for category in study_guides.values():
+ for guide in category:
+ guide['verse_refs'] = [
+ {
+ 'text': verse,
+ 'url': verse_reference_to_url(verse) or '#'
+ }
+ for verse in guide['verses']
+ ]
+
+ return templates.TemplateResponse(
+ request, "index.html", {"books": books, "daily_verse": daily_verse, "study_guides": study_guides}
+ )
+
+
+@router.get("/books", response_class=HTMLResponse)
+async def books_page(request: Request):
+ """Browse all books of the Bible"""
+ books = bible.get_books()
+
+ # Define book categories with types
+ book_types = {
+ # Old Testament
+ 'Genesis': 'law', 'Exodus': 'law', 'Leviticus': 'law', 'Numbers': 'law', 'Deuteronomy': 'law',
+ 'Joshua': 'historical', 'Judges': 'historical', 'Ruth': 'historical',
+ '1 Samuel': 'historical', '2 Samuel': 'historical', '1 Kings': 'historical', '2 Kings': 'historical',
+ '1 Chronicles': 'historical', '2 Chronicles': 'historical', 'Ezra': 'historical',
+ 'Nehemiah': 'historical', 'Esther': 'historical',
+ 'Job': 'wisdom', 'Psalms': 'wisdom', 'Proverbs': 'wisdom', 'Ecclesiastes': 'wisdom', 'Song of Solomon': 'wisdom',
+ 'Isaiah': 'major-prophets', 'Jeremiah': 'major-prophets', 'Lamentations': 'major-prophets',
+ 'Ezekiel': 'major-prophets', 'Daniel': 'major-prophets',
+ 'Hosea': 'minor-prophets', 'Joel': 'minor-prophets', 'Amos': 'minor-prophets',
+ 'Obadiah': 'minor-prophets', 'Jonah': 'minor-prophets', 'Micah': 'minor-prophets',
+ 'Nahum': 'minor-prophets', 'Habakkuk': 'minor-prophets', 'Zephaniah': 'minor-prophets',
+ 'Haggai': 'minor-prophets', 'Zechariah': 'minor-prophets', 'Malachi': 'minor-prophets',
+ # New Testament
+ 'Matthew': 'gospels', 'Mark': 'gospels', 'Luke': 'gospels', 'John': 'gospels',
+ 'Acts': 'acts',
+ 'Romans': 'pauline', '1 Corinthians': 'pauline', '2 Corinthians': 'pauline',
+ 'Galatians': 'pauline', 'Ephesians': 'pauline', 'Philippians': 'pauline', 'Colossians': 'pauline',
+ '1 Thessalonians': 'pauline', '2 Thessalonians': 'pauline',
+ '1 Timothy': 'pauline', '2 Timothy': 'pauline', 'Titus': 'pauline', 'Philemon': 'pauline',
+ 'Hebrews': 'general', 'James': 'general', '1 Peter': 'general', '2 Peter': 'general',
+ '1 John': 'general', '2 John': 'general', '3 John': 'general', 'Jude': 'general',
+ 'Revelation': 'apocalyptic'
+ }
+
+ # Organize books by testament
+ old_testament_books = [
+ 'Genesis', 'Exodus', 'Leviticus', 'Numbers', 'Deuteronomy', 'Joshua', 'Judges', 'Ruth',
+ '1 Samuel', '2 Samuel', '1 Kings', '2 Kings', '1 Chronicles', '2 Chronicles', 'Ezra',
+ 'Nehemiah', 'Esther', 'Job', 'Psalms', 'Proverbs', 'Ecclesiastes', 'Song of Solomon',
+ 'Isaiah', 'Jeremiah', 'Lamentations', 'Ezekiel', 'Daniel', 'Hosea', 'Joel', 'Amos',
+ 'Obadiah', 'Jonah', 'Micah', 'Nahum', 'Habakkuk', 'Zephaniah', 'Haggai', 'Zechariah', 'Malachi'
+ ]
+
+ new_testament_books = [
+ 'Matthew', 'Mark', 'Luke', 'John', 'Acts', 'Romans', '1 Corinthians', '2 Corinthians',
+ 'Galatians', 'Ephesians', 'Philippians', 'Colossians', '1 Thessalonians', '2 Thessalonians',
+ '1 Timothy', '2 Timothy', 'Titus', 'Philemon', 'Hebrews', 'James', '1 Peter', '2 Peter',
+ '1 John', '2 John', '3 John', 'Jude', 'Revelation'
+ ]
+
+ # Get chapter counts for each book
+ def get_chapter_count(book_name):
+ chapters = bible.get_chapters_for_book(book_name)
+ return len(chapters)
+
+ old_testament = [
+ {
+ 'name': book,
+ 'chapters': get_chapter_count(book),
+ 'available': book in books,
+ 'type': book_types.get(book, '')
+ }
+ for book in old_testament_books
+ ]
+
+ new_testament = [
+ {
+ 'name': book,
+ 'chapters': get_chapter_count(book),
+ 'available': book in books,
+ 'type': book_types.get(book, '')
+ }
+ for book in new_testament_books
+ ]
+
+ breadcrumbs = [
+ {"text": "Home", "url": "/"},
+ {"text": "Books", "url": None}
+ ]
+
+ return templates.TemplateResponse(
+ request,
+ "books.html",
+ {
+ "old_testament": old_testament,
+ "new_testament": new_testament,
+ "books": books,
+ "breadcrumbs": breadcrumbs
+ }
+ )
+
+
+@router.get("/resources", response_class=HTMLResponse)
+async def resources_page(request: Request):
+ """Browse all theological resources"""
+ books = bible.get_books()
+
+ # Organize resources into categories
+ resources = {
+ "People": [
+ {
+ "name": "Biblical Prophets",
+ "url": "/biblical-prophets",
+ "description": "Explore the prophetic ministry throughout Scripture, from Isaiah to Malachi",
+ "count": "9 prophets"
+ },
+ {
+ "name": "The Twelve Apostles",
+ "url": "/the-twelve-apostles",
+ "description": "The twelve disciples chosen by Jesus to be witnesses of His ministry",
+ "count": "12 apostles"
+ },
+ {
+ "name": "Women of the Bible",
+ "url": "/women-of-the-bible",
+ "description": "Notable women of Scripture and their significance in redemptive history",
+ "count": "12 women"
+ }
+ ],
+ "Theology": [
+ {
+ "name": "Biblical Angels",
+ "url": "/biblical-angels",
+ "description": "Angelic beings mentioned in Scripture, including Michael, Gabriel, and the heavenly host",
+ "count": "12 entries"
+ },
+ {
+ "name": "The Tetragrammaton",
+ "url": "/tetragrammaton",
+ "description": "The sacred four-letter name of God (YHWH) and its profound significance",
+ "count": "Deep dive"
+ },
+ {
+ "name": "Names of God",
+ "url": "/names-of-god",
+ "description": "The revelation of God's names throughout Scripture and their meanings",
+ "count": "14 names"
+ },
+ {
+ "name": "Parables of Jesus",
+ "url": "/parables",
+ "description": "The parables spoken by Christ to illustrate spiritual truths",
+ "count": "11 parables"
+ },
+ {
+ "name": "Miracles of Jesus",
+ "url": "/miracles-of-jesus",
+ "description": "Signs and wonders manifesting divine authority over nature, disease, demons, and death",
+ "count": "35+ miracles"
+ },
+ {
+ "name": "I Am Statements",
+ "url": "/i-am-statements",
+ "description": "The seven 'I Am' statements of Jesus in John's Gospel revealing His divine nature",
+ "count": "7 statements"
+ },
+ {
+ "name": "The Beatitudes",
+ "url": "/beatitudes",
+ "description": "The blessings proclaimed by Jesus in the Sermon on the Mount",
+ "count": "8 beatitudes"
+ },
+ {
+ "name": "Ten Commandments",
+ "url": "/ten-commandments",
+ "description": "The moral law given by God to Moses on Mount Sinai",
+ "count": "10 commandments"
+ },
+ {
+ "name": "Armor of God",
+ "url": "/armor-of-god",
+ "description": "The spiritual equipment for warfare described in Ephesians 6",
+ "count": "7 pieces"
+ },
+ {
+ "name": "Prayers of the Bible",
+ "url": "/prayers-of-the-bible",
+ "description": "Sacred prayers from the Psalms, Jesus, Paul, and the early church",
+ "count": "20+ prayers"
+ },
+ {
+ "name": "Biblical Covenants",
+ "url": "/biblical-covenants",
+ "description": "Divine covenants established between God and His people",
+ "count": "7 covenants"
+ },
+ {
+ "name": "Fruits of the Spirit",
+ "url": "/fruits-of-the-spirit",
+ "description": "The nine graces of Galatians 5:22-23 manifested in believers through the Holy Spirit",
+ "count": "9 fruits"
+ }
+ ],
+ "Systematic Theology": [
+ {
+ "name": "The Trinity",
+ "url": "/trinity",
+ "description": "The mystery of God revealed as Father, Son, and Holy Spirit—three Persons, one God",
+ "count": "4 categories"
+ },
+ {
+ "name": "Christology",
+ "url": "/christology",
+ "description": "The Person and work of Jesus Christ—His deity, humanity, and offices",
+ "count": "4 categories"
+ },
+ {
+ "name": "Pneumatology",
+ "url": "/pneumatology",
+ "description": "The doctrine of the Holy Spirit—His Person, deity, and work in believers",
+ "count": "4 categories"
+ },
+ {
+ "name": "Soteriology",
+ "url": "/soteriology",
+ "description": "The doctrine of salvation—from election to glorification",
+ "count": "5 categories"
+ },
+ {
+ "name": "Ecclesiology",
+ "url": "/ecclesiology",
+ "description": "The doctrine of the Church—its nature, mission, and governance",
+ "count": "4 categories"
+ },
+ {
+ "name": "Eschatology",
+ "url": "/eschatology",
+ "description": "The doctrine of last things—Christ's return, judgment, and eternal state",
+ "count": "5 categories"
+ },
+ {
+ "name": "The Kingdom of God",
+ "url": "/kingdom-of-god",
+ "description": "God's sovereign reign inaugurated in Christ and consummated at His return",
+ "count": "5 categories"
+ },
+ {
+ "name": "Types and Shadows",
+ "url": "/types-and-shadows",
+ "description": "Old Testament persons, events, and institutions that prefigure Christ",
+ "count": "5 categories"
+ },
+ {
+ "name": "Messianic Prophecies",
+ "url": "/messianic-prophecies",
+ "description": "Old Testament prophecies fulfilled in Jesus Christ",
+ "count": "5 categories"
+ },
+ {
+ "name": "The Blood in Scripture",
+ "url": "/blood-in-scripture",
+ "description": "The theology of blood, sacrifice, and redemption throughout Scripture",
+ "count": "5 categories"
+ },
+ {
+ "name": "Names and Titles of Christ",
+ "url": "/names-of-christ",
+ "description": "The names and titles ascribed to Jesus revealing His Person and work",
+ "count": "5 categories"
+ },
+ {
+ "name": "Spirits & Demons",
+ "url": "/spirits-and-demons",
+ "description": "Biblical demonology—Satan, evil spirits, Legion, and spiritual warfare",
+ "count": "7 categories"
+ },
+ {
+ "name": "Personifications",
+ "url": "/personifications",
+ "description": "Abstract concepts given human form—Wisdom, Folly, Death, Sin, and more",
+ "count": "6 categories"
+ },
+ {
+ "name": "Bibliology",
+ "url": "/bibliology",
+ "description": "The Doctrine of Scripture—inspiration, authority, sufficiency, and preservation",
+ "count": "4 categories"
+ },
+ {
+ "name": "Theology Proper",
+ "url": "/theology-proper",
+ "description": "The Attributes of God—His incommunicable and communicable perfections",
+ "count": "4 categories"
+ },
+ {
+ "name": "Anthropology",
+ "url": "/anthropology",
+ "description": "The Doctrine of Man—creation, constitution, and condition of humanity",
+ "count": "4 categories"
+ },
+ {
+ "name": "Hamartiology",
+ "url": "/hamartiology",
+ "description": "The Doctrine of Sin—its origin, nature, transmission, and consequences",
+ "count": "4 categories"
+ },
+ {
+ "name": "Providence",
+ "url": "/providence",
+ "description": "Divine Providence—God's preservation, governance, and concurrence in all things",
+ "count": "4 categories"
+ },
+ {
+ "name": "Grace",
+ "url": "/grace",
+ "description": "The Doctrine of Grace—common grace, effectual grace, election, and perseverance",
+ "count": "4 categories"
+ },
+ {
+ "name": "Justification",
+ "url": "/justification",
+ "description": "The Doctrine of Justification—declared righteous through faith in Christ alone",
+ "count": "4 categories"
+ },
+ {
+ "name": "Sanctification",
+ "url": "/sanctification",
+ "description": "The Doctrine of Sanctification—progressive holiness through the Spirit",
+ "count": "4 categories"
+ },
+ {
+ "name": "Law and Gospel",
+ "url": "/law-and-gospel",
+ "description": "The distinction between Law and Gospel—God's demands and His gracious provision",
+ "count": "4 categories"
+ },
+ {
+ "name": "Worship",
+ "url": "/worship",
+ "description": "The Doctrine of Worship—regulative principle, elements, and the heart of worship",
+ "count": "4 categories"
+ }
+ ],
+ "History & Culture": [
+ {
+ "name": "Biblical Festivals",
+ "url": "/biblical-festivals",
+ "description": "The appointed feasts and holy days ordained in the Law of Moses",
+ "count": "7 festivals"
+ },
+ {
+ "name": "Biblical Geography",
+ "url": "/biblical-maps",
+ "description": "Locations mentioned in Scripture and their historical significance",
+ "count": "Maps & places",
+ "badge": "Interactive"
+ },
+ {
+ "name": "Biblical Timeline",
+ "url": "/biblical-timeline",
+ "description": "Chronological overview of biblical events from Creation to Revelation",
+ "count": "Timeline"
+ },
+ {
+ "name": "Genealogies",
+ "url": "/family-tree",
+ "description": "Interactive family tree from Adam to Jesus Christ with detailed person profiles",
+ "count": "160+ people",
+ "badge": "Interactive"
+ }
+ ],
+ "Study Tools": [
+ {
+ "name": "Study Guides",
+ "url": "/study-guides",
+ "description": "In-depth guides for studying biblical books, themes, and doctrines",
+ "count": "Multiple guides"
+ }
+ ]
+ }
+
+ breadcrumbs = [
+ {"text": "Home", "url": "/"},
+ {"text": "Resources", "url": None}
+ ]
+
+ return templates.TemplateResponse(
+ request,
+ "resources.html",
+ {
+ "resources": resources,
+ "books": books,
+ "breadcrumbs": breadcrumbs
+ }
+ )
diff --git a/kjvstudy_org/routes/misc.py b/kjvstudy_org/routes/misc.py
new file mode 100644
index 0000000..a3760d3
--- /dev/null
+++ b/kjvstudy_org/routes/misc.py
@@ -0,0 +1,225 @@
+"""Miscellaneous routes - search, interlinear, random verse, verse of the day."""
+import hashlib
+import random
+from datetime import datetime, timedelta
+
+from fastapi import APIRouter, Query, Request
+from fastapi.responses import HTMLResponse, RedirectResponse
+from fastapi.templating import Jinja2Templates
+
+from ..kjv import bible
+from ..utils.search import perform_full_text_search
+
+router = APIRouter()
+templates = None
+
+# Will be set by init_search_family_tree()
+_search_family_tree_fn = None
+
+
+def init_templates(t: Jinja2Templates):
+ """Initialize templates for misc routes."""
+ global templates
+ templates = t
+
+
+def init_search_family_tree(fn):
+ """Initialize the search_family_tree function from server.py."""
+ global _search_family_tree_fn
+ _search_family_tree_fn = fn
+
+
+# =============================================================================
+# Helper Functions
+# =============================================================================
+
+def get_daily_verse(date_str=None):
+ """Get the verse of the day based on a specific date (or current date if not provided)"""
+ # Use date as seed for consistent daily verse
+ if date_str is None:
+ date_str = datetime.now().strftime("%Y-%m-%d")
+ seed = int(hashlib.md5(date_str.encode()).hexdigest(), 16) % 1000000
+
+ # Featured verses for rotation
+ featured_verses = [
+ ("John", 3, 16),
+ ("Jeremiah", 29, 11),
+ ("Philippians", 4, 13),
+ ("Romans", 8, 28),
+ ("Proverbs", 3, 5),
+ ("Isaiah", 41, 10),
+ ("Matthew", 11, 28),
+ ("1 John", 4, 19),
+ ("Psalms", 23, 1),
+ ("2 Corinthians", 5, 17),
+ ("Ephesians", 2, 8),
+ ("Romans", 10, 9),
+ ("1 Peter", 5, 7),
+ ("James", 1, 5),
+ ("Philippians", 4, 19),
+ ("Psalms", 119, 105),
+ ("Matthew", 6, 33),
+ ("Romans", 12, 2),
+ ("1 Corinthians", 13, 13),
+ ("Galatians", 5, 22),
+ ("Hebrews", 11, 1),
+ ("1 Thessalonians", 5, 18),
+ ("Psalms", 46, 1),
+ ("Isaiah", 40, 31),
+ ("Matthew", 5, 16),
+ ("Romans", 15, 13),
+ ("Colossians", 3, 23),
+ ("1 John", 1, 9),
+ ("Psalms", 37, 4),
+ ("Proverbs", 27, 17)
+ ]
+
+ # Select verse based on seed
+ verse_index = seed % len(featured_verses)
+ book, chapter, verse = featured_verses[verse_index]
+
+ verse_text = bible.get_verse_text(book, chapter, verse)
+ if not verse_text:
+ # Fallback to John 3:16
+ book, chapter, verse = "John", 3, 16
+ verse_text = bible.get_verse_text(book, chapter, verse)
+
+ return {
+ "book": book,
+ "chapter": chapter,
+ "verse": verse,
+ "text": verse_text,
+ "reference": f"{book} {chapter}:{verse}",
+ "date": date_str
+ }
+
+
+# =============================================================================
+# Routes
+# =============================================================================
+
+@router.get("/search", response_class=HTMLResponse)
+async def search_page(request: Request, q: str = Query(None, description="Search query")):
+ """Search page with results (includes Bible verses and family tree)"""
+ books = bible.get_books()
+ search_results = []
+ family_tree_results = []
+ is_direct_verse = False
+
+ if q and len(q.strip()) >= 2:
+ # Search Bible verses
+ search_results = perform_full_text_search(q.strip())
+ # Check if this was a direct verse reference match
+ if search_results and len(search_results) == 1 and search_results[0].get("score") == 100.0:
+ is_direct_verse = True
+
+ # Also search family tree (limit to 5 results)
+ if _search_family_tree_fn:
+ family_tree_results = _search_family_tree_fn(q.strip(), limit=5)
+
+ return templates.TemplateResponse(
+ request,
+ "search.html",
+ {
+ "query": q or "",
+ "results": search_results,
+ "family_tree_results": family_tree_results,
+ "books": books,
+ "total_results": len(search_results) + len(family_tree_results),
+ "is_direct_verse": is_direct_verse
+ }
+ )
+
+
+@router.get("/interlinear", response_class=HTMLResponse)
+async def interlinear_landing_page(request: Request):
+ """Landing page explaining interlinear Bible study"""
+ books = bible.get_books()
+
+ # Featured verses with interlinear data
+ featured_verses = [
+ {"reference": "John 3:16", "url": "/book/John/chapter/3/verse/16", "note": "God's love for the world"},
+ {"reference": "Genesis 1:1", "url": "/book/Genesis/chapter/1/verse/1", "note": "In the beginning"},
+ {"reference": "Psalm 23:1", "url": "/book/Psalms/chapter/23/verse/1", "note": "The Lord is my shepherd"},
+ {"reference": "Romans 8:28", "url": "/book/Romans/chapter/8/verse/28", "note": "All things work together for good"},
+ {"reference": "Matthew 28:19", "url": "/book/Matthew/chapter/28/verse/19", "note": "The Great Commission"},
+ {"reference": "1 Corinthians 13:4", "url": "/book/1 Corinthians/chapter/13/verse/4", "note": "Love is patient"},
+ ]
+
+ # Build breadcrumbs
+ breadcrumbs = [
+ {"text": "Home", "url": "/"},
+ {"text": "Interlinear", "url": None}
+ ]
+
+ return templates.TemplateResponse(
+ request,
+ "interlinear_landing.html",
+ {
+ "books": books,
+ "featured_verses": featured_verses,
+ "breadcrumbs": breadcrumbs
+ }
+ )
+
+
+@router.get("/random-verse")
+async def random_verse(request: Request):
+ """Redirect to a random Bible verse"""
+ # Get all books
+ all_books = bible.get_books()
+
+ # Pick a random book
+ book = random.choice(all_books)
+
+ # Get all chapters for this book
+ chapters = bible.get_chapters_for_book(book)
+
+ # Pick a random chapter
+ chapter = random.choice(chapters)
+
+ # Get all verses for this chapter
+ verses = bible.get_verses_by_book_chapter(book, chapter)
+
+ # Pick a random verse
+ verse = random.choice(verses)
+
+ # Redirect to the verse page with cache control headers to ensure fresh random verse each time
+ response = RedirectResponse(url=f"/book/{book}/chapter/{chapter}/verse/{verse.verse}", status_code=302)
+ response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
+ response.headers["Pragma"] = "no-cache"
+ response.headers["Expires"] = "0"
+ return response
+
+
+@router.get("/verse-of-the-day", response_class=HTMLResponse)
+async def verse_of_the_day_page(request: Request):
+ """Verse of the day page"""
+ books = bible.get_books()
+ daily_verse = get_daily_verse()
+
+ # Generate past 30 days of verses
+ past_verses = []
+ today = datetime.now()
+ for i in range(1, 31): # Past 30 days (not including today)
+ past_date = today - timedelta(days=i)
+ date_str = past_date.strftime("%Y-%m-%d")
+ verse = get_daily_verse(date_str)
+ past_verses.append(verse)
+
+ # Build breadcrumbs
+ breadcrumbs = [
+ {"text": "Home", "url": "/"},
+ {"text": "Verse of the Day", "url": "/verse-of-the-day"}
+ ]
+
+ return templates.TemplateResponse(
+ request,
+ "verse_of_the_day.html",
+ {
+ "books": books,
+ "daily_verse": daily_verse,
+ "past_verses": past_verses,
+ "breadcrumbs": breadcrumbs
+ }
+ )
diff --git a/kjvstudy_org/routes/reading_plans.py b/kjvstudy_org/routes/reading_plans.py
new file mode 100644
index 0000000..3bcfb20
--- /dev/null
+++ b/kjvstudy_org/routes/reading_plans.py
@@ -0,0 +1,185 @@
+"""Reading plans routes - browse and view Bible reading plans."""
+import re
+
+from fastapi import APIRouter, HTTPException, Request
+from fastapi.responses import HTMLResponse, StreamingResponse
+from fastapi.templating import Jinja2Templates
+
+from ..kjv import bible
+from ..reading_plans import get_plan, get_plan_summary
+from ..utils.books import normalize_book_name, OT_BOOKS, NT_BOOKS
+from ..utils.pdf import WEASYPRINT_AVAILABLE, render_html_to_pdf_async
+
+router = APIRouter()
+templates = None
+
+
+def init_templates(t: Jinja2Templates):
+ """Initialize templates for reading plans routes."""
+ global templates
+ templates = t
+
+
+# =============================================================================
+# Helper Functions
+# =============================================================================
+
+def parse_reading_reference(ref: str) -> list:
+ """Parse a reading reference like 'Genesis 1-3' or 'Matthew 1' into chapter list.
+
+ Returns list of tuples: [(book, chapter), ...]
+ """
+ # Handle patterns like "Genesis 1-3", "Matthew 1", "1 John 2-3"
+ # Pattern: optional number prefix + book name + chapter range
+ pattern = r'^((?:\d\s+)?[A-Za-z]+(?:\s+[A-Za-z]+)?)\s+(\d+)(?:-(\d+))?$'
+ match = re.match(pattern, ref.strip())
+ if not match:
+ return []
+
+ book = match.group(1)
+ start_ch = int(match.group(2))
+ end_ch = int(match.group(3)) if match.group(3) else start_ch
+
+ # Normalize book name - if it's already canonical, use it as-is
+ normalized = normalize_book_name(book)
+ if not normalized:
+ # Check if it's already a valid canonical name
+ all_books = OT_BOOKS + NT_BOOKS
+ if book in all_books:
+ normalized = book
+ else:
+ return []
+
+ return [(normalized, ch) for ch in range(start_ch, end_ch + 1)]
+
+
+def get_reading_text(readings: list) -> list:
+ """Get the Bible text for a list of reading references.
+
+ Returns list of dicts with book, chapter, and verses.
+ """
+ result = []
+ for ref in readings:
+ chapters = parse_reading_reference(ref)
+ for book, chapter in chapters:
+ verses = bible.get_verses_by_book_chapter(book, chapter)
+ if verses:
+ result.append({
+ 'book': book,
+ 'chapter': chapter,
+ 'verses': verses,
+ 'reference': f"{book} {chapter}"
+ })
+ return result
+
+
+# =============================================================================
+# Routes
+# =============================================================================
+
+@router.get("/reading-plans", response_class=HTMLResponse)
+async def reading_plans_page(request: Request):
+ """Browse Bible reading plans"""
+ books = bible.get_books()
+ plans = get_plan_summary()
+
+ breadcrumbs = [
+ {"text": "Home", "url": "/"},
+ {"text": "Reading Plans", "url": None}
+ ]
+
+ return templates.TemplateResponse(
+ request,
+ "reading_plans.html",
+ {
+ "plans": plans,
+ "books": books,
+ "breadcrumbs": breadcrumbs
+ }
+ )
+
+
+@router.get("/reading-plans/{plan_id}", response_class=HTMLResponse)
+async def reading_plan_detail(request: Request, plan_id: str):
+ """View a specific reading plan"""
+ books = bible.get_books()
+ plan = get_plan(plan_id)
+
+ if not plan:
+ raise HTTPException(status_code=404, detail="Reading plan not found")
+
+ # Pass day info without text - text will be lazy loaded via API
+ all_days = plan.get('days') or plan.get('sample_days', [])
+ days_data = []
+ for day in all_days:
+ day_data = {
+ 'day': day['day'],
+ 'theme': day.get('theme', ''),
+ 'readings': day['readings']
+ }
+ days_data.append(day_data)
+
+ breadcrumbs = [
+ {"text": "Home", "url": "/"},
+ {"text": "Reading Plans", "url": "/reading-plans"},
+ {"text": plan["name"], "url": None}
+ ]
+
+ return templates.TemplateResponse(
+ request,
+ "reading_plan_detail.html",
+ {
+ "plan": plan,
+ "plan_id": plan_id,
+ "books": books,
+ "breadcrumbs": breadcrumbs,
+ "pdf_available": WEASYPRINT_AVAILABLE,
+ "pdf_url": f"/reading-plans/{plan_id}/pdf" if WEASYPRINT_AVAILABLE else None,
+ "days_data": days_data,
+ "total_days": plan.get('duration_days', len(days_data))
+ }
+ )
+
+
+@router.get("/reading-plans/{plan_id}/pdf")
+async def reading_plan_pdf(plan_id: str):
+ """Generate a PDF export for a reading plan."""
+ if not WEASYPRINT_AVAILABLE:
+ raise HTTPException(
+ status_code=503,
+ detail="PDF generation is not available. WeasyPrint system libraries are not installed."
+ )
+
+ plan = get_plan(plan_id)
+ if not plan:
+ raise HTTPException(status_code=404, detail="Reading plan not found")
+
+ # Include full Bible text for all plans (including 365-day plans)
+ include_text = True
+
+ days_with_text = None
+ if include_text:
+ all_days = plan.get('days') or plan.get('sample_days', [])
+ days_with_text = []
+ for day in all_days:
+ day_data = {
+ 'day': day['day'],
+ 'theme': day.get('theme', ''),
+ 'readings': day['readings'],
+ 'text': get_reading_text(day['readings'])
+ }
+ days_with_text.append(day_data)
+
+ html_content = templates.get_template("reading_plan_pdf.html").render(
+ plan=plan,
+ include_text=include_text,
+ days_with_text=days_with_text
+ )
+ pdf_buffer = await render_html_to_pdf_async(html_content)
+
+ filename = f"reading-plan-{plan_id}.pdf"
+ return StreamingResponse(
+ pdf_buffer,
+ media_type="application/pdf",
+ headers={"Content-Disposition": f"attachment; filename={filename}"}
+ )
diff --git a/kjvstudy_org/routes/strongs.py b/kjvstudy_org/routes/strongs.py
new file mode 100644
index 0000000..93895d0
--- /dev/null
+++ b/kjvstudy_org/routes/strongs.py
@@ -0,0 +1,171 @@
+"""Strong's Concordance routes - Hebrew and Greek word study."""
+import re
+
+from fastapi import APIRouter, HTTPException, Request
+from fastapi.responses import HTMLResponse
+from fastapi.templating import Jinja2Templates
+
+from ..kjv import bible
+from ..strongs import format_strongs_entry, search_strongs, get_all_strongs
+from ..interlinear_loader import find_verses_by_strongs
+
+router = APIRouter()
+templates = None
+
+
+def init_templates(t: Jinja2Templates):
+ """Initialize templates for Strong's routes."""
+ global templates
+ templates = t
+
+
+# =============================================================================
+# Routes
+# =============================================================================
+
+@router.get("/strongs", response_class=HTMLResponse)
+async def strongs_index(request: Request, q: str = None):
+ """Strong's Concordance search and lookup page."""
+ results = []
+ if q:
+ results = search_strongs(q, language="both", limit=100)
+
+ books = bible.get_books()
+ breadcrumbs = [
+ {"text": "Home", "url": "/"},
+ {"text": "Strong's Concordance", "url": None}
+ ]
+
+ return templates.TemplateResponse(
+ request,
+ "strongs_index.html",
+ {
+ "query": q or "",
+ "results": results,
+ "books": books,
+ "breadcrumbs": breadcrumbs
+ }
+ )
+
+
+@router.get("/strongs/hebrew", response_class=HTMLResponse)
+async def strongs_hebrew_index(request: Request, page: int = 1):
+ """Paginated index of all Hebrew Strong's entries."""
+ data = get_all_strongs("hebrew", page=page, per_page=100)
+
+ books = bible.get_books()
+ breadcrumbs = [
+ {"text": "Home", "url": "/"},
+ {"text": "Strong's Concordance", "url": "/strongs"},
+ {"text": "Hebrew", "url": None}
+ ]
+
+ return templates.TemplateResponse(
+ request,
+ "strongs_language_index.html",
+ {
+ "language": "Hebrew",
+ "language_code": "hebrew",
+ "entries": data["entries"],
+ "page": data["page"],
+ "total_pages": data["total_pages"],
+ "total": data["total"],
+ "books": books,
+ "breadcrumbs": breadcrumbs
+ }
+ )
+
+
+@router.get("/strongs/greek", response_class=HTMLResponse)
+async def strongs_greek_index(request: Request, page: int = 1):
+ """Paginated index of all Greek Strong's entries."""
+ data = get_all_strongs("greek", page=page, per_page=100)
+
+ books = bible.get_books()
+ breadcrumbs = [
+ {"text": "Home", "url": "/"},
+ {"text": "Strong's Concordance", "url": "/strongs"},
+ {"text": "Greek", "url": None}
+ ]
+
+ return templates.TemplateResponse(
+ request,
+ "strongs_language_index.html",
+ {
+ "language": "Greek",
+ "language_code": "greek",
+ "entries": data["entries"],
+ "page": data["page"],
+ "total_pages": data["total_pages"],
+ "total": data["total"],
+ "books": books,
+ "breadcrumbs": breadcrumbs
+ }
+ )
+
+
+@router.get("/strongs/{strongs_number}", response_class=HTMLResponse)
+async def strongs_entry(request: Request, strongs_number: str):
+ """View a single Strong's concordance entry."""
+ entry = format_strongs_entry(strongs_number)
+
+ if not entry:
+ raise HTTPException(
+ status_code=404,
+ detail=f"Strong's number '{strongs_number}' not found"
+ )
+
+ # Find all verses containing this Strong's number
+ verse_occurrences = find_verses_by_strongs(strongs_number, limit=10000)
+ total_occurrences = len(verse_occurrences)
+
+ # Fetch full verse text for each occurrence and highlight the word
+ for occ in verse_occurrences:
+ verse_text = bible.get_verse_text(occ["book"], occ["chapter"], occ["verse"])
+ if verse_text:
+ # Highlight the English word in the verse text
+ english_word = occ.get("english", "")
+ if english_word and english_word in verse_text:
+ occ["verse_text"] = verse_text.replace(
+ english_word,
+ f'{english_word}',
+ 1 # Only highlight first occurrence
+ )
+ else:
+ occ["verse_text"] = verse_text
+ else:
+ occ["verse_text"] = ""
+
+ # Extract and fetch related Strong's entries from derivation
+ related_entries = []
+ if entry.get("derivation"):
+ # Find all Strong's references like H1234 or G5678
+ strongs_refs = re.findall(r'([HG])(\d+)', entry["derivation"])
+ seen = set()
+ for prefix, num in strongs_refs:
+ ref = f"{prefix}{num}"
+ if ref.upper() != strongs_number.upper() and ref not in seen:
+ seen.add(ref)
+ related = format_strongs_entry(ref)
+ if related:
+ related_entries.append(related)
+
+ books = bible.get_books()
+ breadcrumbs = [
+ {"text": "Home", "url": "/"},
+ {"text": "Strong's Concordance", "url": "/strongs"},
+ {"text": strongs_number.upper(), "url": None}
+ ]
+
+ return templates.TemplateResponse(
+ request,
+ "strongs_entry.html",
+ {
+ "entry": entry,
+ "books": books,
+ "breadcrumbs": breadcrumbs,
+ "verse_occurrences": verse_occurrences,
+ "total_occurrences": total_occurrences,
+ "related_entries": related_entries
+ }
+ )
diff --git a/kjvstudy_org/routes/timeline.py b/kjvstudy_org/routes/timeline.py
new file mode 100644
index 0000000..2c812a2
--- /dev/null
+++ b/kjvstudy_org/routes/timeline.py
@@ -0,0 +1,105 @@
+"""Biblical timeline routes."""
+import json
+from pathlib import Path
+
+from fastapi import APIRouter, HTTPException, Request
+from fastapi.responses import HTMLResponse, StreamingResponse
+from fastapi.templating import Jinja2Templates
+
+from ..kjv import bible
+from ..utils.pdf import WEASYPRINT_AVAILABLE, render_html_to_pdf_async
+
+router = APIRouter()
+templates = None
+
+
+def init_templates(t: Jinja2Templates):
+ """Initialize templates for timeline routes."""
+ global templates
+ templates = t
+
+
+# =============================================================================
+# Helper Functions
+# =============================================================================
+
+def get_biblical_timeline_context():
+ """
+ Load comprehensive biblical timeline data from JSON file.
+
+ Returns tuple of (timeline_events, introduction, chronology_note, chronology_comparison, conclusion)
+ """
+ # Load timeline data from JSON file
+ data_dir = Path(__file__).parent.parent / "data"
+ timeline_path = data_dir / "biblical_timeline.json"
+
+ with open(timeline_path, 'r', encoding='utf-8') as f:
+ timeline_data = json.load(f)
+
+ timeline_events = timeline_data.get("timeline_events", {})
+ introduction = timeline_data.get("introduction", "")
+ chronology_note = timeline_data.get("chronology_note", "")
+ chronology_comparison = timeline_data.get("chronology_comparison", [])
+ conclusion = timeline_data.get("conclusion", "")
+
+ return timeline_events, introduction, chronology_note, chronology_comparison, conclusion
+
+
+# =============================================================================
+# Routes
+# =============================================================================
+
+@router.get("/biblical-timeline", response_class=HTMLResponse)
+def biblical_timeline_page(request: Request):
+ """Biblical timeline page showing major biblical events chronologically"""
+ books = bible.get_books()
+
+ timeline_events, introduction, chronology_note, chronology_comparison, conclusion = get_biblical_timeline_context()
+
+ breadcrumbs = [
+ {"text": "Home", "url": "/"},
+ {"text": "Resources", "url": "/resources"},
+ {"text": "Biblical Timeline", "url": None}
+ ]
+
+ return templates.TemplateResponse(
+ request,
+ "biblical_timeline.html",
+ {
+ "books": books,
+ "timeline_events": timeline_events,
+ "introduction": introduction,
+ "chronology_note": chronology_note,
+ "chronology_comparison": chronology_comparison,
+ "conclusion": conclusion,
+ "breadcrumbs": breadcrumbs,
+ "pdf_available": WEASYPRINT_AVAILABLE
+ }
+ )
+
+
+@router.get("/biblical-timeline/pdf")
+async def biblical_timeline_pdf():
+ """Generate PDF export for the biblical timeline."""
+ if not WEASYPRINT_AVAILABLE:
+ raise HTTPException(
+ status_code=503,
+ detail="PDF generation is not available. WeasyPrint system libraries are not installed."
+ )
+
+ timeline_events, introduction, chronology_note, chronology_comparison, conclusion = get_biblical_timeline_context()
+
+ html_content = templates.get_template("biblical_timeline_pdf.html").render(
+ timeline_events=timeline_events,
+ introduction=introduction,
+ chronology_note=chronology_note,
+ chronology_comparison=chronology_comparison,
+ conclusion=conclusion
+ )
+ pdf_buffer = await render_html_to_pdf_async(html_content)
+
+ return StreamingResponse(
+ pdf_buffer,
+ media_type="application/pdf",
+ headers={"Content-Disposition": "attachment; filename=biblical-timeline.pdf"}
+ )
diff --git a/kjvstudy_org/routes/topics.py b/kjvstudy_org/routes/topics.py
new file mode 100644
index 0000000..5be7b5f
--- /dev/null
+++ b/kjvstudy_org/routes/topics.py
@@ -0,0 +1,100 @@
+"""Topics routes - browse and view topical Bible studies."""
+from fastapi import APIRouter, HTTPException, Request
+from fastapi.responses import HTMLResponse, StreamingResponse
+from fastapi.templating import Jinja2Templates
+
+from ..kjv import bible
+from ..topics import get_all_topics, get_topic_with_text
+from ..utils.pdf import WEASYPRINT_AVAILABLE, render_html_to_pdf_async
+
+router = APIRouter()
+templates = None
+
+
+def init_templates(t: Jinja2Templates):
+ """Initialize templates for topics routes."""
+ global templates
+ templates = t
+
+
+# =============================================================================
+# Routes
+# =============================================================================
+
+@router.get("/topics", response_class=HTMLResponse)
+async def topics_page(request: Request):
+ """Browse topical index of Bible themes"""
+ books = bible.get_books()
+ topics = get_all_topics()
+
+ breadcrumbs = [
+ {"text": "Home", "url": "/"},
+ {"text": "Topics", "url": None}
+ ]
+
+ return templates.TemplateResponse(
+ request,
+ "topics.html",
+ {
+ "topics": topics,
+ "books": books,
+ "breadcrumbs": breadcrumbs,
+ "pdf_available": WEASYPRINT_AVAILABLE
+ }
+ )
+
+
+@router.get("/topics/{topic_name}", response_class=HTMLResponse)
+async def topic_detail(request: Request, topic_name: str):
+ """View verses for a specific topic"""
+ books = bible.get_books()
+ topic = get_topic_with_text(topic_name)
+
+ if not topic:
+ raise HTTPException(status_code=404, detail="Topic not found")
+
+ breadcrumbs = [
+ {"text": "Home", "url": "/"},
+ {"text": "Topics", "url": "/topics"},
+ {"text": topic_name, "url": None}
+ ]
+
+ return templates.TemplateResponse(
+ request,
+ "topic_detail.html",
+ {
+ "topic": topic,
+ "topic_name": topic_name,
+ "books": books,
+ "breadcrumbs": breadcrumbs,
+ "pdf_available": WEASYPRINT_AVAILABLE,
+ "pdf_url": f"/topics/{topic_name}/pdf" if WEASYPRINT_AVAILABLE else None
+ }
+ )
+
+
+@router.get("/topics/{topic_name}/pdf")
+async def topic_detail_pdf(topic_name: str):
+ """Generate a PDF export for a topic detail page."""
+ if not WEASYPRINT_AVAILABLE:
+ raise HTTPException(
+ status_code=503,
+ detail="PDF generation is not available. WeasyPrint system libraries are not installed."
+ )
+
+ topic = get_topic_with_text(topic_name)
+ if not topic:
+ raise HTTPException(status_code=404, detail="Topic not found")
+
+ html_content = templates.get_template("topic_pdf.html").render(
+ topic=topic,
+ topic_name=topic_name,
+ )
+ pdf_buffer = await render_html_to_pdf_async(html_content)
+
+ filename = f"{topic_name}.pdf"
+ return StreamingResponse(
+ pdf_buffer,
+ media_type="application/pdf",
+ headers={"Content-Disposition": f"attachment; filename={filename}"}
+ )
diff --git a/kjvstudy_org/server.py b/kjvstudy_org/server.py
index 3fc0a1f..6918455 100644
--- a/kjvstudy_org/server.py
+++ b/kjvstudy_org/server.py
@@ -35,6 +35,14 @@ from .routes import (
commentary_router, init_commentary_templates,
stories_router, init_stories_templates,
utility_router,
+ bible_router, init_bible_templates, init_bible_commentary,
+ reading_plans_router, init_reading_plans_templates,
+ topics_router, init_topics_templates,
+ strongs_router, init_strongs_templates,
+ timeline_router, init_timeline_templates,
+ about_router, init_about_templates,
+ main_router, init_main_templates,
+ misc_router, init_misc_templates, init_search_family_tree,
)
from .routes.commentary import (
generate_commentary,
@@ -108,6 +116,30 @@ app.include_router(stories_router)
# Include the utility router (sitemap, robots.txt, health)
app.include_router(utility_router)
+# Include the Bible router (book, chapter, verse, interlinear routes)
+app.include_router(bible_router)
+
+# Include the reading plans router
+app.include_router(reading_plans_router)
+
+# Include the topics router
+app.include_router(topics_router)
+
+# Include the Strong's Concordance router
+app.include_router(strongs_router)
+
+# Include the timeline router
+app.include_router(timeline_router)
+
+# Include the about router
+app.include_router(about_router)
+
+# Include the main router (homepage, books, resources)
+app.include_router(main_router)
+
+# Include the misc router (search, interlinear, random-verse, verse-of-the-day)
+app.include_router(misc_router)
+
# Custom OpenAPI schema to only include /api routes
def custom_openapi():
@@ -236,6 +268,15 @@ init_family_tree_templates(templates)
init_study_guides_templates(templates)
init_commentary_templates(templates)
init_stories_templates(templates)
+init_bible_templates(templates)
+init_bible_commentary(generate_commentary, generate_chapter_overview, generate_book_commentary, generate_word_study_sidenotes)
+init_reading_plans_templates(templates)
+init_topics_templates(templates)
+init_strongs_templates(templates)
+init_timeline_templates(templates)
+init_about_templates(templates)
+init_main_templates(templates)
+init_misc_templates(templates)
# Load Scofield commentary for cross-references
scofield_commentary = {}
@@ -267,157 +308,6 @@ async def custom_http_exception_handler(request: Request, exc: StarletteHTTPExce
return await http_exception_handler(request, exc)
-@app.get("/search", response_class=HTMLResponse)
-async def search_page(request: Request, q: str = Query(None, description="Search query")):
- """Search page with results (includes Bible verses and family tree)"""
- books = bible.get_books()
- search_results = []
- family_tree_results = []
- is_direct_verse = False
-
- if q and len(q.strip()) >= 2:
- # Search Bible verses
- search_results = perform_full_text_search(q.strip())
- # Check if this was a direct verse reference match
- if search_results and len(search_results) == 1 and search_results[0].get("score") == 100.0:
- is_direct_verse = True
-
- # Also search family tree (limit to 5 results)
- family_tree_results = search_family_tree(q.strip(), limit=5)
-
- return templates.TemplateResponse(
- request,
- "search.html",
- {
- "query": q or "",
- "results": search_results,
- "family_tree_results": family_tree_results,
- "books": books,
- "total_results": len(search_results) + len(family_tree_results),
- "is_direct_verse": is_direct_verse
- }
- )
-
-@app.get("/interlinear", response_class=HTMLResponse)
-async def interlinear_landing_page(request: Request):
- """Landing page explaining interlinear Bible study"""
- books = bible.get_books()
-
- # Featured verses with interlinear data
- featured_verses = [
- {"reference": "John 3:16", "url": "/book/John/chapter/3/verse/16", "note": "God's love for the world"},
- {"reference": "Genesis 1:1", "url": "/book/Genesis/chapter/1/verse/1", "note": "In the beginning"},
- {"reference": "Psalm 23:1", "url": "/book/Psalms/chapter/23/verse/1", "note": "The Lord is my shepherd"},
- {"reference": "Romans 8:28", "url": "/book/Romans/chapter/8/verse/28", "note": "All things work together for good"},
- {"reference": "Matthew 28:19", "url": "/book/Matthew/chapter/28/verse/19", "note": "The Great Commission"},
- {"reference": "1 Corinthians 13:4", "url": "/book/1 Corinthians/chapter/13/verse/4", "note": "Love is patient"},
- ]
-
- # Build breadcrumbs
- breadcrumbs = [
- {"text": "Home", "url": "/"},
- {"text": "Interlinear", "url": None}
- ]
-
- return templates.TemplateResponse(
- request,
- "interlinear_landing.html",
- {
- "books": books,
- "featured_verses": featured_verses,
- "breadcrumbs": breadcrumbs
- }
- )
-
-
-def verse_reference_to_url(reference: str):
- """Convert a verse reference to a URL path.
-
- Examples:
- "John 3:16" -> "/book/John/chapter/3#verse-16"
- "Romans 8:38-39" -> "/book/Romans/chapter/8#verse-38-39"
- "Ephesians 2:8-9" -> "/book/Ephesians/chapter/2#verse-8-9"
- """
- # Pattern: Book Chapter:Verse or Book Chapter:Verse-Verse
- match = re.match(r'^(.+?)\s+(\d+):(\d+)(?:-(\d+))?$', reference.strip())
- if not match:
- return None
-
- book = match.group(1).strip()
- chapter = match.group(2)
- verse_start = match.group(3)
- verse_end = match.group(4)
-
- if verse_end:
- # Verse range - link to chapter with anchor
- return f"/book/{book}/chapter/{chapter}#verse-{verse_start}-{verse_end}"
- else:
- # Single verse - link to chapter with anchor
- return f"/book/{book}/chapter/{chapter}#verse-{verse_start}"
-
-@app.get("/random-verse")
-async def random_verse(request: Request):
- """Redirect to a random Bible verse"""
- # Get all books
- all_books = bible.get_books()
-
- # Pick a random book
- book = random.choice(all_books)
-
- # Get all chapters for this book
- chapters = bible.get_chapters_for_book(book)
-
- # Pick a random chapter
- chapter = random.choice(chapters)
-
- # Get all verses for this chapter
- verses = bible.get_verses_by_book_chapter(book, chapter)
-
- # Pick a random verse
- verse = random.choice(verses)
-
- # Redirect to the verse page with cache control headers to ensure fresh random verse each time
- response = RedirectResponse(url=f"/book/{book}/chapter/{chapter}/verse/{verse.verse}", status_code=302)
- response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
- response.headers["Pragma"] = "no-cache"
- response.headers["Expires"] = "0"
- return response
-
-
-@app.get("/verse-of-the-day", response_class=HTMLResponse)
-async def verse_of_the_day_page(request: Request):
- """Verse of the day page"""
- books = bible.get_books()
- daily_verse = get_daily_verse()
-
- # Generate past 30 days of verses
- past_verses = []
- today = datetime.now()
- for i in range(1, 31): # Past 30 days (not including today)
- past_date = today - timedelta(days=i)
- date_str = past_date.strftime("%Y-%m-%d")
- verse = get_daily_verse(date_str)
- past_verses.append(verse)
-
- # Build breadcrumbs
- breadcrumbs = [
- {"text": "Home", "url": "/"},
- {"text": "Verse of the Day", "url": "/verse-of-the-day"}
- ]
-
- return templates.TemplateResponse(
- request,
- "verse_of_the_day.html",
- {
- "books": books,
- "daily_verse": daily_verse,
- "past_verses": past_verses,
- "breadcrumbs": breadcrumbs
- }
- )
-
-# Note: API routes have been moved to routes/api.py and are included via app.include_router(api_router)
-
def expand_book_abbreviation(abbrev):
"""Expand common Bible book abbreviations to full names"""
@@ -793,84 +683,6 @@ def search_family_tree(query: str, limit: Optional[int] = None) -> List[Dict]:
return results
-def get_biblical_timeline_context():
- """
- Load comprehensive biblical timeline data from JSON file.
-
- Returns tuple of (timeline_events, introduction, chronology_note, chronology_comparison, conclusion)
- """
- # Load timeline data from JSON file
- data_dir = PathLib(__file__).parent / "data"
- timeline_path = data_dir / "biblical_timeline.json"
-
- with open(timeline_path, 'r', encoding='utf-8') as f:
- timeline_data = json.load(f)
-
- timeline_events = timeline_data.get("timeline_events", {})
- introduction = timeline_data.get("introduction", "")
- chronology_note = timeline_data.get("chronology_note", "")
- chronology_comparison = timeline_data.get("chronology_comparison", [])
- conclusion = timeline_data.get("conclusion", "")
-
- return timeline_events, introduction, chronology_note, chronology_comparison, conclusion
-
-
-@app.get("/biblical-timeline", response_class=HTMLResponse)
-def biblical_timeline_page(request: Request):
- """Biblical timeline page showing major biblical events chronologically"""
- books = bible.get_books()
-
- timeline_events, introduction, chronology_note, chronology_comparison, conclusion = get_biblical_timeline_context()
-
- breadcrumbs = [
- {"text": "Home", "url": "/"},
- {"text": "Resources", "url": "/resources"},
- {"text": "Biblical Timeline", "url": None}
- ]
-
- return templates.TemplateResponse(
- request,
- "biblical_timeline.html",
- {
- "books": books,
- "timeline_events": timeline_events,
- "introduction": introduction,
- "chronology_note": chronology_note,
- "chronology_comparison": chronology_comparison,
- "conclusion": conclusion,
- "breadcrumbs": breadcrumbs,
- "pdf_available": WEASYPRINT_AVAILABLE
- }
- )
-
-
-@app.get("/biblical-timeline/pdf")
-async def biblical_timeline_pdf():
- """Generate PDF export for the biblical timeline."""
- if not WEASYPRINT_AVAILABLE:
- raise HTTPException(
- status_code=503,
- detail="PDF generation is not available. WeasyPrint system libraries are not installed."
- )
-
- timeline_events, introduction, chronology_note, chronology_comparison, conclusion = get_biblical_timeline_context()
-
- html_content = templates.get_template("biblical_timeline_pdf.html").render(
- timeline_events=timeline_events,
- introduction=introduction,
- chronology_note=chronology_note,
- chronology_comparison=chronology_comparison,
- conclusion=conclusion
- )
- pdf_buffer = await render_html_to_pdf_async(html_content)
-
- return StreamingResponse(
- pdf_buffer,
- media_type="application/pdf",
- headers={"Content-Disposition": "attachment; filename=biblical-timeline.pdf"}
- )
-
-
def get_biblical_verses(name):
"""Get relevant Bible verses for a person based on their name"""
verse_map = {
@@ -972,1745 +784,5 @@ def get_daily_verse(date_str=None):
}
-
-@app.get("/about/stats", response_class=HTMLResponse)
-async def stats(request: Request):
- """Hidden statistics page - comprehensive site metrics"""
- import json
- from pathlib import Path
-
- data_dir = Path(__file__).parent / "data"
-
- # Bible statistics
- total_verses = bible.get_verse_count()
- total_books = len(bible.get_books())
- total_chapters = len(bible.get_chapters())
-
- # Calculate words in Bible
- total_words = sum(len(verse.text.split()) for verse in bible.iter_verses())
-
- # Count unique book types
- ot_books = len(OT_BOOKS)
- nt_books = len(NT_BOOKS)
-
- # Data file statistics
- total_json_files = len(list(data_dir.glob('**/*.json')))
-
- # Verse commentary statistics
- verse_commentary_files = len(list((data_dir / 'verse_commentary').glob('*.json')))
- total_commentary_verses = 0
- total_commentary_words = 0
- for file in (data_dir / 'verse_commentary').glob('*.json'):
- data = json.load(open(file))
- commentary = data.get('commentary', {})
- for chapter in commentary.values():
- for verse_data in chapter.values():
- total_commentary_verses += 1
- # Count words in analysis + historical
- analysis = verse_data.get('analysis', '')
- historical = verse_data.get('historical', '')
- # Strip HTML tags for accurate word count
- import re
- clean_analysis = re.sub(r'<[^>]+>', '', analysis)
- clean_historical = re.sub(r'<[^>]+>', '', historical)
- total_commentary_words += len(clean_analysis.split()) + len(clean_historical.split())
-
- # Cross-reference statistics
- cross_reference_files = len(list((data_dir / 'cross_references').glob('*.json')))
- total_cross_refs = 0
- verses_with_cross_refs = 0
- for file in (data_dir / 'cross_references').glob('*.json'):
- data = json.load(open(file))
- verses_with_cross_refs += len(data)
- for verse_refs in data.values():
- total_cross_refs += len(verse_refs)
-
- # Red letter statistics
- red_letter_data = json.load(open(data_dir / 'red_letter_verses.json'))
- total_red_letter_verses = len(red_letter_data['verses'])
-
- # Study resources
- study_guide_files = len(list((data_dir / 'study_guides').glob('*.json')))
- topic_files = len(list((data_dir / 'topics').glob('*.json')))
- resource_files = len(list((data_dir / 'resources').glob('*.json')))
- story_files = len(list((data_dir / 'stories').glob('*.json')))
-
- # Interlinear data size
- interlinear_file = data_dir / 'interlinear.json.gz'
- interlinear_size_mb = interlinear_file.stat().st_size / 1024 / 1024 if interlinear_file.exists() else 0
-
- # Calculate total data directory size
- total_data_size = sum(f.stat().st_size for f in data_dir.glob('**/*') if f.is_file())
- total_data_size_mb = total_data_size / 1024 / 1024
-
- # Book abbreviations
- bible_metadata_file = data_dir / 'bible_metadata.json'
- bible_metadata = json.load(open(bible_metadata_file))
- total_abbreviations = len(bible_metadata.get('book_abbreviations', {}))
-
- # Biographies
- bio_data = json.load(open(data_dir / 'biographies.json'))
- total_biographies = len(bio_data.get('biographies', {}))
-
- # Reading plans
- reading_plan_files = len(list((data_dir / 'reading_plans').glob('*.json')))
-
- # Strong's concordance
- strongs_dir = data_dir / 'strongs'
- if strongs_dir.exists():
- hebrew_data = json.load(open(strongs_dir / 'hebrew.json'))
- greek_data = json.load(open(strongs_dir / 'greek.json'))
- total_hebrew_entries = len(hebrew_data)
- total_greek_entries = len(greek_data)
- else:
- total_hebrew_entries = 0
- total_greek_entries = 0
-
- stats_data = {
- 'bible': {
- 'total_verses': total_verses,
- 'total_books': total_books,
- 'ot_books': ot_books,
- 'nt_books': nt_books,
- 'total_chapters': total_chapters,
- 'total_words': total_words,
- 'avg_words_per_verse': round(total_words / total_verses, 1),
- 'avg_verses_per_chapter': round(total_verses / total_chapters, 1),
- },
- 'commentary': {
- 'files': verse_commentary_files,
- 'verses_covered': total_commentary_verses,
- 'total_words': total_commentary_words,
- 'avg_words_per_verse': round(total_commentary_words / total_commentary_verses, 1) if total_commentary_verses > 0 else 0,
- 'coverage_percent': round((total_commentary_verses / total_verses) * 100, 1),
- },
- 'cross_references': {
- 'files': cross_reference_files,
- 'verses_with_refs': verses_with_cross_refs,
- 'total_references': total_cross_refs,
- 'avg_refs_per_verse': round(total_cross_refs / verses_with_cross_refs, 1) if verses_with_cross_refs > 0 else 0,
- 'coverage_percent': round((verses_with_cross_refs / total_verses) * 100, 1),
- },
- 'red_letter': {
- 'total_verses': total_red_letter_verses,
- 'percent_of_bible': round((total_red_letter_verses / total_verses) * 100, 1),
- },
- 'study_resources': {
- 'study_guides': study_guide_files,
- 'topics': topic_files,
- 'resources': resource_files,
- 'stories': story_files,
- 'biographies': total_biographies,
- 'reading_plans': reading_plan_files,
- },
- 'language_tools': {
- 'hebrew_entries': total_hebrew_entries,
- 'greek_entries': total_greek_entries,
- 'total_strongs': total_hebrew_entries + total_greek_entries,
- 'interlinear_size_mb': round(interlinear_size_mb, 1),
- },
- 'data': {
- 'total_json_files': total_json_files,
- 'total_size_mb': round(total_data_size_mb, 1),
- 'book_abbreviations': total_abbreviations,
- }
- }
-
- books = bible.get_books()
- breadcrumbs = [
- {"text": "Home", "url": "/"},
- {"text": "About", "url": "/about"},
- {"text": "Statistics", "url": None}
- ]
-
- return templates.TemplateResponse(
- "stats.html",
- {
- "request": request,
- "books": books,
- "stats": stats_data,
- "breadcrumbs": breadcrumbs,
- }
- )
-
-
-@app.get("/about/cross-references", response_class=HTMLResponse)
-async def cross_references_index(request: Request):
- """Cross-references index - list all verses with cross-references"""
- from collections import defaultdict
- import json
- from pathlib import Path
-
- data_dir = PathLib(__file__).parent / "data" / "cross_references"
-
- # Build index of all verses with cross-references, grouped by book
- crossref_index = defaultdict(lambda: defaultdict(list))
-
- for file in sorted(data_dir.glob('*.json')):
- with open(file, 'r') as f:
- data = json.load(f)
-
- for verse_key, refs in data.items():
- # Parse verse key: "Book:Chapter:Verse"
- parts = verse_key.split(':')
- if len(parts) == 3:
- book, chapter, verse = parts
- crossref_index[book][int(chapter)].append({
- 'verse': int(verse),
- 'ref_count': len(refs)
- })
-
- # Sort books in biblical order (OT then NT)
- biblical_order = OT_BOOKS + NT_BOOKS
- book_order = {book: i for i, book in enumerate(biblical_order)}
-
- # Convert to regular dict and sort
- crossref_index = {
- book: {
- chapter: sorted(verses, key=lambda x: x['verse'])
- for chapter, verses in sorted(chapters.items())
- }
- for book, chapters in sorted(crossref_index.items(), key=lambda x: book_order.get(x[0], 999))
- }
-
- # Calculate statistics
- total_books = len(crossref_index)
- total_verses = sum(
- len(verses)
- for chapters in crossref_index.values()
- for verses in chapters.values()
- )
- total_refs = sum(
- sum(v['ref_count'] for v in verses)
- for chapters in crossref_index.values()
- for verses in chapters.values()
- )
-
- books = bible.get_books()
- breadcrumbs = [
- {"text": "Home", "url": "/"},
- {"text": "About", "url": "/about"},
- {"text": "Cross-References Index", "url": None}
- ]
-
- return templates.TemplateResponse(
- "cross_references_index.html",
- {
- "request": request,
- "books": books,
- "crossref_index": crossref_index,
- "total_books": total_books,
- "total_verses": total_verses,
- "total_refs": total_refs,
- "breadcrumbs": breadcrumbs,
- }
- )
-
-
-@app.get("/about", response_class=HTMLResponse)
-async def about(request: Request):
- """About page - site information, creator, data sources, theological approach"""
- books = bible.get_books()
- breadcrumbs = [
- {"text": "Home", "url": "/"},
- {"text": "About", "url": None}
- ]
- return templates.TemplateResponse(
- "about.html",
- {
- "request": request,
- "books": books,
- "breadcrumbs": breadcrumbs,
- }
- )
-
-
-@app.get("/", response_class=HTMLResponse)
-async def read_root(request: Request):
- books = bible.get_books()
- daily_verse = get_daily_verse()
-
- # Define study guide categories
- study_guides = {
- "Foundational Studies": [
- {
- "title": "New Believer's Guide",
- "description": "Essential truths for new Christians",
- "slug": "new-believer",
- "verses": ["John 3:16", "Romans 10:9", "1 John 1:9", "2 Corinthians 5:17"]
- },
- {
- "title": "Salvation by Grace",
- "description": "Understanding God's gift of salvation",
- "slug": "salvation",
- "verses": ["Ephesians 2:8-9", "Romans 3:23", "Romans 6:23", "Titus 3:5"]
- },
- {
- "title": "The Gospel Message",
- "description": "The good news of Jesus Christ",
- "slug": "gospel",
- "verses": ["1 Corinthians 15:3-4", "Romans 1:16", "Mark 16:15", "Acts 4:12"]
- }
- ],
- "Character & Living": [
- {
- "title": "Fruits of the Spirit",
- "description": "Developing Christian character",
- "slug": "fruits-spirit",
- "verses": ["Galatians 5:22-23", "1 Corinthians 13:4-7", "Philippians 4:8", "Colossians 3:12-14"]
- },
- {
- "title": "Prayer & Faith",
- "description": "Growing in prayer and trust",
- "slug": "prayer-faith",
- "verses": ["Matthew 6:9-13", "1 Thessalonians 5:17", "Hebrews 11:1", "James 1:6"]
- },
- {
- "title": "Christian Living",
- "description": "Walking as followers of Christ",
- "slug": "christian-living",
- "verses": ["Romans 12:1-2", "1 Peter 2:9", "Matthew 5:14-16", "Philippians 2:14-16"]
- }
- ],
- "Biblical Themes": [
- {
- "title": "God's Love",
- "description": "Understanding the depth of God's love",
- "slug": "gods-love",
- "verses": ["1 John 4:8", "John 3:16", "Romans 8:38-39", "1 John 3:1"]
- },
- {
- "title": "Hope & Comfort",
- "description": "Finding hope in difficult times",
- "slug": "hope-comfort",
- "verses": ["Romans 15:13", "2 Corinthians 1:3-4", "Psalm 23:4", "Isaiah 41:10"]
- },
- {
- "title": "Wisdom & Guidance",
- "description": "Seeking God's wisdom for life",
- "slug": "wisdom-guidance",
- "verses": ["Proverbs 3:5-6", "James 1:5", "Psalm 119:105", "Proverbs 27:17"]
- }
- ],
- "Doctrinal Studies": [
- {
- "title": "The Trinity",
- "description": "Understanding God as Father, Son, and Holy Spirit",
- "slug": "trinity",
- "verses": ["Matthew 28:19", "2 Corinthians 13:14", "1 Peter 1:2", "John 14:16-17"]
- },
- {
- "title": "The Resurrection",
- "description": "Christ's victory over death and our hope",
- "slug": "resurrection",
- "verses": ["1 Corinthians 15:20-22", "Romans 6:4-5", "John 11:25-26", "1 Thessalonians 4:16-17"]
- },
- {
- "title": "Heaven & Eternity",
- "description": "Our eternal home with God",
- "slug": "heaven-eternity",
- "verses": ["Revelation 21:1-4", "John 14:2-3", "Philippians 3:20-21", "1 Corinthians 2:9"]
- }
- ],
- "Family & Relationships": [
- {
- "title": "Biblical Marriage",
- "description": "God's design for marriage",
- "slug": "biblical-marriage",
- "verses": ["Ephesians 5:22-33", "Genesis 2:24", "1 Corinthians 7:3-5", "Hebrews 13:4"]
- },
- {
- "title": "Raising Children",
- "description": "Biblical principles for parenting",
- "slug": "raising-children",
- "verses": ["Proverbs 22:6", "Ephesians 6:4", "Deuteronomy 6:6-7", "Colossians 3:21"]
- },
- {
- "title": "Money & Stewardship",
- "description": "Biblical wisdom on finances",
- "slug": "money-stewardship",
- "verses": ["Malachi 3:10", "Luke 16:10-11", "1 Timothy 6:10", "Proverbs 3:9-10"]
- }
- ]
- }
-
- # Process verse references to add URLs
- for category in study_guides.values():
- for guide in category:
- guide['verse_refs'] = [
- {
- 'text': verse,
- 'url': verse_reference_to_url(verse) or '#'
- }
- for verse in guide['verses']
- ]
-
- return templates.TemplateResponse(
- request, "index.html", {"books": books, "daily_verse": daily_verse, "study_guides": study_guides}
- )
-
-
-@app.get("/books", response_class=HTMLResponse)
-async def books_page(request: Request):
- """Browse all books of the Bible"""
- books = bible.get_books()
-
- # Define book categories with types
- book_types = {
- # Old Testament
- 'Genesis': 'law', 'Exodus': 'law', 'Leviticus': 'law', 'Numbers': 'law', 'Deuteronomy': 'law',
- 'Joshua': 'historical', 'Judges': 'historical', 'Ruth': 'historical',
- '1 Samuel': 'historical', '2 Samuel': 'historical', '1 Kings': 'historical', '2 Kings': 'historical',
- '1 Chronicles': 'historical', '2 Chronicles': 'historical', 'Ezra': 'historical',
- 'Nehemiah': 'historical', 'Esther': 'historical',
- 'Job': 'wisdom', 'Psalms': 'wisdom', 'Proverbs': 'wisdom', 'Ecclesiastes': 'wisdom', 'Song of Solomon': 'wisdom',
- 'Isaiah': 'major-prophets', 'Jeremiah': 'major-prophets', 'Lamentations': 'major-prophets',
- 'Ezekiel': 'major-prophets', 'Daniel': 'major-prophets',
- 'Hosea': 'minor-prophets', 'Joel': 'minor-prophets', 'Amos': 'minor-prophets',
- 'Obadiah': 'minor-prophets', 'Jonah': 'minor-prophets', 'Micah': 'minor-prophets',
- 'Nahum': 'minor-prophets', 'Habakkuk': 'minor-prophets', 'Zephaniah': 'minor-prophets',
- 'Haggai': 'minor-prophets', 'Zechariah': 'minor-prophets', 'Malachi': 'minor-prophets',
- # New Testament
- 'Matthew': 'gospels', 'Mark': 'gospels', 'Luke': 'gospels', 'John': 'gospels',
- 'Acts': 'acts',
- 'Romans': 'pauline', '1 Corinthians': 'pauline', '2 Corinthians': 'pauline',
- 'Galatians': 'pauline', 'Ephesians': 'pauline', 'Philippians': 'pauline', 'Colossians': 'pauline',
- '1 Thessalonians': 'pauline', '2 Thessalonians': 'pauline',
- '1 Timothy': 'pauline', '2 Timothy': 'pauline', 'Titus': 'pauline', 'Philemon': 'pauline',
- 'Hebrews': 'general', 'James': 'general', '1 Peter': 'general', '2 Peter': 'general',
- '1 John': 'general', '2 John': 'general', '3 John': 'general', 'Jude': 'general',
- 'Revelation': 'apocalyptic'
- }
-
- # Organize books by testament
- old_testament_books = [
- 'Genesis', 'Exodus', 'Leviticus', 'Numbers', 'Deuteronomy', 'Joshua', 'Judges', 'Ruth',
- '1 Samuel', '2 Samuel', '1 Kings', '2 Kings', '1 Chronicles', '2 Chronicles', 'Ezra',
- 'Nehemiah', 'Esther', 'Job', 'Psalms', 'Proverbs', 'Ecclesiastes', 'Song of Solomon',
- 'Isaiah', 'Jeremiah', 'Lamentations', 'Ezekiel', 'Daniel', 'Hosea', 'Joel', 'Amos',
- 'Obadiah', 'Jonah', 'Micah', 'Nahum', 'Habakkuk', 'Zephaniah', 'Haggai', 'Zechariah', 'Malachi'
- ]
-
- new_testament_books = [
- 'Matthew', 'Mark', 'Luke', 'John', 'Acts', 'Romans', '1 Corinthians', '2 Corinthians',
- 'Galatians', 'Ephesians', 'Philippians', 'Colossians', '1 Thessalonians', '2 Thessalonians',
- '1 Timothy', '2 Timothy', 'Titus', 'Philemon', 'Hebrews', 'James', '1 Peter', '2 Peter',
- '1 John', '2 John', '3 John', 'Jude', 'Revelation'
- ]
-
- # Get chapter counts for each book
- def get_chapter_count(book_name):
- chapters = bible.get_chapters_for_book(book_name)
- return len(chapters)
-
- old_testament = [
- {
- 'name': book,
- 'chapters': get_chapter_count(book),
- 'available': book in books,
- 'type': book_types.get(book, '')
- }
- for book in old_testament_books
- ]
-
- new_testament = [
- {
- 'name': book,
- 'chapters': get_chapter_count(book),
- 'available': book in books,
- 'type': book_types.get(book, '')
- }
- for book in new_testament_books
- ]
-
- breadcrumbs = [
- {"text": "Home", "url": "/"},
- {"text": "Books", "url": None}
- ]
-
- return templates.TemplateResponse(
- request,
- "books.html",
- {
- "old_testament": old_testament,
- "new_testament": new_testament,
- "books": books,
- "breadcrumbs": breadcrumbs
- }
- )
-
-
-@app.get("/reading-plans", response_class=HTMLResponse)
-async def reading_plans_page(request: Request):
- """Browse Bible reading plans"""
- books = bible.get_books()
- plans = get_plan_summary()
-
- breadcrumbs = [
- {"text": "Home", "url": "/"},
- {"text": "Reading Plans", "url": None}
- ]
-
- return templates.TemplateResponse(
- request,
- "reading_plans.html",
- {
- "plans": plans,
- "books": books,
- "breadcrumbs": breadcrumbs
- }
- )
-
-
-@app.get("/reading-plans/{plan_id}", response_class=HTMLResponse)
-async def reading_plan_detail(request: Request, plan_id: str):
- """View a specific reading plan"""
- books = bible.get_books()
- plan = get_plan(plan_id)
-
- if not plan:
- raise HTTPException(status_code=404, detail="Reading plan not found")
-
- # Pass day info without text - text will be lazy loaded via API
- all_days = plan.get('days') or plan.get('sample_days', [])
- days_data = []
- for day in all_days:
- day_data = {
- 'day': day['day'],
- 'theme': day.get('theme', ''),
- 'readings': day['readings']
- }
- days_data.append(day_data)
-
- breadcrumbs = [
- {"text": "Home", "url": "/"},
- {"text": "Reading Plans", "url": "/reading-plans"},
- {"text": plan["name"], "url": None}
- ]
-
- return templates.TemplateResponse(
- request,
- "reading_plan_detail.html",
- {
- "plan": plan,
- "plan_id": plan_id,
- "books": books,
- "breadcrumbs": breadcrumbs,
- "pdf_available": WEASYPRINT_AVAILABLE,
- "pdf_url": f"/reading-plans/{plan_id}/pdf" if WEASYPRINT_AVAILABLE else None,
- "days_data": days_data,
- "total_days": plan.get('duration_days', len(days_data))
- }
- )
-
-
-def parse_reading_reference(ref: str) -> list:
- """Parse a reading reference like 'Genesis 1-3' or 'Matthew 1' into chapter list.
-
- Returns list of tuples: [(book, chapter), ...]
- """
- # Handle patterns like "Genesis 1-3", "Matthew 1", "1 John 2-3"
- # Pattern: optional number prefix + book name + chapter range
- pattern = r'^((?:\d\s+)?[A-Za-z]+(?:\s+[A-Za-z]+)?)\s+(\d+)(?:-(\d+))?$'
- match = re.match(pattern, ref.strip())
- if not match:
- return []
-
- book = match.group(1)
- start_ch = int(match.group(2))
- end_ch = int(match.group(3)) if match.group(3) else start_ch
-
- # Normalize book name - if it's already canonical, use it as-is
- normalized = normalize_book_name(book)
- if not normalized:
- # Check if it's already a valid canonical name
- all_books = OT_BOOKS + NT_BOOKS
- if book in all_books:
- normalized = book
- else:
- return []
-
- return [(normalized, ch) for ch in range(start_ch, end_ch + 1)]
-
-
-def get_reading_text(readings: list) -> list:
- """Get the Bible text for a list of reading references.
-
- Returns list of dicts with book, chapter, and verses.
- """
- result = []
- for ref in readings:
- chapters = parse_reading_reference(ref)
- for book, chapter in chapters:
- verses = bible.get_verses_by_book_chapter(book, chapter)
- if verses:
- result.append({
- 'book': book,
- 'chapter': chapter,
- 'verses': verses,
- 'reference': f"{book} {chapter}"
- })
- return result
-
-
-@app.get("/reading-plans/{plan_id}/pdf")
-async def reading_plan_pdf(plan_id: str):
- """Generate a PDF export for a reading plan."""
- if not WEASYPRINT_AVAILABLE:
- raise HTTPException(
- status_code=503,
- detail="PDF generation is not available. WeasyPrint system libraries are not installed."
- )
-
- plan = get_plan(plan_id)
- if not plan:
- raise HTTPException(status_code=404, detail="Reading plan not found")
-
- # Include full Bible text for all plans (including 365-day plans)
- include_text = True
-
- days_with_text = None
- if include_text:
- all_days = plan.get('days') or plan.get('sample_days', [])
- days_with_text = []
- for day in all_days:
- day_data = {
- 'day': day['day'],
- 'theme': day.get('theme', ''),
- 'readings': day['readings'],
- 'text': get_reading_text(day['readings'])
- }
- days_with_text.append(day_data)
-
- html_content = templates.get_template("reading_plan_pdf.html").render(
- plan=plan,
- include_text=include_text,
- days_with_text=days_with_text
- )
- pdf_buffer = await render_html_to_pdf_async(html_content)
-
- filename = f"reading-plan-{plan_id}.pdf"
- return StreamingResponse(
- pdf_buffer,
- media_type="application/pdf",
- headers={"Content-Disposition": f"attachment; filename={filename}"}
- )
-
-
-@app.get("/topics", response_class=HTMLResponse)
-async def topics_page(request: Request):
- """Browse topical index of Bible themes"""
- books = bible.get_books()
- topics = get_all_topics()
-
- breadcrumbs = [
- {"text": "Home", "url": "/"},
- {"text": "Topics", "url": None}
- ]
-
- return templates.TemplateResponse(
- request,
- "topics.html",
- {
- "topics": topics,
- "books": books,
- "breadcrumbs": breadcrumbs,
- "pdf_available": WEASYPRINT_AVAILABLE
- }
- )
-
-
-@app.get("/resources", response_class=HTMLResponse)
-async def resources_page(request: Request):
- """Browse all theological resources"""
- books = bible.get_books()
-
- # Organize resources into categories
- resources = {
- "People": [
- {
- "name": "Biblical Prophets",
- "url": "/biblical-prophets",
- "description": "Explore the prophetic ministry throughout Scripture, from Isaiah to Malachi",
- "count": "9 prophets"
- },
- {
- "name": "The Twelve Apostles",
- "url": "/the-twelve-apostles",
- "description": "The twelve disciples chosen by Jesus to be witnesses of His ministry",
- "count": "12 apostles"
- },
- {
- "name": "Women of the Bible",
- "url": "/women-of-the-bible",
- "description": "Notable women of Scripture and their significance in redemptive history",
- "count": "12 women"
- }
- ],
- "Theology": [
- {
- "name": "Biblical Angels",
- "url": "/biblical-angels",
- "description": "Angelic beings mentioned in Scripture, including Michael, Gabriel, and the heavenly host",
- "count": "12 entries"
- },
- {
- "name": "The Tetragrammaton",
- "url": "/tetragrammaton",
- "description": "The sacred four-letter name of God (YHWH) and its profound significance",
- "count": "Deep dive"
- },
- {
- "name": "Names of God",
- "url": "/names-of-god",
- "description": "The revelation of God's names throughout Scripture and their meanings",
- "count": "14 names"
- },
- {
- "name": "Parables of Jesus",
- "url": "/parables",
- "description": "The parables spoken by Christ to illustrate spiritual truths",
- "count": "11 parables"
- },
- {
- "name": "Miracles of Jesus",
- "url": "/miracles-of-jesus",
- "description": "Signs and wonders manifesting divine authority over nature, disease, demons, and death",
- "count": "35+ miracles"
- },
- {
- "name": "I Am Statements",
- "url": "/i-am-statements",
- "description": "The seven 'I Am' statements of Jesus in John's Gospel revealing His divine nature",
- "count": "7 statements"
- },
- {
- "name": "The Beatitudes",
- "url": "/beatitudes",
- "description": "The blessings proclaimed by Jesus in the Sermon on the Mount",
- "count": "8 beatitudes"
- },
- {
- "name": "Ten Commandments",
- "url": "/ten-commandments",
- "description": "The moral law given by God to Moses on Mount Sinai",
- "count": "10 commandments"
- },
- {
- "name": "Armor of God",
- "url": "/armor-of-god",
- "description": "The spiritual equipment for warfare described in Ephesians 6",
- "count": "7 pieces"
- },
- {
- "name": "Prayers of the Bible",
- "url": "/prayers-of-the-bible",
- "description": "Sacred prayers from the Psalms, Jesus, Paul, and the early church",
- "count": "20+ prayers"
- },
- {
- "name": "Biblical Covenants",
- "url": "/biblical-covenants",
- "description": "Divine covenants established between God and His people",
- "count": "7 covenants"
- },
- {
- "name": "Fruits of the Spirit",
- "url": "/fruits-of-the-spirit",
- "description": "The nine graces of Galatians 5:22-23 manifested in believers through the Holy Spirit",
- "count": "9 fruits"
- }
- ],
- "Systematic Theology": [
- {
- "name": "The Trinity",
- "url": "/trinity",
- "description": "The mystery of God revealed as Father, Son, and Holy Spirit—three Persons, one God",
- "count": "4 categories"
- },
- {
- "name": "Christology",
- "url": "/christology",
- "description": "The Person and work of Jesus Christ—His deity, humanity, and offices",
- "count": "4 categories"
- },
- {
- "name": "Pneumatology",
- "url": "/pneumatology",
- "description": "The doctrine of the Holy Spirit—His Person, deity, and work in believers",
- "count": "4 categories"
- },
- {
- "name": "Soteriology",
- "url": "/soteriology",
- "description": "The doctrine of salvation—from election to glorification",
- "count": "5 categories"
- },
- {
- "name": "Ecclesiology",
- "url": "/ecclesiology",
- "description": "The doctrine of the Church—its nature, mission, and governance",
- "count": "4 categories"
- },
- {
- "name": "Eschatology",
- "url": "/eschatology",
- "description": "The doctrine of last things—Christ's return, judgment, and eternal state",
- "count": "5 categories"
- },
- {
- "name": "The Kingdom of God",
- "url": "/kingdom-of-god",
- "description": "God's sovereign reign inaugurated in Christ and consummated at His return",
- "count": "5 categories"
- },
- {
- "name": "Types and Shadows",
- "url": "/types-and-shadows",
- "description": "Old Testament persons, events, and institutions that prefigure Christ",
- "count": "5 categories"
- },
- {
- "name": "Messianic Prophecies",
- "url": "/messianic-prophecies",
- "description": "Old Testament prophecies fulfilled in Jesus Christ",
- "count": "5 categories"
- },
- {
- "name": "The Blood in Scripture",
- "url": "/blood-in-scripture",
- "description": "The theology of blood, sacrifice, and redemption throughout Scripture",
- "count": "5 categories"
- },
- {
- "name": "Names and Titles of Christ",
- "url": "/names-of-christ",
- "description": "The names and titles ascribed to Jesus revealing His Person and work",
- "count": "5 categories"
- },
- {
- "name": "Spirits & Demons",
- "url": "/spirits-and-demons",
- "description": "Biblical demonology—Satan, evil spirits, Legion, and spiritual warfare",
- "count": "7 categories"
- },
- {
- "name": "Personifications",
- "url": "/personifications",
- "description": "Abstract concepts given human form—Wisdom, Folly, Death, Sin, and more",
- "count": "6 categories"
- },
- {
- "name": "Bibliology",
- "url": "/bibliology",
- "description": "The Doctrine of Scripture—inspiration, authority, sufficiency, and preservation",
- "count": "4 categories"
- },
- {
- "name": "Theology Proper",
- "url": "/theology-proper",
- "description": "The Attributes of God—His incommunicable and communicable perfections",
- "count": "4 categories"
- },
- {
- "name": "Anthropology",
- "url": "/anthropology",
- "description": "The Doctrine of Man—creation, constitution, and condition of humanity",
- "count": "4 categories"
- },
- {
- "name": "Hamartiology",
- "url": "/hamartiology",
- "description": "The Doctrine of Sin—its origin, nature, transmission, and consequences",
- "count": "4 categories"
- },
- {
- "name": "Providence",
- "url": "/providence",
- "description": "Divine Providence—God's preservation, governance, and concurrence in all things",
- "count": "4 categories"
- },
- {
- "name": "Grace",
- "url": "/grace",
- "description": "The Doctrine of Grace—common grace, effectual grace, election, and perseverance",
- "count": "4 categories"
- },
- {
- "name": "Justification",
- "url": "/justification",
- "description": "The Doctrine of Justification—declared righteous through faith in Christ alone",
- "count": "4 categories"
- },
- {
- "name": "Sanctification",
- "url": "/sanctification",
- "description": "The Doctrine of Sanctification—progressive holiness through the Spirit",
- "count": "4 categories"
- },
- {
- "name": "Law and Gospel",
- "url": "/law-and-gospel",
- "description": "The distinction between Law and Gospel—God's demands and His gracious provision",
- "count": "4 categories"
- },
- {
- "name": "Worship",
- "url": "/worship",
- "description": "The Doctrine of Worship—regulative principle, elements, and the heart of worship",
- "count": "4 categories"
- }
- ],
- "History & Culture": [
- {
- "name": "Biblical Festivals",
- "url": "/biblical-festivals",
- "description": "The appointed feasts and holy days ordained in the Law of Moses",
- "count": "7 festivals"
- },
- {
- "name": "Biblical Geography",
- "url": "/biblical-maps",
- "description": "Locations mentioned in Scripture and their historical significance",
- "count": "Maps & places",
- "badge": "Interactive"
- },
- {
- "name": "Biblical Timeline",
- "url": "/biblical-timeline",
- "description": "Chronological overview of biblical events from Creation to Revelation",
- "count": "Timeline"
- },
- {
- "name": "Genealogies",
- "url": "/family-tree",
- "description": "Interactive family tree from Adam to Jesus Christ with detailed person profiles",
- "count": "160+ people",
- "badge": "Interactive"
- }
- ],
- "Study Tools": [
- {
- "name": "Study Guides",
- "url": "/study-guides",
- "description": "In-depth guides for studying biblical books, themes, and doctrines",
- "count": "Multiple guides"
- }
- ]
- }
-
- breadcrumbs = [
- {"text": "Home", "url": "/"},
- {"text": "Resources", "url": None}
- ]
-
- return templates.TemplateResponse(
- request,
- "resources.html",
- {
- "resources": resources,
- "books": books,
- "breadcrumbs": breadcrumbs
- }
- )
-
-
-@app.get("/topics/{topic_name}", response_class=HTMLResponse)
-async def topic_detail(request: Request, topic_name: str):
- """View verses for a specific topic"""
- books = bible.get_books()
- topic = get_topic_with_text(topic_name)
-
- if not topic:
- raise HTTPException(status_code=404, detail="Topic not found")
-
- breadcrumbs = [
- {"text": "Home", "url": "/"},
- {"text": "Topics", "url": "/topics"},
- {"text": topic_name, "url": None}
- ]
-
- return templates.TemplateResponse(
- request,
- "topic_detail.html",
- {
- "topic": topic,
- "topic_name": topic_name,
- "books": books,
- "breadcrumbs": breadcrumbs,
- "pdf_available": WEASYPRINT_AVAILABLE,
- "pdf_url": f"/topics/{topic_name}/pdf" if WEASYPRINT_AVAILABLE else None
- }
- )
-
-
-@app.get("/topics/{topic_name}/pdf")
-async def topic_detail_pdf(topic_name: str):
- """Generate a PDF export for a topic detail page."""
- if not WEASYPRINT_AVAILABLE:
- raise HTTPException(
- status_code=503,
- detail="PDF generation is not available. WeasyPrint system libraries are not installed."
- )
-
- topic = get_topic_with_text(topic_name)
- if not topic:
- raise HTTPException(status_code=404, detail="Topic not found")
-
- html_content = templates.get_template("topic_pdf.html").render(
- topic=topic,
- topic_name=topic_name,
- )
- pdf_buffer = await render_html_to_pdf_async(html_content)
-
- filename = f"{topic_name}.pdf"
- return StreamingResponse(
- pdf_buffer,
- media_type="application/pdf",
- headers={"Content-Disposition": f"attachment; filename={filename}"}
- )
-
-
-@app.get("/book/{book}", response_class=HTMLResponse)
-async def read_book(request: Request, book: str):
- # Redirect book name variations to canonical form
- canonical_name = normalize_book_name(book)
- if canonical_name:
- return RedirectResponse(url=f"/book/{canonical_name}", status_code=301)
-
- books = bible.get_books()
- chapters = bible.get_chapters_for_book(book)
-
- if not chapters:
- raise HTTPException(
- status_code=404,
- detail=f"The book '{book}' was not found. Please check the spelling or browse all available books."
- )
-
- # Generate commentary data for the book page
- commentary_data = generate_book_commentary(book, chapters)
-
- # Calculate popularity scores for each chapter
- chapter_popularity = {}
- chapter_explanations = {}
- for chapter in chapters:
- chapter_popularity[chapter] = get_chapter_popularity_score(book, chapter)
- chapter_explanations[chapter] = get_chapter_popularity_explanation(book, chapter)
-
- # Get book introduction data if available
- book_intro = get_book_data(book) if has_book_data(book) else None
-
- # Build breadcrumbs
- breadcrumbs = [
- {"text": "Home", "url": "/"},
- {"text": "Books", "url": "/books"},
- {"text": book, "url": None}
- ]
-
- return templates.TemplateResponse(
- request,
- "book.html",
- {
- "book": book,
- "chapters": chapters,
- "books": books,
- "chapter_popularity": chapter_popularity,
- "chapter_explanations": chapter_explanations,
- "breadcrumbs": breadcrumbs,
- "current_book": book,
- "pdf_available": WEASYPRINT_AVAILABLE,
- "book_intro": book_intro,
- **commentary_data
- },
- )
-
-
-@app.get("/book/{book}/pdf")
-async def book_pdf(request: Request, book: str):
- """Generate a PDF export for an entire Bible book."""
- if not WEASYPRINT_AVAILABLE:
- raise HTTPException(
- status_code=503,
- detail="PDF generation is not available. WeasyPrint system libraries are not installed."
- )
-
- canonical_name = normalize_book_name(book)
- if canonical_name:
- return RedirectResponse(url=f"/book/{canonical_name}/pdf", status_code=301)
-
- chapters = bible.get_chapters_for_book(book)
- if not chapters:
- raise HTTPException(
- status_code=404,
- detail=f"The book '{book}' was not found. Please check the spelling or browse all available books."
- )
-
- chapters_data = []
- total_verses = 0
- for chapter_num in chapters:
- verses = bible.get_verses_by_book_chapter(book, chapter_num)
- if not verses:
- continue
- total_verses += len(verses)
- chapters_data.append({
- "chapter": chapter_num,
- "verses": verses
- })
-
- if not chapters_data:
- raise HTTPException(
- status_code=404,
- detail=f"No verses found for the book '{book}'."
- )
-
- # Get book introduction data if available
- book_intro = get_book_data(book) if has_book_data(book) else None
-
- html_content = templates.get_template("book_pdf.html").render(
- book=book,
- chapters=chapters_data,
- chapter_count=len(chapters_data),
- verse_count=total_verses,
- book_intro=book_intro,
- )
-
- pdf_buffer = await render_html_to_pdf_async(html_content)
- filename = f"{create_slug(book)}.pdf"
-
- return StreamingResponse(
- pdf_buffer,
- media_type="application/pdf",
- headers={"Content-Disposition": f"attachment; filename={filename}"}
- )
-
-
-@app.get("/book/{book}/commentary")
-def book_commentary_redirect(book: str):
- """Redirect old book commentary URLs to book page"""
- return RedirectResponse(url=f"/book/{book}", status_code=301)
-
-
-@app.get("/book/{book}/{chapter}")
-def redirect_chapter_legacy(book: str, chapter: int):
- """Redirect legacy chapter URLs to correct format"""
- return RedirectResponse(url=f"/book/{book}/chapter/{chapter}", status_code=301)
-
-@app.get("/book/{book}/chapter/{chapter}", response_class=HTMLResponse)
-async def read_chapter(request: Request, book: str, chapter: int):
- # Redirect book name variations to canonical form
- canonical_name = normalize_book_name(book)
- if canonical_name:
- return RedirectResponse(url=f"/book/{canonical_name}/chapter/{chapter}", status_code=301)
-
- books = bible.get_books()
- verses = bible.get_verses_by_book_chapter(book, chapter)
- chapters = bible.get_chapters_for_book(book)
-
- if not verses:
- # Check if the book exists first
- if not chapters:
- raise HTTPException(
- status_code=404,
- detail=f"The book '{book}' was not found. Please check the spelling or browse all available books."
- )
- else:
- raise HTTPException(
- status_code=404,
- detail=f"Chapter {chapter} of {book} was not found. This book has {len(chapters)} chapters."
- )
-
-
-
- # Generate AI commentary for the chapter
- commentaries = {}
- shown_words = set() # Track which words have already been shown in this chapter
- for verse in verses:
- commentary = generate_commentary(book, chapter, verse)
- # Add word study sidenotes (avoiding repetition within chapter)
- word_studies = generate_word_study_sidenotes(verse.text, book, chapter, verse.verse, shown_words)
- commentary['word_studies'] = word_studies
- # Track which words were shown
- for study in word_studies:
- shown_words.add(study['word'].lower())
- # Add cross-references with proper URLs, grouped by description
- cross_refs = get_cross_references(book, chapter, verse.verse)
-
- # Group cross-references by their description/note
- from collections import defaultdict
- grouped_refs = defaultdict(list)
- for ref in cross_refs:
- description = ref['note'] if ref['note'] else 'Related'
- # Parse the reference to build URL
- if ' ' in ref['ref'] and ':' in ref['ref']:
- ref_book = ref['ref'].rsplit(' ', 1)[0]
- ref_chapter_verse = ref['ref'].rsplit(' ', 1)[1]
- ref_chapter = ref_chapter_verse.split(':')[0]
- ref_verse = ref_chapter_verse.split(':')[1]
- # Same chapter: use anchor link; different chapter/book: link to chapter view with anchor
- if ref_book == book and ref_chapter == str(chapter):
- url = f"#verse-{ref_verse}"
- else:
- url = f"/book/{ref_book}/chapter/{ref_chapter}#verse-{ref_verse}"
- else:
- url = '#'
- grouped_refs[description].append({
- 'text': ref['ref'],
- 'url': url
- })
-
- # Convert to list of groups for template
- commentary['cross_reference_groups'] = [
- {'description': desc, 'refs': refs}
- for desc, refs in grouped_refs.items()
- ]
- commentaries[verse.verse] = commentary
-
- # Generate chapter overview
- chapter_overview = generate_chapter_overview(book, chapter, verses)
-
- # Build breadcrumbs
- breadcrumbs = [
- {"text": "Home", "url": "/"},
- {"text": "Books", "url": "/books"},
- {"text": book, "url": f"/book/{book}"},
- {"text": f"Chapter {chapter}", "url": None}
- ]
-
- return templates.TemplateResponse(
- request,
- "chapter.html",
- {
- "book": book,
- "chapter": chapter,
- "verses": verses,
- "books": books,
- "chapters": chapters,
- "commentaries": commentaries,
- "chapter_overview": chapter_overview,
- "breadcrumbs": breadcrumbs,
- "current_book": book,
- "current_chapter": chapter,
- "pdf_available": WEASYPRINT_AVAILABLE
- }
- )
-
-
-@app.get("/book/{book}/chapter/{chapter}/pdf")
-async def chapter_pdf(request: Request, book: str, chapter: int):
- """Generate a PDF export for a specific Bible chapter."""
- if not WEASYPRINT_AVAILABLE:
- raise HTTPException(
- status_code=503,
- detail="PDF generation is not available. WeasyPrint system libraries are not installed."
- )
-
- canonical_name = normalize_book_name(book)
- if canonical_name:
- return RedirectResponse(url=f"/book/{canonical_name}/chapter/{chapter}/pdf", status_code=301)
-
- verses = bible.get_verses_by_book_chapter(book, chapter)
- chapters = bible.get_chapters_for_book(book)
-
- if not verses:
- if not chapters:
- raise HTTPException(
- status_code=404,
- detail=f"The book '{book}' was not found. Please check the spelling or browse all available books."
- )
- raise HTTPException(
- status_code=404,
- detail=f"Chapter {chapter} of {book} was not found. This book has {len(chapters)} chapters."
- )
-
- # Generate commentaries with cross-references and word studies for PDF
- commentaries = {}
- shown_words = set()
- for verse in verses:
- commentary = generate_commentary(book, chapter, verse)
- # Add word study sidenotes (avoiding repetition within chapter, more liberal for PDF)
- word_studies = generate_word_study_sidenotes(verse.text, book, chapter, verse.verse, shown_words, for_pdf=True)
- commentary['word_studies'] = word_studies
- for study in word_studies:
- shown_words.add(study['word'].lower())
-
- # Add cross-references grouped by description
- from collections import defaultdict
- cross_refs = get_cross_references(book, chapter, verse.verse)
- grouped_refs = defaultdict(list)
- for ref in cross_refs:
- description = ref['note'] if ref['note'] else 'Related'
- grouped_refs[description].append(ref['ref'])
-
- commentary['cross_reference_groups'] = [
- {'description': desc, 'refs': refs}
- for desc, refs in grouped_refs.items()
- ]
- commentaries[verse.verse] = commentary
-
- # Get book metadata for richer PDF
- book_data = get_book_data(book)
- total_chapters = len(chapters)
-
- # Collect all unique word studies shown for glossary
- glossary = []
- seen_words = set()
- for verse_num, commentary in commentaries.items():
- for study in commentary.get('word_studies', []):
- word_lower = study['word'].lower()
- if word_lower not in seen_words:
- seen_words.add(word_lower)
- glossary.append(study)
- glossary.sort(key=lambda x: x['word'])
-
- html_content = templates.get_template("chapter_pdf.html").render(
- book=book,
- chapter=chapter,
- verses=verses,
- verse_count=len(verses),
- commentaries=commentaries,
- book_data=book_data,
- total_chapters=total_chapters,
- glossary=glossary,
- )
-
- pdf_buffer = await render_html_to_pdf_async(html_content)
- filename = f"{create_slug(book)}-chapter-{chapter}.pdf"
-
- return StreamingResponse(
- pdf_buffer,
- media_type="application/pdf",
- headers={"Content-Disposition": f"attachment; filename={filename}"}
- )
-
-
-@app.get("/book/{book}/chapter/{chapter}/interlinear/pdf")
-async def chapter_interlinear_pdf(book: str, chapter: int):
- """Generate PDF export for interlinear chapter view."""
- # Redirect book name variations to canonical form
- canonical_name = normalize_book_name(book)
- if canonical_name:
- return RedirectResponse(url=f"/book/{canonical_name}/chapter/{chapter}/interlinear/pdf", status_code=301)
-
- if not WEASYPRINT_AVAILABLE:
- raise HTTPException(
- status_code=503,
- detail="PDF generation is not available. WeasyPrint system libraries are not installed."
- )
-
- verses = bible.get_verses_by_book_chapter(book, chapter)
- chapters = bible.get_chapters_for_book(book)
-
- if not verses:
- if not chapters:
- raise HTTPException(status_code=404, detail=f"The book '{book}' was not found.")
- else:
- raise HTTPException(status_code=404, detail=f"Chapter {chapter} of {book} was not found.")
-
- # Get interlinear data for each verse
- verses_with_interlinear = []
- for verse in verses:
- interlinear_words = get_interlinear_data(book, chapter, verse.verse)
- verses_with_interlinear.append({
- 'verse': verse,
- 'interlinear_words': interlinear_words or []
- })
-
- # Determine if OT or NT for language badge
- is_old_testament = book in OT_BOOKS
-
- html_content = templates.get_template("chapter_interlinear_pdf.html").render(
- book=book,
- chapter=chapter,
- verses_with_interlinear=verses_with_interlinear,
- is_old_testament=is_old_testament
- )
- pdf_buffer = await render_html_to_pdf_async(html_content)
-
- filename = f"{book.lower().replace(' ', '-')}-{chapter}-interlinear.pdf"
- return StreamingResponse(
- pdf_buffer,
- media_type="application/pdf",
- headers={"Content-Disposition": f"attachment; filename={filename}"}
- )
-
-
-@app.get("/book/{book}/chapter/{chapter}/interlinear", response_class=HTMLResponse)
-async def read_chapter_interlinear(request: Request, book: str, chapter: int):
- """Display a chapter with interlinear Hebrew/Greek for every verse"""
- # Redirect book name variations to canonical form
- canonical_name = normalize_book_name(book)
- if canonical_name:
- return RedirectResponse(url=f"/book/{canonical_name}/chapter/{chapter}/interlinear", status_code=301)
-
- books = bible.get_books()
- verses = bible.get_verses_by_book_chapter(book, chapter)
- chapters = bible.get_chapters_for_book(book)
-
- if not verses:
- if not chapters:
- raise HTTPException(
- status_code=404,
- detail=f"The book '{book}' was not found."
- )
- else:
- raise HTTPException(
- status_code=404,
- detail=f"Chapter {chapter} of {book} was not found. This book has {len(chapters)} chapters."
- )
-
- # Get interlinear data for each verse
- verses_with_interlinear = []
- for verse in verses:
- interlinear_words = get_interlinear_data(book, chapter, verse.verse)
- verses_with_interlinear.append({
- 'verse': verse,
- 'interlinear_words': interlinear_words or []
- })
-
- # Build breadcrumbs
- breadcrumbs = [
- {"text": "Home", "url": "/"},
- {"text": "Books", "url": "/books"},
- {"text": book, "url": f"/book/{book}"},
- {"text": f"Chapter {chapter}", "url": f"/book/{book}/chapter/{chapter}"},
- {"text": "Interlinear", "url": None}
- ]
-
- # Determine if OT or NT for language badge
- ot_books = ['Genesis', 'Exodus', 'Leviticus', 'Numbers', 'Deuteronomy', 'Joshua', 'Judges', 'Ruth',
- '1 Samuel', '2 Samuel', '1 Kings', '2 Kings', '1 Chronicles', '2 Chronicles',
- 'Ezra', 'Nehemiah', 'Esther', 'Job', 'Psalms', 'Proverbs', 'Ecclesiastes',
- 'Song of Solomon', 'Isaiah', 'Jeremiah', 'Lamentations', 'Ezekiel', 'Daniel',
- 'Hosea', 'Joel', 'Amos', 'Obadiah', 'Jonah', 'Micah', 'Nahum', 'Habakkuk',
- 'Zephaniah', 'Haggai', 'Zechariah', 'Malachi']
- is_old_testament = book in ot_books
-
- return templates.TemplateResponse(
- request,
- "chapter_interlinear.html",
- {
- "book": book,
- "chapter": chapter,
- "verses_with_interlinear": verses_with_interlinear,
- "books": books,
- "chapters": chapters,
- "breadcrumbs": breadcrumbs,
- "current_book": book,
- "current_chapter": chapter,
- "is_old_testament": is_old_testament,
- "pdf_available": WEASYPRINT_AVAILABLE,
- "pdf_url": f"/book/{book}/chapter/{chapter}/interlinear/pdf" if WEASYPRINT_AVAILABLE else None
- }
- )
-
-
-@app.get("/book/{book}/chapter/{chapter}/verse/{verse_num}/pdf")
-async def verse_pdf(book: str, chapter: int, verse_num: int):
- """Generate PDF export for a single verse with commentary."""
- # Redirect book name variations to canonical form
- canonical_name = normalize_book_name(book)
- if canonical_name:
- return RedirectResponse(url=f"/book/{canonical_name}/chapter/{chapter}/verse/{verse_num}/pdf", status_code=301)
-
- if not WEASYPRINT_AVAILABLE:
- raise HTTPException(
- status_code=503,
- detail="PDF generation is not available. WeasyPrint system libraries are not installed."
- )
-
- verses = bible.get_verses_by_book_chapter(book, chapter)
- if not verses:
- raise HTTPException(status_code=404, detail=f"Chapter {chapter} of {book} was not found.")
-
- # Find the specific verse
- verse = None
- for v in verses:
- if v.verse == verse_num:
- verse = v
- break
-
- if not verse:
- raise HTTPException(status_code=404, detail=f"Verse {verse_num} not found in {book} {chapter}.")
-
- # Generate commentary
- try:
- commentary = generate_commentary(book, chapter, verse)
- except Exception:
- commentary = None
-
- # Get cross-references
- cross_refs = get_cross_references(book, chapter, verse_num)
-
- # Get interlinear data
- interlinear_words = get_interlinear_data(book, chapter, verse_num)
-
- # Determine if OT
- is_ot = book in OT_BOOKS
-
- html_content = templates.get_template("verse_pdf.html").render(
- book=book,
- chapter=chapter,
- verse_num=verse_num,
- verse_text=verse.text,
- commentary=commentary,
- cross_references=cross_refs,
- interlinear_words=interlinear_words,
- is_old_testament=is_ot
- )
- pdf_buffer = await render_html_to_pdf_async(html_content)
-
- filename = f"{book.lower().replace(' ', '-')}-{chapter}-{verse_num}.pdf"
- return StreamingResponse(
- pdf_buffer,
- media_type="application/pdf",
- headers={"Content-Disposition": f"attachment; filename={filename}"}
- )
-
-
-@app.get("/book/{book}/chapter/{chapter}/verse/{verse_num}", response_class=HTMLResponse)
-async def read_verse(request: Request, book: str, chapter: int, verse_num: int):
- """Display a single verse with detailed commentary"""
- # Redirect book name variations to canonical form
- canonical_name = normalize_book_name(book)
- if canonical_name:
- return RedirectResponse(url=f"/book/{canonical_name}/chapter/{chapter}/verse/{verse_num}", status_code=301)
-
- books = bible.get_books()
- verses = bible.get_verses_by_book_chapter(book, chapter)
- chapters = bible.get_chapters_for_book(book)
-
- if not verses:
- # Check if the book exists first
- if not chapters:
- raise HTTPException(
- status_code=404,
- detail=f"The book '{book}' was not found. Please check the spelling or browse all available books."
- )
- else:
- raise HTTPException(
- status_code=404,
- detail=f"Chapter {chapter} of {book} was not found. This book has {len(chapters)} chapters."
- )
-
- # Find the specific verse
- verse = None
- for v in verses:
- if v.verse == verse_num:
- verse = v
- break
-
- if not verse:
- raise HTTPException(
- status_code=404,
- detail=f"Verse {verse_num} not found in {book} {chapter}. This chapter has {len(verses)} verses."
- )
-
- # Generate commentary for this verse
- try:
- commentary = generate_commentary(book, chapter, verse)
- except Exception as e:
- # Log the error but don't fail the request
- print(f"Error generating commentary for {book} {chapter}:{verse_num}: {e}")
- commentary = None
-
- # Get cross-references for this verse
- cross_refs = get_cross_references(book, chapter, verse_num)
-
- # Check if interlinear data is available and load it
- has_interlinear = has_interlinear_data(book, chapter, verse_num)
- interlinear_words = get_interlinear_data(book, chapter, verse_num) if has_interlinear else None
-
- # Get related content for internal linking
- related_content = get_related_content(book, chapter, verse_num)
-
- # Build breadcrumbs
- breadcrumbs = [
- {"text": "Home", "url": "/"},
- {"text": "Books", "url": "/books"},
- {"text": book, "url": f"/book/{book}"},
- {"text": f"Chapter {chapter}", "url": f"/book/{book}/chapter/{chapter}"},
- {"text": f"Verse {verse_num}", "url": None}
- ]
-
- # Determine if Old Testament for interlinear styling
- is_ot = book in OT_BOOKS
-
- return templates.TemplateResponse(
- request,
- "verse.html",
- {
- "book": book,
- "chapter": chapter,
- "verse_num": verse_num,
- "verse_text": verse.text,
- "commentary": commentary,
- "cross_references": cross_refs,
- "total_verses": len(verses),
- "books": books,
- "chapters": chapters,
- "breadcrumbs": breadcrumbs,
- "current_book": book,
- "current_chapter": chapter,
- "current_verse": verse_num,
- "has_interlinear": has_interlinear,
- "interlinear_words": interlinear_words,
- "related_content": related_content,
- "is_old_testament": is_ot,
- "pdf_available": WEASYPRINT_AVAILABLE,
- "pdf_url": f"/book/{book}/chapter/{chapter}/verse/{verse_num}/pdf" if WEASYPRINT_AVAILABLE else None
- }
- )
-
-
-# =============================================================================
-# Strong's Concordance Routes
-# =============================================================================
-
-@app.get("/strongs", response_class=HTMLResponse)
-async def strongs_index(request: Request, q: str = None):
- """Strong's Concordance search and lookup page."""
- results = []
- if q:
- results = search_strongs(q, language="both", limit=100)
-
- books = bible.get_books()
- breadcrumbs = [
- {"text": "Home", "url": "/"},
- {"text": "Strong's Concordance", "url": None}
- ]
-
- return templates.TemplateResponse(
- request,
- "strongs_index.html",
- {
- "query": q or "",
- "results": results,
- "books": books,
- "breadcrumbs": breadcrumbs
- }
- )
-
-
-@app.get("/strongs/hebrew", response_class=HTMLResponse)
-async def strongs_hebrew_index(request: Request, page: int = 1):
- """Paginated index of all Hebrew Strong's entries."""
- data = get_all_strongs("hebrew", page=page, per_page=100)
-
- books = bible.get_books()
- breadcrumbs = [
- {"text": "Home", "url": "/"},
- {"text": "Strong's Concordance", "url": "/strongs"},
- {"text": "Hebrew", "url": None}
- ]
-
- return templates.TemplateResponse(
- request,
- "strongs_language_index.html",
- {
- "language": "Hebrew",
- "language_code": "hebrew",
- "entries": data["entries"],
- "page": data["page"],
- "total_pages": data["total_pages"],
- "total": data["total"],
- "books": books,
- "breadcrumbs": breadcrumbs
- }
- )
-
-
-@app.get("/strongs/greek", response_class=HTMLResponse)
-async def strongs_greek_index(request: Request, page: int = 1):
- """Paginated index of all Greek Strong's entries."""
- data = get_all_strongs("greek", page=page, per_page=100)
-
- books = bible.get_books()
- breadcrumbs = [
- {"text": "Home", "url": "/"},
- {"text": "Strong's Concordance", "url": "/strongs"},
- {"text": "Greek", "url": None}
- ]
-
- return templates.TemplateResponse(
- request,
- "strongs_language_index.html",
- {
- "language": "Greek",
- "language_code": "greek",
- "entries": data["entries"],
- "page": data["page"],
- "total_pages": data["total_pages"],
- "total": data["total"],
- "books": books,
- "breadcrumbs": breadcrumbs
- }
- )
-
-
-@app.get("/strongs/{strongs_number}", response_class=HTMLResponse)
-async def strongs_entry(request: Request, strongs_number: str):
- """View a single Strong's concordance entry."""
- import re
-
- entry = format_strongs_entry(strongs_number)
-
- if not entry:
- raise HTTPException(
- status_code=404,
- detail=f"Strong's number '{strongs_number}' not found"
- )
-
- # Find all verses containing this Strong's number
- verse_occurrences = find_verses_by_strongs(strongs_number, limit=10000)
- total_occurrences = len(verse_occurrences)
-
- # Fetch full verse text for each occurrence and highlight the word
- for occ in verse_occurrences:
- verse_text = bible.get_verse_text(occ["book"], occ["chapter"], occ["verse"])
- if verse_text:
- # Highlight the English word in the verse text
- english_word = occ.get("english", "")
- if english_word and english_word in verse_text:
- occ["verse_text"] = verse_text.replace(
- english_word,
- f'{english_word}',
- 1 # Only highlight first occurrence
- )
- else:
- occ["verse_text"] = verse_text
- else:
- occ["verse_text"] = ""
-
- # Extract and fetch related Strong's entries from derivation
- related_entries = []
- if entry.get("derivation"):
- # Find all Strong's references like H1234 or G5678
- strongs_refs = re.findall(r'([HG])(\d+)', entry["derivation"])
- seen = set()
- for prefix, num in strongs_refs:
- ref = f"{prefix}{num}"
- if ref.upper() != strongs_number.upper() and ref not in seen:
- seen.add(ref)
- related = format_strongs_entry(ref)
- if related:
- related_entries.append(related)
-
- books = bible.get_books()
- breadcrumbs = [
- {"text": "Home", "url": "/"},
- {"text": "Strong's Concordance", "url": "/strongs"},
- {"text": strongs_number.upper(), "url": None}
- ]
-
- return templates.TemplateResponse(
- request,
- "strongs_entry.html",
- {
- "entry": entry,
- "books": books,
- "breadcrumbs": breadcrumbs,
- "verse_occurrences": verse_occurrences,
- "total_occurrences": total_occurrences,
- "related_entries": related_entries
- }
- )
+# Initialize the search_family_tree function in misc routes
+init_search_family_tree(search_family_tree)