mirror of
https://github.com/dergigi/boris.git
synced 2025-12-18 23:24:22 +01:00
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:
@@ -280,8 +280,8 @@ const Me: React.FC<MeProps> = ({
|
|||||||
try {
|
try {
|
||||||
if (!hasBeenLoaded) setLoading(true)
|
if (!hasBeenLoaded) setLoading(true)
|
||||||
|
|
||||||
// Derive links from bookmarks immediately (bookmarks come from centralized loading in App.tsx)
|
// Derive links from bookmarks with OpenGraph enhancement
|
||||||
const initialLinks = deriveLinksFromBookmarks(bookmarks)
|
const initialLinks = await deriveLinksFromBookmarks(bookmarks)
|
||||||
const initialMap = new Map(initialLinks.map(item => [item.id, item]))
|
const initialMap = new Map(initialLinks.map(item => [item.id, item]))
|
||||||
setLinksMap(initialMap)
|
setLinksMap(initialMap)
|
||||||
setLinks(initialLinks)
|
setLinks(initialLinks)
|
||||||
|
|||||||
117
src/services/opengraphEnhancer.ts
Normal file
117
src/services/opengraphEnhancer.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,13 +2,14 @@ import { Bookmark } from '../types/bookmarks'
|
|||||||
import { ReadItem } from '../services/readsService'
|
import { ReadItem } from '../services/readsService'
|
||||||
import { KINDS } from '../config/kinds'
|
import { KINDS } from '../config/kinds'
|
||||||
import { fallbackTitleFromUrl } from './readItemMerge'
|
import { fallbackTitleFromUrl } from './readItemMerge'
|
||||||
|
import { enhanceReadItemsWithOpenGraph } from '../services/opengraphEnhancer'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Derives ReadItems from bookmarks for external URLs:
|
* Derives ReadItems from bookmarks for external URLs:
|
||||||
* - Web bookmarks (kind:39701)
|
* - Web bookmarks (kind:39701)
|
||||||
* - Any bookmark with http(s) URLs in content or urlReferences
|
* - 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 linksMap = new Map<string, ReadItem>()
|
||||||
|
|
||||||
const allBookmarks = bookmarks.flatMap(b => b.individualBookmarks || [])
|
const allBookmarks = bookmarks.flatMap(b => b.individualBookmarks || [])
|
||||||
@@ -59,11 +60,14 @@ export function deriveLinksFromBookmarks(bookmarks: Bookmark[]): ReadItem[] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort by most recent bookmark activity
|
// Get initial items sorted by most recent bookmark activity
|
||||||
return Array.from(linksMap.values()).sort((a, b) => {
|
const initialItems = Array.from(linksMap.values()).sort((a, b) => {
|
||||||
const timeA = a.readingTimestamp || 0
|
const timeA = a.readingTimestamp || 0
|
||||||
const timeB = b.readingTimestamp || 0
|
const timeB = b.readingTimestamp || 0
|
||||||
return timeB - timeA
|
return timeB - timeA
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Enhance with OpenGraph data
|
||||||
|
return await enhanceReadItemsWithOpenGraph(initialItems)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user