feat: implement SEO-friendly URLs for Bible reader
- Add dynamic route structure /[locale]/bible/[version]/[book]/[chapter] - Convert UUID-based URLs to readable format (e.g., /en/bible/eng-kjv/genesis/1) - Implement automatic redirects from old URLs to new SEO-friendly format - Add SEO metadata generation with proper titles, descriptions, and OpenGraph tags - Create API endpoint for URL conversion between formats - Update navigation in search results, bookmarks, and internal links - Fix PWA manifest icons to correct dimensions (192x192, 512x512) - Resolve JavaScript parameter passing issues between server and client components - Maintain backward compatibility with existing bookmark and search functionality Benefits: - Improved SEO with descriptive URLs - Better user experience with readable URLs - Enhanced social media sharing - Maintained full backward compatibility
This commit is contained in:
134
app/[locale]/bible/[version]/[book]/[chapter]/page.tsx
Normal file
134
app/[locale]/bible/[version]/[book]/[chapter]/page.tsx
Normal file
@@ -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 (
|
||||||
|
<Suspense fallback={<div>Loading...</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
|
||||||
@@ -1,7 +1,54 @@
|
|||||||
import { Suspense } from 'react'
|
import { Suspense } from 'react'
|
||||||
|
import { redirect } from 'next/navigation'
|
||||||
import BibleReader from './reader'
|
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 (
|
return (
|
||||||
<Suspense fallback={<div>Loading...</div>}>
|
<Suspense fallback={<div>Loading...</div>}>
|
||||||
<BibleReader />
|
<BibleReader />
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
import React, { useState, useEffect, useRef, useCallback } from 'react'
|
||||||
import { useTranslations, useLocale } from 'next-intl'
|
import { useTranslations, useLocale } from 'next-intl'
|
||||||
import { useAuth } from '@/hooks/use-auth'
|
import { useAuth } from '@/hooks/use-auth'
|
||||||
import { useSearchParams, useRouter } from 'next/navigation'
|
import { useSearchParams, useRouter } from 'next/navigation'
|
||||||
@@ -128,7 +128,13 @@ const defaultPreferences: ReadingPreferences = {
|
|||||||
readingMode: false
|
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 theme = useTheme()
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'))
|
const isMobile = useMediaQuery(theme.breakpoints.down('md'))
|
||||||
const t = useTranslations('pages.bible')
|
const t = useTranslations('pages.bible')
|
||||||
@@ -137,6 +143,31 @@ export default function BibleReaderNew() {
|
|||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const { user } = useAuth()
|
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
|
// Core state
|
||||||
const [books, setBooks] = useState<BibleBook[]>([])
|
const [books, setBooks] = useState<BibleBook[]>([])
|
||||||
const [versions, setVersions] = useState<BibleVersion[]>([])
|
const [versions, setVersions] = useState<BibleVersion[]>([])
|
||||||
@@ -313,10 +344,10 @@ export default function BibleReaderNew() {
|
|||||||
|
|
||||||
// Handle URL parameters for bookmark navigation
|
// Handle URL parameters for bookmark navigation
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const urlVersion = searchParams.get('version')
|
const urlVersion = effectiveParams.get('version')
|
||||||
const urlBook = searchParams.get('book')
|
const urlBook = effectiveParams.get('book')
|
||||||
const urlChapter = searchParams.get('chapter')
|
const urlChapter = effectiveParams.get('chapter')
|
||||||
const urlVerse = searchParams.get('verse')
|
const urlVerse = effectiveParams.get('verse')
|
||||||
|
|
||||||
if (urlVersion && versions.length > 0) {
|
if (urlVersion && versions.length > 0) {
|
||||||
// Check if this version exists
|
// Check if this version exists
|
||||||
@@ -353,25 +384,46 @@ export default function BibleReaderNew() {
|
|||||||
}, 500)
|
}, 500)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [searchParams, versions, books, verses, selectedVersion, selectedBook, selectedChapter])
|
}, [effectiveParams, versions, books, verses, selectedVersion, selectedBook, selectedChapter])
|
||||||
|
|
||||||
// Function to update URL without causing full page reload
|
// Function to update URL without causing full page reload
|
||||||
const updateUrl = useCallback((bookId?: string, chapter?: number, versionId?: string) => {
|
const updateUrl = useCallback(async (bookId?: string, chapter?: number, versionId?: string) => {
|
||||||
const params = new URLSearchParams()
|
const targetVersionId = versionId || selectedVersion
|
||||||
|
const targetBookId = bookId || selectedBook
|
||||||
|
const targetChapter = chapter || selectedChapter
|
||||||
|
|
||||||
if (versionId || selectedVersion) {
|
// Try to generate SEO-friendly URL
|
||||||
params.set('version', versionId || selectedVersion)
|
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
|
||||||
}
|
}
|
||||||
if (bookId || selectedBook) {
|
} catch (error) {
|
||||||
params.set('book', bookId || selectedBook)
|
console.error('Error generating SEO-friendly URL:', error)
|
||||||
}
|
}
|
||||||
if (chapter || selectedChapter) {
|
|
||||||
params.set('chapter', String(chapter || selectedChapter))
|
// Fallback to query parameter URL
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (targetVersionId) {
|
||||||
|
params.set('version', targetVersionId)
|
||||||
|
}
|
||||||
|
if (targetBookId) {
|
||||||
|
params.set('book', targetBookId)
|
||||||
|
}
|
||||||
|
if (targetChapter) {
|
||||||
|
params.set('chapter', String(targetChapter))
|
||||||
}
|
}
|
||||||
|
|
||||||
const newUrl = `/${locale}/bible?${params.toString()}`
|
const newUrl = `/${locale}/bible?${params.toString()}`
|
||||||
router.replace(newUrl, { scroll: false })
|
router.replace(newUrl, { scroll: false })
|
||||||
}, [locale, selectedVersion, selectedBook, selectedChapter, router])
|
}, [locale, selectedVersion, selectedBook, selectedChapter, router, versions, books])
|
||||||
|
|
||||||
// Fetch books when version changes
|
// Fetch books when version changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -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({
|
const params = new URLSearchParams({
|
||||||
book: bookmark.navigation.bookId,
|
book: bookmark.navigation.bookId,
|
||||||
chapter: bookmark.navigation.chapterNum.toString(),
|
chapter: bookmark.navigation.chapterNum.toString(),
|
||||||
|
|||||||
@@ -409,7 +409,9 @@ export default function SearchContent() {
|
|||||||
navigator.clipboard.writeText(text)
|
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
|
const bookIdentifier = result.bookId || result.bookKey || result.book
|
||||||
router.push(`/${locale}/bible?book=${bookIdentifier}&chapter=${result.chapter}&verse=${result.verse}`)
|
router.push(`/${locale}/bible?book=${bookIdentifier}&chapter=${result.chapter}&verse=${result.verse}`)
|
||||||
}, [router, locale])
|
}, [router, locale])
|
||||||
|
|||||||
150
app/api/bible/seo-url/route.ts
Normal file
150
app/api/bible/seo-url/route.ts
Normal file
@@ -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 })
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
public/icon-192-old.png
Normal file
BIN
public/icon-192-old.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
BIN
public/icon-512-old.png
Normal file
BIN
public/icon-512-old.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 185 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 185 KiB After Width: | Height: | Size: 187 KiB |
Reference in New Issue
Block a user