From 8ca561d59160fe78d81931b063cd80e3cedf18f7 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Tue, 2 Dec 2025 18:08:47 -0500 Subject: [PATCH] Add Related Resources section to chapter pages with stories and topics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add related_content to chapter page route with topics, people, resources, and stories - Add Bible stories matching based on verse references in story data - Improve topic matching to use actual verse references instead of hardcoded book list - Display Related Resources section at bottom of chapter and verse pages 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- kjvstudy_org/routes/bible.py | 643 ++++++++++++++++++++++++++++ kjvstudy_org/templates/chapter.html | 64 +++ kjvstudy_org/templates/verse.html | 59 ++- kjvstudy_org/utils/helpers.py | 72 +++- 4 files changed, 806 insertions(+), 32 deletions(-) create mode 100644 kjvstudy_org/routes/bible.py diff --git a/kjvstudy_org/routes/bible.py b/kjvstudy_org/routes/bible.py new file mode 100644 index 0000000..72e08b0 --- /dev/null +++ b/kjvstudy_org/routes/bible.py @@ -0,0 +1,643 @@ +"""Bible routes - book, chapter, verse, and interlinear views.""" +from collections import defaultdict + +from fastapi import APIRouter, HTTPException, Request +from fastapi.responses import HTMLResponse, RedirectResponse, StreamingResponse +from fastapi.templating import Jinja2Templates + +from ..kjv import bible +from ..cross_references import get_cross_references +from ..interlinear_loader import get_interlinear_data, has_interlinear_data +from ..books import get_book_data, has_book_data +from ..utils.books import normalize_book_name, OT_BOOKS +from ..utils.helpers import create_slug, get_related_content, get_chapter_popularity_score, get_chapter_popularity_explanation +from ..utils.pdf import WEASYPRINT_AVAILABLE, render_html_to_pdf_async + +router = APIRouter() +templates = None + + +def init_bible_templates(t: Jinja2Templates): + """Initialize templates for Bible routes.""" + global templates + templates = t + + +# Import these from commentary route to avoid circular imports +generate_commentary = None +generate_chapter_overview = None +generate_book_commentary = None +generate_word_study_sidenotes = None + + +def init_commentary_functions(commentary_func, chapter_overview_func, book_commentary_func, word_study_func): + """Initialize commentary functions.""" + global generate_commentary, generate_chapter_overview, generate_book_commentary, generate_word_study_sidenotes + generate_commentary = commentary_func + generate_chapter_overview = chapter_overview_func + generate_book_commentary = book_commentary_func + generate_word_study_sidenotes = word_study_func + + +# ============================================================================= +# Book Routes +# ============================================================================= + +@router.get("/book/{book}", response_class=HTMLResponse) +async def read_book(request: Request, book: str): + """Display a Bible book overview with chapter listing.""" + # 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 + }, + ) + + +@router.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}"} + ) + + +@router.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) + + +@router.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) + + +# ============================================================================= +# Chapter Routes +# ============================================================================= + +@router.get("/book/{book}/chapter/{chapter}", response_class=HTMLResponse) +async def read_chapter(request: Request, book: str, chapter: int): + """Display a Bible chapter 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}", 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 + 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) + + # Get related content for internal linking + related_content = get_related_content(book, chapter) + + # 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, + "related_content": related_content + } + ) + + +@router.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 + 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}"} + ) + + +# ============================================================================= +# Interlinear Routes +# ============================================================================= + +@router.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}"} + ) + + +@router.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 + 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 + } + ) + + +# ============================================================================= +# Verse Routes +# ============================================================================= + +@router.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}"} + ) + + +@router.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 + } + ) diff --git a/kjvstudy_org/templates/chapter.html b/kjvstudy_org/templates/chapter.html index 8046ae5..4be2ae1 100644 --- a/kjvstudy_org/templates/chapter.html +++ b/kjvstudy_org/templates/chapter.html @@ -732,4 +732,68 @@ document.addEventListener('DOMContentLoaded', function() { }); }); + +{% if related_content and (related_content.topics or related_content.people or related_content.resources or related_content.stories) %} +
+

Related Resources

+

+ Explore related topics, people, and study resources to deepen your understanding of this chapter. +

+ +
+ {% if related_content.topics %} +
+

Topics

+
    + {% for topic in related_content.topics %} +
  • + {{ topic.name }} +
  • + {% endfor %} +
+
+ {% endif %} + + {% if related_content.people %} +
+

People

+
    + {% for person in related_content.people %} +
  • + {{ person.name }} +
  • + {% endfor %} +
+
+ {% endif %} + + {% if related_content.resources %} +
+

Study Resources

+ +
+ {% endif %} + + {% if related_content.stories %} +
+

Bible Stories

+
    + {% for story in related_content.stories %} +
  • + {{ story.name }} +
  • + {% endfor %} +
+
+ {% endif %} +
+
+{% endif %} + {% endblock %} diff --git a/kjvstudy_org/templates/verse.html b/kjvstudy_org/templates/verse.html index b44a01b..ceceb68 100644 --- a/kjvstudy_org/templates/verse.html +++ b/kjvstudy_org/templates/verse.html @@ -984,28 +984,28 @@ {% if commentary %} {% if commentary.analysis %} -
+

Analysis

-

{{ commentary.analysis|format_lists|safe }}

+ {{ commentary.analysis|split_paragraphs|safe }}
{% endif %} {% if commentary.historical %} -
+

Historical Context

-

{{ commentary.historical|format_lists|safe }}

+ {{ commentary.historical|split_paragraphs|safe }}
{% endif %} {% if commentary.theological %} -
+

Theological Significance

-

{{ commentary.theological|format_lists|safe }}

+ {{ commentary.theological|split_paragraphs|safe }}
{% endif %} {% if commentary.questions %} -
+

Questions for Reflection

    {% for question in commentary.questions %} @@ -1020,15 +1020,24 @@ -{% if related_content and (related_content.topics or related_content.people or related_content.resources) %} +{% if related_content and (related_content.topics or related_content.people or related_content.resources or related_content.stories) %}

    Related Resources

    @@ -1256,6 +1268,19 @@

{% endif %} + + {% if related_content.stories %} +
+

Bible Stories

+
    + {% for story in related_content.stories %} +
  • + {{ story.name }} +
  • + {% endfor %} +
+
+ {% endif %}
{% endif %} diff --git a/kjvstudy_org/utils/helpers.py b/kjvstudy_org/utils/helpers.py index 7f37ca2..4e979d8 100644 --- a/kjvstudy_org/utils/helpers.py +++ b/kjvstudy_org/utils/helpers.py @@ -10,6 +10,7 @@ from functools import lru_cache from ..kjv import bible, VerseReference from ..topics import get_all_topics from ..red_letter import get_christ_words +from ..stories import get_all_stories_flat # Paths to data files _DATA_DIR = Path(__file__).parent.parent / "data" @@ -146,9 +147,37 @@ def get_related_content(book: str, chapter: int = None, verse: int = None) -> Di "study_guides": [], "topics": [], "people": [], - "resources": [] + "resources": [], + "stories": [] } + # Find related Bible stories based on verse references + all_stories = get_all_stories_flat() + for story in all_stories: + story_verses = story.get("verses", []) + for verse_ref in story_verses: + # Parse verse references like "Genesis 1:1-31" or "Genesis 2:1-3" + if verse_ref.startswith(book + " "): + # Extract chapter from reference + ref_part = verse_ref[len(book) + 1:] # e.g., "1:1-31" or "2:1-3" + if ":" in ref_part: + ref_chapter = ref_part.split(":")[0] + try: + ref_chapter_num = int(ref_chapter) + # If chapter matches (or no chapter specified), include this story + if chapter is None or ref_chapter_num == chapter: + story_entry = { + "name": story.get("title", ""), + "url": f"/stories/{story.get('slug', '')}", + "description": story.get("description", "")[:100] + "..." if len(story.get("description", "")) > 100 else story.get("description", "") + } + # Avoid duplicates + if story_entry not in related["stories"]: + related["stories"].append(story_entry) + break # Only add each story once per book/chapter match + except ValueError: + pass + # Map books to related people book_people_map = { "Genesis": [{"name": "Abraham", "url": "/family-tree/person/i60"}, {"name": "Jacob", "url": "/family-tree/person/i58"}], @@ -188,21 +217,34 @@ def get_related_content(book: str, chapter: int = None, verse: int = None) -> Di if book in ["Matthew", "Mark", "Luke", "John"]: related["resources"].append({"name": "Parables of Jesus", "url": "/parables"}) - # Add topic links based on common themes - topic_keywords = { - "Salvation": ["John", "Romans", "Ephesians", "Titus"], - "Prayer": ["Matthew", "Luke", "1 Thessalonians", "James"], - "Love": ["John", "1 Corinthians", "1 John"], - "Faith": ["Hebrews", "James", "Romans"], - "Hope": ["Romans", "1 Peter", "Hebrews"], - "Peace": ["Philippians", "John", "Romans"], - "Wisdom": ["Proverbs", "Ecclesiastes", "James"], - } - + # Add topic links based on verse references in topic data topics_data = get_all_topics() - for topic_name in topics_data.keys(): - if topic_name in topic_keywords and book in topic_keywords[topic_name]: - related["topics"].append({"name": topic_name, "url": f"/topics/{topic_name}"}) + for topic_name, topic_data in topics_data.items(): + topic_matched = False + # Check subtopics for verse references + subtopics = topic_data.get("subtopics", {}) + for subtopic_name, subtopic_data in subtopics.items(): + if topic_matched: + break + verses = subtopic_data.get("verses", []) + for verse_entry in verses: + ref = verse_entry.get("ref", "") + # Check if this reference matches our book/chapter + if ref.startswith(book + " "): + ref_part = ref[len(book) + 1:] + if ":" in ref_part: + ref_chapter = ref_part.split(":")[0] + try: + ref_chapter_num = int(ref_chapter) + if chapter is None or ref_chapter_num == chapter: + related["topics"].append({ + "name": topic_name, + "url": f"/topics/{topic_name}" + }) + topic_matched = True + break + except ValueError: + pass return related