diff --git a/src/components/Me.tsx b/src/components/Me.tsx index b03a2fa2..bd80894a 100644 --- a/src/components/Me.tsx +++ b/src/components/Me.tsx @@ -280,8 +280,8 @@ const Me: React.FC = ({ try { if (!hasBeenLoaded) setLoading(true) - // Derive links from bookmarks immediately (bookmarks come from centralized loading in App.tsx) - const initialLinks = deriveLinksFromBookmarks(bookmarks) + // Derive links from bookmarks with OpenGraph enhancement + const initialLinks = await deriveLinksFromBookmarks(bookmarks) const initialMap = new Map(initialLinks.map(item => [item.id, item])) setLinksMap(initialMap) setLinks(initialLinks) diff --git a/src/services/opengraphEnhancer.ts b/src/services/opengraphEnhancer.ts new file mode 100644 index 00000000..a71ad4e4 --- /dev/null +++ b/src/services/opengraphEnhancer.ts @@ -0,0 +1,117 @@ +import { fetch } from 'fetch-opengraph' +import { ReadItem } from './readsService' + +// Cache for OpenGraph data to avoid repeated requests +const ogCache = new Map() +const CACHE_TTL = 7 * 24 * 60 * 60 * 1000 // 7 days + +interface CachedOgData { + data: any + timestamp: number +} + +function getCachedOgData(url: string): any | null { + const cached = ogCache.get(url) + if (!cached) return null + + const age = Date.now() - cached.timestamp + if (age > CACHE_TTL) { + ogCache.delete(url) + return null + } + + return cached.data +} + +function setCachedOgData(url: string, data: any): void { + ogCache.set(url, { + data, + timestamp: Date.now() + }) +} + +/** + * Enhances a ReadItem with OpenGraph data + * Only fetches if the item doesn't already have good metadata + */ +export async function enhanceReadItemWithOpenGraph(item: ReadItem): Promise { + // Skip if we already have good metadata + if (item.title && item.title !== fallbackTitleFromUrl(item.url || '') && item.image) { + return item + } + + if (!item.url) return item + + try { + // Check cache first + let ogData = getCachedOgData(item.url) + + if (!ogData) { + // Fetch OpenGraph data + ogData = await fetch(item.url) + setCachedOgData(item.url, ogData) + } + + if (!ogData) return item + + // Enhance the item with OpenGraph data + const enhanced: ReadItem = { ...item } + + // Use OpenGraph title if we don't have a good title + if (!enhanced.title || enhanced.title === fallbackTitleFromUrl(item.url)) { + enhanced.title = ogData['og:title'] || ogData['twitter:title'] || ogData.title || enhanced.title + } + + // Use OpenGraph description if we don't have a summary + if (!enhanced.summary) { + enhanced.summary = ogData['og:description'] || ogData['twitter:description'] || ogData.description + } + + // Use OpenGraph image if we don't have an image + if (!enhanced.image) { + enhanced.image = ogData['og:image'] || ogData['twitter:image'] || ogData.image + } + + return enhanced + } catch (error) { + console.warn('Failed to enhance ReadItem with OpenGraph data:', error) + return item + } +} + +/** + * Enhances multiple ReadItems with OpenGraph data in parallel + * Uses batching to avoid overwhelming the service + */ +export async function enhanceReadItemsWithOpenGraph(items: ReadItem[]): Promise { + const BATCH_SIZE = 5 + const BATCH_DELAY = 1000 // 1 second between batches + + const enhancedItems: ReadItem[] = [] + + for (let i = 0; i < items.length; i += BATCH_SIZE) { + const batch = items.slice(i, i + BATCH_SIZE) + + // Process batch in parallel + const batchPromises = batch.map(item => enhanceReadItemWithOpenGraph(item)) + const batchResults = await Promise.all(batchPromises) + enhancedItems.push(...batchResults) + + // Add delay between batches to be respectful to the service + if (i + BATCH_SIZE < items.length) { + await new Promise(resolve => setTimeout(resolve, BATCH_DELAY)) + } + } + + return enhancedItems +} + +// Helper function to generate fallback title from URL +function fallbackTitleFromUrl(url: string): string { + try { + const urlObj = new URL(url) + return urlObj.hostname.replace('www.', '') + } catch { + return url + } +} diff --git a/src/utils/linksFromBookmarks.ts b/src/utils/linksFromBookmarks.ts index ec83c5e4..e7227bfe 100644 --- a/src/utils/linksFromBookmarks.ts +++ b/src/utils/linksFromBookmarks.ts @@ -2,13 +2,14 @@ import { Bookmark } from '../types/bookmarks' import { ReadItem } from '../services/readsService' import { KINDS } from '../config/kinds' import { fallbackTitleFromUrl } from './readItemMerge' +import { enhanceReadItemsWithOpenGraph } from '../services/opengraphEnhancer' /** * Derives ReadItems from bookmarks for external URLs: * - Web bookmarks (kind:39701) * - Any bookmark with http(s) URLs in content or urlReferences */ -export function deriveLinksFromBookmarks(bookmarks: Bookmark[]): ReadItem[] { +export async function deriveLinksFromBookmarks(bookmarks: Bookmark[]): Promise { const linksMap = new Map() const allBookmarks = bookmarks.flatMap(b => b.individualBookmarks || []) @@ -59,11 +60,14 @@ export function deriveLinksFromBookmarks(bookmarks: Bookmark[]): ReadItem[] { } } - // Sort by most recent bookmark activity - return Array.from(linksMap.values()).sort((a, b) => { + // Get initial items sorted by most recent bookmark activity + const initialItems = Array.from(linksMap.values()).sort((a, b) => { const timeA = a.readingTimestamp || 0 const timeB = b.readingTimestamp || 0 return timeB - timeA }) + + // Enhance with OpenGraph data + return await enhanceReadItemsWithOpenGraph(initialItems) }