diff --git a/app/[locale]/bible/[version]/[book]/[chapter]/page.tsx b/app/[locale]/bible/[version]/[book]/[chapter]/page.tsx
new file mode 100644
index 0000000..0f90376
--- /dev/null
+++ b/app/[locale]/bible/[version]/[book]/[chapter]/page.tsx
@@ -0,0 +1,134 @@
+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 {
+ // Find version by abbreviation (slug)
+ const version = await prisma.bibleVersion.findFirst({
+ where: {
+ abbreviation: {
+ equals: versionSlug,
+ mode: 'insensitive'
+ }
+ }
+ })
+
+ if (!version) {
+ return null
+ }
+
+ // 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) {
+ return null
+ }
+
+ // Validate chapter number
+ const chapter = parseInt(chapterNum)
+ if (isNaN(chapter) || chapter < 1) {
+ return null
+ }
+
+ // Check if chapter exists
+ const chapterRecord = await prisma.bibleChapter.findFirst({
+ where: {
+ bookId: book.id,
+ chapterNum: chapter
+ }
+ })
+
+ if (!chapterRecord) {
+ return null
+ }
+
+ return {
+ versionId: version.id,
+ bookId: book.id,
+ chapter: chapter,
+ version: version,
+ book: book
+ }
+ } catch (error) {
+ console.error('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 (
+ Loading...}>
+
+
+ )
+}
+
+// Note: generateStaticParams removed for now to avoid complexity
+// Can be added later for better performance with popular Bible chapters
diff --git a/app/[locale]/bible/page.tsx b/app/[locale]/bible/page.tsx
index 6fcfa44..35ccd94 100644
--- a/app/[locale]/bible/page.tsx
+++ b/app/[locale]/bible/page.tsx
@@ -1,7 +1,54 @@
import { Suspense } from 'react'
+import { redirect } from 'next/navigation'
import BibleReader from './reader'
+import { prisma } from '@/lib/db'
+
+interface PageProps {
+ searchParams: Promise<{
+ version?: string
+ book?: string
+ chapter?: string
+ verse?: string
+ }>
+ params: Promise<{
+ locale: string
+ }>
+}
+
+// Helper function to convert UUIDs to SEO-friendly slugs
+async function convertToSeoUrl(versionId: string, bookId: string, chapter: string, locale: string) {
+ try {
+ const version = await prisma.bibleVersion.findUnique({
+ where: { id: versionId }
+ })
+
+ const book = await prisma.bibleBook.findUnique({
+ where: { id: bookId }
+ })
+
+ if (version && book) {
+ const versionSlug = version.abbreviation.toLowerCase()
+ const bookSlug = book.bookKey.toLowerCase()
+ return `/${locale}/bible/${versionSlug}/${bookSlug}/${chapter}`
+ }
+ } catch (error) {
+ console.error('Error converting to SEO URL:', error)
+ }
+ return null
+}
+
+export default async function BiblePage({ searchParams, params }: PageProps) {
+ const { version, book, chapter } = await searchParams
+ const { locale } = await params
+
+ // If we have the old URL format with UUIDs, redirect to SEO-friendly URL
+ if (version && book && chapter) {
+ const seoUrl = await convertToSeoUrl(version, book, chapter, locale)
+ if (seoUrl) {
+ redirect(seoUrl)
+ }
+ }
-export default function BiblePage() {
return (
Loading...}>
diff --git a/app/[locale]/bible/reader.tsx b/app/[locale]/bible/reader.tsx
index a837bf5..46bfb5d 100644
--- a/app/[locale]/bible/reader.tsx
+++ b/app/[locale]/bible/reader.tsx
@@ -1,6 +1,6 @@
'use client'
-import { useState, useEffect, useRef, useCallback } from 'react'
+import React, { useState, useEffect, useRef, useCallback } from 'react'
import { useTranslations, useLocale } from 'next-intl'
import { useAuth } from '@/hooks/use-auth'
import { useSearchParams, useRouter } from 'next/navigation'
@@ -128,7 +128,13 @@ const defaultPreferences: ReadingPreferences = {
readingMode: false
}
-export default function BibleReaderNew() {
+interface BibleReaderProps {
+ initialVersion?: string
+ initialBook?: string
+ initialChapter?: string
+}
+
+export default function BibleReaderNew({ initialVersion, initialBook, initialChapter }: BibleReaderProps = {}) {
const theme = useTheme()
const isMobile = useMediaQuery(theme.breakpoints.down('md'))
const t = useTranslations('pages.bible')
@@ -136,6 +142,31 @@ export default function BibleReaderNew() {
const router = useRouter()
const searchParams = useSearchParams()
const { user } = useAuth()
+
+ // Use initial props if provided, otherwise use search params
+ const effectiveParams = React.useMemo(() => {
+ if (initialVersion || initialBook || initialChapter) {
+ // Create a params-like object from the initial props
+ return {
+ get: (key: string) => {
+ if (key === 'version') return initialVersion || null
+ if (key === 'book') return initialBook || null
+ if (key === 'chapter') return initialChapter || null
+ if (key === 'verse') return searchParams.get('verse') // Still get verse from URL query params
+ return null
+ },
+ has: (key: string) => {
+ if (key === 'version') return !!initialVersion
+ if (key === 'book') return !!initialBook
+ if (key === 'chapter') return !!initialChapter
+ if (key === 'verse') return searchParams.has('verse')
+ return false
+ },
+ toString: (): string => ''
+ }
+ }
+ return searchParams
+ }, [initialVersion, initialBook, initialChapter, searchParams])
// Core state
const [books, setBooks] = useState([])
@@ -313,10 +344,10 @@ export default function BibleReaderNew() {
// Handle URL parameters for bookmark navigation
useEffect(() => {
- const urlVersion = searchParams.get('version')
- const urlBook = searchParams.get('book')
- const urlChapter = searchParams.get('chapter')
- const urlVerse = searchParams.get('verse')
+ const urlVersion = effectiveParams.get('version')
+ const urlBook = effectiveParams.get('book')
+ const urlChapter = effectiveParams.get('chapter')
+ const urlVerse = effectiveParams.get('verse')
if (urlVersion && versions.length > 0) {
// Check if this version exists
@@ -353,25 +384,46 @@ export default function BibleReaderNew() {
}, 500)
}
}
- }, [searchParams, versions, books, verses, selectedVersion, selectedBook, selectedChapter])
+ }, [effectiveParams, versions, books, verses, selectedVersion, selectedBook, selectedChapter])
// Function to update URL without causing full page reload
- const updateUrl = useCallback((bookId?: string, chapter?: number, versionId?: string) => {
- const params = new URLSearchParams()
+ const updateUrl = useCallback(async (bookId?: string, chapter?: number, versionId?: string) => {
+ const targetVersionId = versionId || selectedVersion
+ const targetBookId = bookId || selectedBook
+ const targetChapter = chapter || selectedChapter
- if (versionId || selectedVersion) {
- params.set('version', versionId || selectedVersion)
+ // Try to generate SEO-friendly URL
+ try {
+ const version = versions.find(v => v.id === targetVersionId)
+ const book = books.find(b => b.id === targetBookId)
+
+ if (version && book && targetChapter) {
+ // Generate SEO-friendly URL
+ const versionSlug = version.abbreviation.toLowerCase()
+ const bookSlug = book.bookKey.toLowerCase()
+ const newUrl = `/${locale}/bible/${versionSlug}/${bookSlug}/${targetChapter}`
+ router.replace(newUrl, { scroll: false })
+ return
+ }
+ } catch (error) {
+ console.error('Error generating SEO-friendly URL:', error)
}
- if (bookId || selectedBook) {
- params.set('book', bookId || selectedBook)
+
+ // Fallback to query parameter URL
+ const params = new URLSearchParams()
+ if (targetVersionId) {
+ params.set('version', targetVersionId)
}
- if (chapter || selectedChapter) {
- params.set('chapter', String(chapter || selectedChapter))
+ if (targetBookId) {
+ params.set('book', targetBookId)
+ }
+ if (targetChapter) {
+ params.set('chapter', String(targetChapter))
}
const newUrl = `/${locale}/bible?${params.toString()}`
router.replace(newUrl, { scroll: false })
- }, [locale, selectedVersion, selectedBook, selectedChapter, router])
+ }, [locale, selectedVersion, selectedBook, selectedChapter, router, versions, books])
// Fetch books when version changes
useEffect(() => {
diff --git a/app/[locale]/bookmarks/page.tsx b/app/[locale]/bookmarks/page.tsx
index 69a53a5..b0a1451 100644
--- a/app/[locale]/bookmarks/page.tsx
+++ b/app/[locale]/bookmarks/page.tsx
@@ -116,7 +116,38 @@ export default function BookmarksPage() {
}
}
- const handleNavigateToBookmark = (bookmark: BookmarkItem) => {
+ const handleNavigateToBookmark = async (bookmark: BookmarkItem) => {
+ try {
+ // Try to generate SEO-friendly URL
+ const response = await fetch('/api/bible/seo-url', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ versionId: bookmark.navigation.versionId,
+ bookId: bookmark.navigation.bookId,
+ chapter: bookmark.navigation.chapterNum.toString(),
+ locale
+ })
+ })
+
+ if (response.ok) {
+ const data = await response.json()
+ if (data.success && data.seoUrl) {
+ let url = data.seoUrl
+ if (bookmark.navigation.verseNum) {
+ url += `?verse=${bookmark.navigation.verseNum}`
+ }
+ router.push(url)
+ return
+ }
+ }
+ } catch (error) {
+ console.error('Error generating SEO URL:', error)
+ }
+
+ // Fallback to old URL format
const params = new URLSearchParams({
book: bookmark.navigation.bookId,
chapter: bookmark.navigation.chapterNum.toString(),
diff --git a/app/[locale]/search/search-content.tsx b/app/[locale]/search/search-content.tsx
index 6a43129..d76d884 100644
--- a/app/[locale]/search/search-content.tsx
+++ b/app/[locale]/search/search-content.tsx
@@ -409,7 +409,9 @@ export default function SearchContent() {
navigator.clipboard.writeText(text)
}, [])
- const handleNavigateToVerse = useCallback((result: SearchResult) => {
+ const handleNavigateToVerse = useCallback(async (result: SearchResult) => {
+ // For now, use the fallback URL format since we don't have version info in SearchResult
+ // TODO: Enhance SearchResult to include version information for SEO-friendly URLs
const bookIdentifier = result.bookId || result.bookKey || result.book
router.push(`/${locale}/bible?book=${bookIdentifier}&chapter=${result.chapter}&verse=${result.verse}`)
}, [router, locale])
diff --git a/app/api/bible/seo-url/route.ts b/app/api/bible/seo-url/route.ts
new file mode 100644
index 0000000..bdbe56b
--- /dev/null
+++ b/app/api/bible/seo-url/route.ts
@@ -0,0 +1,150 @@
+import { NextResponse } from 'next/server'
+import { prisma } from '@/lib/db'
+
+export const runtime = 'nodejs'
+
+// Convert UUIDs to SEO-friendly slugs
+export async function POST(request: Request) {
+ try {
+ const { versionId, bookId, chapter, locale } = await request.json()
+
+ if (!versionId || !bookId || !chapter) {
+ return NextResponse.json({
+ success: false,
+ error: 'Missing required parameters'
+ }, { status: 400 })
+ }
+
+ const version = await prisma.bibleVersion.findUnique({
+ where: { id: versionId }
+ })
+
+ const book = await prisma.bibleBook.findUnique({
+ where: { id: bookId }
+ })
+
+ if (!version || !book) {
+ return NextResponse.json({
+ success: false,
+ error: 'Version or book not found'
+ }, { status: 404 })
+ }
+
+ const versionSlug = version.abbreviation.toLowerCase()
+ const bookSlug = book.bookKey.toLowerCase()
+ const seoUrl = `/${locale}/bible/${versionSlug}/${bookSlug}/${chapter}`
+
+ return NextResponse.json({
+ success: true,
+ seoUrl,
+ version: {
+ id: version.id,
+ name: version.name,
+ abbreviation: version.abbreviation,
+ slug: versionSlug
+ },
+ book: {
+ id: book.id,
+ name: book.name,
+ bookKey: book.bookKey,
+ slug: bookSlug
+ }
+ })
+ } catch (error) {
+ console.error('Error generating SEO URL:', error)
+ return NextResponse.json({
+ success: false,
+ error: 'Internal server error'
+ }, { status: 500 })
+ }
+}
+
+// Convert SEO-friendly slugs to UUIDs
+export async function GET(request: Request) {
+ try {
+ const { searchParams } = new URL(request.url)
+ const versionSlug = searchParams.get('version')
+ const bookSlug = searchParams.get('book')
+ const chapter = searchParams.get('chapter')
+
+ if (!versionSlug || !bookSlug || !chapter) {
+ return NextResponse.json({
+ success: false,
+ error: 'Missing required parameters'
+ }, { status: 400 })
+ }
+
+ const version = await prisma.bibleVersion.findFirst({
+ where: {
+ abbreviation: {
+ equals: versionSlug,
+ mode: 'insensitive'
+ }
+ }
+ })
+
+ if (!version) {
+ return NextResponse.json({
+ success: false,
+ error: 'Version not found'
+ }, { status: 404 })
+ }
+
+ const book = await prisma.bibleBook.findFirst({
+ where: {
+ versionId: version.id,
+ bookKey: {
+ equals: bookSlug,
+ mode: 'insensitive'
+ }
+ }
+ })
+
+ if (!book) {
+ return NextResponse.json({
+ success: false,
+ error: 'Book not found'
+ }, { status: 404 })
+ }
+
+ // Validate chapter exists
+ const chapterNum = parseInt(chapter)
+ const chapterRecord = await prisma.bibleChapter.findFirst({
+ where: {
+ bookId: book.id,
+ chapterNum: chapterNum
+ }
+ })
+
+ if (!chapterRecord) {
+ return NextResponse.json({
+ success: false,
+ error: 'Chapter not found'
+ }, { status: 404 })
+ }
+
+ return NextResponse.json({
+ success: true,
+ versionId: version.id,
+ bookId: book.id,
+ chapter: chapterNum,
+ version: {
+ id: version.id,
+ name: version.name,
+ abbreviation: version.abbreviation
+ },
+ book: {
+ id: book.id,
+ name: book.name,
+ bookKey: book.bookKey,
+ testament: book.testament
+ }
+ })
+ } catch (error) {
+ console.error('Error resolving SEO URL:', error)
+ return NextResponse.json({
+ success: false,
+ error: 'Internal server error'
+ }, { status: 500 })
+ }
+}
diff --git a/public/icon-192-old.png b/public/icon-192-old.png
new file mode 100644
index 0000000..7aa2574
Binary files /dev/null and b/public/icon-192-old.png differ
diff --git a/public/icon-192.png b/public/icon-192.png
index 7aa2574..9b8f9a7 100644
Binary files a/public/icon-192.png and b/public/icon-192.png differ
diff --git a/public/icon-512-old.png b/public/icon-512-old.png
new file mode 100644
index 0000000..cc5e9e2
Binary files /dev/null and b/public/icon-512-old.png differ
diff --git a/public/icon-512.png b/public/icon-512.png
index cc5e9e2..4c46057 100644
Binary files a/public/icon-512.png and b/public/icon-512.png differ