import type { VercelRequest, VercelResponse } from '@vercel/node' import { RelayPool } from 'applesauce-relay' import { nip19 } from 'nostr-tools' import { AddressPointer } from 'nostr-tools/nip19' import { NostrEvent } from 'nostr-tools' import { Helpers } from 'applesauce-core' const { getArticleTitle, getArticleImage, getArticleSummary } = Helpers // Relay configuration (from src/config/relays.ts) const RELAYS = [ 'wss://relay.damus.io', 'wss://nos.lol', 'wss://relay.nostr.band', 'wss://relay.dergigi.com', 'wss://wot.dergigi.com', 'wss://relay.snort.social', 'wss://relay.current.fyi', 'wss://nostr-pub.wellorder.net', 'wss://purplepag.es', 'wss://relay.primal.net' ] type CacheEntry = { html: string expires: number } const WEEK_MS = 7 * 24 * 60 * 60 * 1000 const memoryCache = new Map() function escapeHtml(text: string): string { return text .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, ''') } interface ArticleMetadata { title: string summary: string image: string author: string published?: number } async function fetchArticleMetadata(naddr: string): Promise { const relayPool = new RelayPool() try { // Decode naddr const decoded = nip19.decode(naddr) if (decoded.type !== 'naddr') { return null } const pointer = decoded.data as AddressPointer // Connect to relays const relayUrls = pointer.relays && pointer.relays.length > 0 ? pointer.relays : RELAYS relayUrls.forEach(url => relayPool.open(url)) // Fetch article (kind:30023) const articleFilter = { kinds: [pointer.kind], authors: [pointer.pubkey], '#d': [pointer.identifier || ''] } const articleEvents: NostrEvent[] = [] await new Promise((resolve) => { const timeout = setTimeout(() => resolve(), 5000) relayPool.req(relayUrls, articleFilter).subscribe({ next: (msg) => { if (msg.type === 'EVENT') { articleEvents.push(msg.event) } }, error: () => resolve(), complete: () => { clearTimeout(timeout) resolve() } }) }) if (articleEvents.length === 0) { relayPool.close() return null } // Sort by created_at and take most recent articleEvents.sort((a, b) => b.created_at - a.created_at) const article = articleEvents[0] // Fetch author profile (kind:0) const profileFilter = { kinds: [0], authors: [pointer.pubkey] } const profileEvents: NostrEvent[] = [] await new Promise((resolve) => { const timeout = setTimeout(() => resolve(), 3000) relayPool.req(relayUrls, profileFilter).subscribe({ next: (msg) => { if (msg.type === 'EVENT') { profileEvents.push(msg.event) } }, error: () => resolve(), complete: () => { clearTimeout(timeout) resolve() } }) }) relayPool.close() // Extract article metadata const title = getArticleTitle(article) || 'Untitled Article' const summary = getArticleSummary(article) || 'Read this article on Boris' const image = getArticleImage(article) || '/boris-social-1200.png' // Extract author name from profile let authorName = pointer.pubkey.slice(0, 8) + '...' if (profileEvents.length > 0) { profileEvents.sort((a, b) => b.created_at - a.created_at) const profile = profileEvents[0] try { const profileData = JSON.parse(profile.content) authorName = profileData.display_name || profileData.name || authorName } catch { // Use fallback } } return { title, summary, image, author: authorName, published: article.created_at } } catch (err) { console.error('Failed to fetch article metadata:', err) relayPool.close() return null } } function generateHtml(naddr: string, meta: ArticleMetadata | null): string { const baseUrl = 'https://read.withboris.com' const articleUrl = `${baseUrl}/a/${naddr}` const title = meta?.title || 'Boris – Nostr Bookmarks' const description = meta?.summary || 'Your reading list for the Nostr world. A minimal nostr client for bookmark management with highlights.' const image = meta?.image?.startsWith('http') ? meta.image : `${baseUrl}${meta?.image || '/boris-social-1200.png'}` const author = meta?.author || 'Boris' return ` ${escapeHtml(title)} ${meta?.published ? `` : ''}
` } export default async function handler(req: VercelRequest, res: VercelResponse) { const naddr = (req.query.naddr as string | undefined)?.trim() if (!naddr) { return res.status(400).json({ error: 'Missing naddr parameter' }) } // Check cache const cacheKey = naddr const now = Date.now() const cached = memoryCache.get(cacheKey) if (cached && cached.expires > now) { res.setHeader('Cache-Control', 'public, max-age=86400, s-maxage=604800') res.setHeader('Content-Type', 'text/html; charset=utf-8') return res.status(200).send(cached.html) } try { // Fetch metadata const meta = await fetchArticleMetadata(naddr) // Generate HTML const html = generateHtml(naddr, meta) // Cache the result memoryCache.set(cacheKey, { html, expires: now + WEEK_MS }) // Send response res.setHeader('Cache-Control', 'public, max-age=86400, s-maxage=604800') res.setHeader('Content-Type', 'text/html; charset=utf-8') return res.status(200).send(html) } catch (err) { console.error('Error generating article OG HTML:', err) // Fallback to basic HTML with SPA boot const html = generateHtml(naddr, null) res.setHeader('Cache-Control', 'public, max-age=3600') res.setHeader('Content-Type', 'text/html; charset=utf-8') return res.status(200).send(html) } }