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