feat: enhance Links type bookmarks with OpenGraph data

- Add opengraphEnhancer service using fetch-opengraph library
- Enhance ReadItems with proper titles, descriptions, and cover images
- Update deriveLinksFromBookmarks to use async OpenGraph enhancement
- Add caching and batching to avoid overwhelming external services
- Improve bookmark card display with rich metadata from OpenGraph tags
This commit is contained in:
Gigi
2025-10-25 01:15:37 +02:00
parent 6ac40c8a17
commit 6fd40f2ff6
3 changed files with 126 additions and 5 deletions

View File

@@ -280,8 +280,8 @@ const Me: React.FC<MeProps> = ({
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)

View File

@@ -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<string, any>()
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<ReadItem> {
// 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<ReadItem[]> {
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
}
}

View File

@@ -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<ReadItem[]> {
const linksMap = new Map<string, ReadItem>()
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)
}