Add Related Resources section to chapter pages with stories and topics

- 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 <noreply@anthropic.com>
This commit is contained in:
2025-12-02 18:08:47 -05:00
parent e4ef72330a
commit 8ca561d591
4 changed files with 806 additions and 32 deletions
+643
View File
@@ -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
}
)
+64
View File
@@ -732,4 +732,68 @@ document.addEventListener('DOMContentLoaded', function() {
});
});
</script>
{% if related_content and (related_content.topics or related_content.people or related_content.resources or related_content.stories) %}
<div style="border-top: 1px solid var(--border-color); padding-top: 2rem; margin-top: 3rem;">
<h2>Related Resources</h2>
<p style="font-size: 0.95rem; color: var(--text-secondary); margin-bottom: 1.5rem;">
Explore related topics, people, and study resources to deepen your understanding of this chapter.
</p>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1.5rem;">
{% if related_content.topics %}
<div>
<h3 style="font-size: 1rem; margin-bottom: 0.75rem; color: var(--text-secondary);">Topics</h3>
<ul style="list-style: none; padding: 0; margin: 0;">
{% for topic in related_content.topics %}
<li style="margin-bottom: 0.5rem;">
<a href="{{ topic.url }}" style="font-size: 0.95rem;">{{ topic.name }}</a>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if related_content.people %}
<div>
<h3 style="font-size: 1rem; margin-bottom: 0.75rem; color: var(--text-secondary);">People</h3>
<ul style="list-style: none; padding: 0; margin: 0;">
{% for person in related_content.people %}
<li style="margin-bottom: 0.5rem;">
<a href="{{ person.url }}" style="font-size: 0.95rem;">{{ person.name }}</a>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if related_content.resources %}
<div>
<h3 style="font-size: 1rem; margin-bottom: 0.75rem; color: var(--text-secondary);">Study Resources</h3>
<ul style="list-style: none; padding: 0; margin: 0;">
{% for resource in related_content.resources %}
<li style="margin-bottom: 0.5rem;">
<a href="{{ resource.url }}" style="font-size: 0.95rem;">{{ resource.name }}</a>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if related_content.stories %}
<div>
<h3 style="font-size: 1rem; margin-bottom: 0.75rem; color: var(--text-secondary);">Bible Stories</h3>
<ul style="list-style: none; padding: 0; margin: 0;">
{% for story in related_content.stories %}
<li style="margin-bottom: 0.5rem;">
<a href="{{ story.url }}" style="font-size: 0.95rem;">{{ story.name }}</a>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
</div>
{% endif %}
{% endblock %}
+42 -17
View File
@@ -984,28 +984,28 @@
{% if commentary %}
{% if commentary.analysis %}
<div>
<div class="commentary-section">
<h2>Analysis</h2>
<p>{{ commentary.analysis|format_lists|safe }}</p>
{{ commentary.analysis|split_paragraphs|safe }}
</div>
{% endif %}
{% if commentary.historical %}
<div>
<div class="commentary-section">
<h2>Historical Context</h2>
<p>{{ commentary.historical|format_lists|safe }}</p>
{{ commentary.historical|split_paragraphs|safe }}
</div>
{% endif %}
{% if commentary.theological %}
<div>
<div class="commentary-section">
<h2>Theological Significance</h2>
<p>{{ commentary.theological|format_lists|safe }}</p>
{{ commentary.theological|split_paragraphs|safe }}
</div>
{% endif %}
{% if commentary.questions %}
<div>
<div class="commentary-section">
<h2>Questions for Reflection</h2>
<ul>
{% for question in commentary.questions %}
@@ -1020,15 +1020,24 @@
<script>
(function() {
// Collect all readable elements: paragraphs, list items, cross-ref items
var elements = Array.from(document.querySelectorAll(
'section > p, ' +
'.cross-references-section li, ' +
'div > p, ' +
'.interlinear-container'
)).filter(function(el) {
// Filter out empty or very short elements
return el.textContent.trim().length > 10 || el.classList.contains('interlinear-container');
});
function getElements() {
return Array.from(document.querySelectorAll(
'section > p.verse-text, ' +
'.interlinear-container, ' +
'#crossrefs-content > p, ' +
'.cross-references-section li, ' +
'.commentary-section p, ' +
'.commentary-section li'
)).filter(function(el) {
// Filter out empty or very short elements, and UI elements
if (el.closest('.word-detail') || el.closest('.share-container') || el.closest('nav')) return false;
// Skip elements inside collapsed sections
var hiddenParent = el.closest('[hidden]');
if (hiddenParent) return false;
return el.textContent.trim().length > 10 || el.classList.contains('interlinear-container');
});
}
var elements = getElements();
var selectedIndex = -1;
var inWordMode = false;
@@ -1064,6 +1073,9 @@
function selectElement(index) {
clearSelection();
// Refresh elements list in case sections were toggled
elements = getElements();
if (elements.length === 0) return;
selectedIndex = Math.max(0, Math.min(index, elements.length - 1));
elements[selectedIndex].style.outline = '2px solid #4a7c59';
elements[selectedIndex].style.outlineOffset = '4px';
@@ -1210,7 +1222,7 @@
})();
</script>
{% 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) %}
<div style="border-top: 1px solid var(--border-color); padding-top: 2rem; margin-top: 3rem;">
<h2>Related Resources</h2>
<p style="font-size: 0.95rem; color: var(--text-secondary); margin-bottom: 1.5rem;">
@@ -1256,6 +1268,19 @@
</ul>
</div>
{% endif %}
{% if related_content.stories %}
<div>
<h3 style="font-size: 1rem; margin-bottom: 0.75rem; color: var(--text-secondary);">Bible Stories</h3>
<ul style="list-style: none; padding: 0; margin: 0;">
{% for story in related_content.stories %}
<li style="margin-bottom: 0.5rem;">
<a href="{{ story.url }}" style="font-size: 0.95rem;">{{ story.name }}</a>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
</div>
{% endif %}
+57 -15
View File
@@ -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