Browse Source

add og data to other websites cards

imwald
Silberengel 3 months ago
parent
commit
b717385567
  1. 56
      src/components/Bookstr/BookstrContent.tsx
  2. 50
      src/services/web.service.ts

56
src/components/Bookstr/BookstrContent.tsx

@ -15,6 +15,7 @@ import {
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import logger from '@/lib/logger' import logger from '@/lib/logger'
import { contentParserService } from '@/services/content-parser.service' import { contentParserService } from '@/services/content-parser.service'
import WebPreview from '@/components/WebPreview'
interface BookstrContentProps { interface BookstrContentProps {
wikilink: string wikilink: string
@ -942,41 +943,30 @@ export function BookstrContent({ wikilink, className }: BookstrContentProps) {
}} }}
/> />
</div> </div>
{(() => {
// Get bookType from parsed wikilink (defaults to 'bible')
const bookType = parsed?.bookType || 'bible'
// Only show external link for bible, torah, or quran collections
// Other collections (secular books) don't have external links
if (!['bible', 'torah', 'quran'].includes(bookType)) {
return null
}
const externalUrl = buildExternalUrl(section.reference, bookType, selectedVersion)
const serviceName = bookType === 'torah' ? 'Sefaria' : bookType === 'quran' ? 'quran.com' : 'Bible Gateway'
if (!externalUrl) return null
return (
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0 shrink-0"
asChild
>
<a
href={externalUrl}
target="_blank"
rel="noopener noreferrer"
title={`View on ${serviceName}`}
>
<ExternalLink className="h-3 w-3" />
</a>
</Button>
)
})()}
</div> </div>
{/* OG Preview Card for bible/torah/quran external URLs */}
{(() => {
// Get bookType from parsed wikilink (defaults to 'bible')
const bookType = parsed?.bookType || 'bible'
// Only show external link for bible, torah, or quran collections
// Other collections (secular books) don't have external links
if (!['bible', 'torah', 'quran'].includes(bookType)) {
return null
}
const externalUrl = buildExternalUrl(section.reference, bookType, selectedVersion)
if (!externalUrl) return null
return (
<div className="mb-3">
<WebPreview url={externalUrl} className="w-full" />
</div>
)
})()}
{/* Verses - render all verses together, including ranges */} {/* Verses - render all verses together, including ranges */}
{filteredEvents.length > 0 && ( {filteredEvents.length > 0 && (
<VerseContent <VerseContent

50
src/services/web.service.ts

@ -9,22 +9,27 @@ class WebService {
async (urls) => { async (urls) => {
return await Promise.all( return await Promise.all(
urls.map(async (url) => { urls.map(async (url) => {
logger.info('[WebService] Starting OG metadata fetch', { url, proxyServer: import.meta.env.VITE_PROXY_SERVER })
// Check if we should use proxy server to avoid CORS issues // Check if we should use proxy server to avoid CORS issues
// Uses the same proxy as wikistr (configured via VITE_PROXY_SERVER build arg) // Uses the same proxy as wikistr (configured via VITE_PROXY_SERVER build arg)
// Since jumble and wikistr run on the same server, they share the same proxy endpoint // Since jumble and wikistr run on the same server, they share the same proxy endpoint
// Default to relative path /sites/ if VITE_PROXY_SERVER is not set (like wikistr does)
const proxyServer = import.meta.env.VITE_PROXY_SERVER const proxyServer = import.meta.env.VITE_PROXY_SERVER
const proxyBase = proxyServer?.trim() || '/sites/'
const isProxyUrl = url.includes('/sites/') || url.includes('/sites/?url=') const isProxyUrl = url.includes('/sites/') || url.includes('/sites/?url=')
// If proxy is configured and URL isn't already proxied, use proxy // Build proxy URL - handle both full URLs and relative paths
// The proxy server expects the URL as a query parameter: /sites/?url=https://example.com
let fetchUrl = url let fetchUrl = url
if (proxyServer && !isProxyUrl) { if (!isProxyUrl) {
fetchUrl = `${proxyServer}/sites/?url=${encodeURIComponent(url)}` if (proxyBase.startsWith('http://') || proxyBase.startsWith('https://')) {
logger.info('[WebService] Using proxy for OG fetch', { originalUrl: url, proxyUrl: fetchUrl }) // Full URL - ensure it ends with / for query param usage
} else if (!proxyServer) { const proxyUrl = proxyBase.endsWith('/') ? proxyBase : `${proxyBase}/`
logger.warn('[WebService] No proxy server configured - VITE_PROXY_SERVER is undefined! Attempting direct fetch (will likely fail due to CORS)', { url }) fetchUrl = `${proxyUrl}sites/?url=${encodeURIComponent(url)}`
} else {
// Relative path - ensure it ends with / for query param usage
const basePath = proxyBase.endsWith('/') ? proxyBase : (proxyBase || '/sites/')
fetchUrl = `${basePath}?url=${encodeURIComponent(url)}`
}
logger.info('[WebService] Using proxy for OG fetch', { originalUrl: url, proxyUrl: fetchUrl, proxyBase })
} else { } else {
logger.info('[WebService] URL already proxied, using as-is', { url, fetchUrl }) logger.info('[WebService] URL already proxied, using as-is', { url, fetchUrl })
} }
@ -32,8 +37,10 @@ class WebService {
try { try {
// Add timeout and better error handling // Add timeout and better error handling
// Use 35 second timeout (proxy has 30s, add buffer for network latency)
// This matches wikistr's timeout and allows Puppeteer to execute JavaScript
const controller = new AbortController() const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), 5000) // 5 second timeout for proxy const timeoutId = setTimeout(() => controller.abort(), 35000) // 35 second timeout for proxy
// Fetch with appropriate headers // Fetch with appropriate headers
// Note: credentials: 'omit' prevents sending cookies, which avoids SameSite warnings // Note: credentials: 'omit' prevents sending cookies, which avoids SameSite warnings
@ -105,9 +112,30 @@ class WebService {
const description = const description =
doc.querySelector('meta[property="og:description"]')?.getAttribute('content') || doc.querySelector('meta[property="og:description"]')?.getAttribute('content') ||
(doc.querySelector('meta[name="description"]') as HTMLMetaElement | null)?.content (doc.querySelector('meta[name="description"]') as HTMLMetaElement | null)?.content
const image = (doc.querySelector('meta[property="og:image"]') as HTMLMetaElement | null) let image = (doc.querySelector('meta[property="og:image"]') as HTMLMetaElement | null)
?.content ?.content
// Convert relative image URLs to absolute URLs by prepending the domain
if (image) {
try {
const urlObj = new URL(url)
// Check if image is a relative URL (starts with / or doesn't have a protocol)
if (image.startsWith('/')) {
// Absolute path on same domain
image = `${urlObj.protocol}//${urlObj.host}${image}`
} else if (!image.match(/^https?:\/\//)) {
// Relative path (e.g., "images/og.jpg")
// Resolve relative to the URL's path
const basePath = urlObj.pathname.substring(0, urlObj.pathname.lastIndexOf('/') + 1)
image = `${urlObj.protocol}//${urlObj.host}${basePath}${image}`
}
logger.info('[WebService] Converted relative image URL to absolute', { originalImage: (doc.querySelector('meta[property="og:image"]') as HTMLMetaElement | null)?.content, absoluteImage: image })
} catch (error) {
logger.warn('[WebService] Failed to convert relative image URL', { image, url, error })
// Keep original image URL if conversion fails
}
}
logger.info('[WebService] Extracted OG metadata', { url, title: title?.substring(0, 100), description: description?.substring(0, 100), hasImage: !!image }) logger.info('[WebService] Extracted OG metadata', { url, title: title?.substring(0, 100), description: description?.substring(0, 100), hasImage: !!image })
// Filter out Jumble's default OG tags if we're fetching a different domain // Filter out Jumble's default OG tags if we're fetching a different domain

Loading…
Cancel
Save