Database & API: - Enhanced ReadingHistory model with versionId field and unique constraint per user/version - Created /api/user/reading-progress endpoint (GET/POST) for saving and retrieving progress - Upsert operation ensures one reading position per user per Bible version Bible Reader Features: - Auto-save reading position after 2 seconds of inactivity - Auto-restore last reading position on page load (respects URL parameters) - Visual progress bar showing completion percentage based on chapters read - Calculate progress across entire Bible (current chapter / total chapters) - Client-side only loading to prevent hydration mismatches Bug Fixes: - Remove 200 version limit when loading "all versions" - now loads ALL versions - Fix version selection resetting to favorite when user manually selects different version - Transform books API response to include chaptersCount property - Update service worker cache version to force client updates - Add comprehensive SEO URL logging for debugging 404 issues 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
163 lines
4.4 KiB
TypeScript
163 lines
4.4 KiB
TypeScript
import { Suspense } from 'react'
|
|
import { notFound } from 'next/navigation'
|
|
import BibleReader from '../../../reader'
|
|
import { prisma } from '@/lib/db'
|
|
|
|
interface PageProps {
|
|
params: Promise<{
|
|
locale: string
|
|
version: string
|
|
book: string
|
|
chapter: string
|
|
}>
|
|
}
|
|
|
|
// Helper function to convert readable names to IDs
|
|
async function getResourceIds(versionSlug: string, bookSlug: string, chapterNum: string) {
|
|
try {
|
|
console.log('[SEO URL] Looking for version:', versionSlug, 'book:', bookSlug, 'chapter:', chapterNum)
|
|
|
|
// Find version by abbreviation (slug)
|
|
const version = await prisma.bibleVersion.findFirst({
|
|
where: {
|
|
abbreviation: {
|
|
equals: versionSlug,
|
|
mode: 'insensitive'
|
|
}
|
|
}
|
|
})
|
|
|
|
if (!version) {
|
|
console.log('[SEO URL] Version not found:', versionSlug)
|
|
return null
|
|
}
|
|
|
|
console.log('[SEO URL] Found version:', version.abbreviation, version.id)
|
|
|
|
// Find book by bookKey (slug) within this version
|
|
const book = await prisma.bibleBook.findFirst({
|
|
where: {
|
|
versionId: version.id,
|
|
bookKey: {
|
|
equals: bookSlug,
|
|
mode: 'insensitive'
|
|
}
|
|
}
|
|
})
|
|
|
|
if (!book) {
|
|
console.log('[SEO URL] Book not found for bookKey:', bookSlug, 'in version:', version.abbreviation)
|
|
// List available books for debugging
|
|
const availableBooks = await prisma.bibleBook.findMany({
|
|
where: { versionId: version.id },
|
|
select: { bookKey: true, name: true },
|
|
take: 5
|
|
})
|
|
console.log('[SEO URL] Sample available books:', availableBooks)
|
|
return null
|
|
}
|
|
|
|
console.log('[SEO URL] Found book:', book.name, book.id)
|
|
|
|
// Validate chapter number
|
|
const chapter = parseInt(chapterNum)
|
|
if (isNaN(chapter) || chapter < 1) {
|
|
console.log('[SEO URL] Invalid chapter number:', chapterNum)
|
|
return null
|
|
}
|
|
|
|
// Check if chapter exists
|
|
const chapterRecord = await prisma.bibleChapter.findFirst({
|
|
where: {
|
|
bookId: book.id,
|
|
chapterNum: chapter
|
|
}
|
|
})
|
|
|
|
if (!chapterRecord) {
|
|
console.log('[SEO URL] Chapter not found:', chapter, 'in book:', book.name)
|
|
return null
|
|
}
|
|
|
|
console.log('[SEO URL] Successfully resolved:', version.abbreviation, book.name, chapter)
|
|
|
|
return {
|
|
versionId: version.id,
|
|
bookId: book.id,
|
|
chapter: chapter,
|
|
version: version,
|
|
book: book
|
|
}
|
|
} catch (error) {
|
|
console.error('[SEO URL] Error resolving resource IDs:', error)
|
|
return null
|
|
}
|
|
}
|
|
|
|
// Generate metadata for SEO
|
|
export async function generateMetadata({ params }: PageProps) {
|
|
const { version, book, chapter } = await params
|
|
const resources = await getResourceIds(version, book, chapter)
|
|
|
|
if (!resources) {
|
|
return {
|
|
title: 'Bible Chapter Not Found',
|
|
description: 'The requested Bible chapter could not be found.'
|
|
}
|
|
}
|
|
|
|
const title = `${resources.book.name} ${chapter} - ${resources.version.name} | Biblical Guide`
|
|
const description = `Read ${resources.book.name} chapter ${chapter} in the ${resources.version.name} translation. Free online Bible study with AI chat and prayer community.`
|
|
|
|
return {
|
|
title,
|
|
description,
|
|
openGraph: {
|
|
title,
|
|
description,
|
|
type: 'article',
|
|
url: `https://biblical-guide.com/${(await params).locale}/bible/${version}/${book}/${chapter}`,
|
|
},
|
|
twitter: {
|
|
card: 'summary',
|
|
title,
|
|
description,
|
|
},
|
|
alternates: {
|
|
canonical: `https://biblical-guide.com/${(await params).locale}/bible/${version}/${book}/${chapter}`,
|
|
}
|
|
}
|
|
}
|
|
|
|
export default async function BibleChapterPage({ params }: PageProps) {
|
|
const { version, book, chapter } = await params
|
|
const resources = await getResourceIds(version, book, chapter)
|
|
|
|
if (!resources) {
|
|
notFound()
|
|
}
|
|
|
|
// Pass the parameters as props instead of URLSearchParams
|
|
return (
|
|
<Suspense fallback={
|
|
<div style={{
|
|
display: 'flex',
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
minHeight: '200px'
|
|
}}>
|
|
Loading Bible reader...
|
|
</div>
|
|
}>
|
|
<BibleReader
|
|
initialVersion={resources.versionId}
|
|
initialBook={resources.bookId}
|
|
initialChapter={chapter}
|
|
/>
|
|
</Suspense>
|
|
)
|
|
}
|
|
|
|
// Note: generateStaticParams removed for now to avoid complexity
|
|
// Can be added later for better performance with popular Bible chapters
|