import { NextRequest, NextResponse } from 'next/server' import { auth } from '@/auth' import { redis } from '@/lib/redis' export async function GET(req: NextRequest) { const session = await auth() if (!session?.user?.id) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } const url = req.nextUrl.searchParams.get('url') if (!url) { return NextResponse.json({ error: 'Missing url' }, { status: 400 }) } const cacheKey = `linkpreview:${session.user.id}:${Buffer.from(url).toString('base64url').slice(0, 80)}` try { const cached = await redis.get(cacheKey) if (cached) { return NextResponse.json(JSON.parse(cached), { headers: { 'Cache-Control': 'public, s-maxage=86400' }, }) } } catch {} try { const parsed = new URL(url) if (!['http:', 'https:'].includes(parsed.protocol)) { return NextResponse.json({ error: 'Invalid protocol' }, { status: 400 }) } const hostname = parsed.hostname if ( hostname === 'localhost' || hostname === '127.0.0.1' || hostname.startsWith('10.') || hostname.startsWith('192.168.') || hostname.startsWith('169.254.') || hostname.startsWith('172.16.') || hostname.startsWith('172.17.') || hostname.startsWith('172.18.') || hostname.startsWith('172.19.') || hostname.startsWith('172.20.') || hostname.startsWith('172.21.') || hostname.startsWith('172.22.') || hostname.startsWith('172.23.') || hostname.startsWith('172.24.') || hostname.startsWith('172.25.') || hostname.startsWith('172.26.') || hostname.startsWith('172.27.') || hostname.startsWith('172.28.') || hostname.startsWith('172.29.') || hostname.startsWith('172.30.') || hostname.startsWith('172.31.') || hostname === '0.0.0.0' || hostname.endsWith('.local') || hostname.endsWith('.internal') ) { return NextResponse.json({ error: 'Blocked' }, { status: 403 }) } const controller = new AbortController() const timeout = setTimeout(() => controller.abort(), 8000) const res = await fetch(url, { signal: controller.signal, headers: { 'User-Agent': 'Mozilla/5.0 (compatible; MementoBot/1.0)', 'Accept': 'text/html', }, redirect: 'follow', }) clearTimeout(timeout) if (!res.ok) { return NextResponse.json({ error: 'Fetch failed' }, { status: 502 }) } const html = await res.text() const data = extractMetadata(html, parsed) try { await redis.setex(cacheKey, 300, JSON.stringify(data)) } catch {} return NextResponse.json(data, { headers: { 'Cache-Control': 'public, s-maxage=86400' }, }) } catch { return NextResponse.json({ error: 'Failed to fetch' }, { status: 502 }) } } function extractMetadata(html: string, url: URL) { const getMeta = (pattern: RegExp): string | null => { const m = html.match(pattern) return m?.[1]?.trim() || null } const getMetaProperty = (prop: string): string | null => { return getMeta(new RegExp(`]+(?:property|name)=["']${prop}["'][^>]+content=["']([^"']+)["']`, 'i')) || getMeta(new RegExp(`]+content=["']([^"']+)["'][^>]+(?:property|name)=["']${prop}["']`, 'i')) } const title = getMetaProperty('og:title') || getMeta(new RegExp(']*>([^<]+)', 'i')) || null const description = getMetaProperty('og:description') || getMetaProperty('description') || getMetaProperty('twitter:description') || null const image = getMetaProperty('og:image') || getMetaProperty('twitter:image') || null const siteName = getMetaProperty('og:site_name') || null const favicon = image ? null : `https://www.google.com/s2/favicons?domain=${url.hostname}&sz=32` const finalImage = image ? (image.startsWith('http') ? image : new URL(image, url.origin).href) : null return { title: title ? decodeEntities(title) : null, description: description ? decodeEntities(description).slice(0, 200) : null, image: finalImage, favicon, siteName: siteName || url.hostname.replace('www.', ''), } } function decodeEntities(text: string): string { return text .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, "'") .replace(/'/g, "'") .replace(/ /g, ' ') }