This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

semble / src / webapp / app / api / reader / route.ts
2.7 kB 85 lines
1import { NextRequest, NextResponse } from 'next/server'; 2import { JSDOM } from 'jsdom'; 3import { Readability } from '@mozilla/readability'; 4import DOMPurify from 'isomorphic-dompurify'; 5 6export interface ReaderContent { 7 title: string | null; 8 content: string; 9 byline: string | null; 10 siteName: string | null; 11 excerpt: string | null; 12} 13 14export async function GET(request: NextRequest) { 15 const { searchParams } = request.nextUrl; 16 const url = searchParams.get('url'); 17 18 if (!url) { 19 return NextResponse.json({ error: 'Missing url parameter' }, { status: 400 }); 20 } 21 22 let targetUrl: URL; 23 try { 24 targetUrl = new URL(url); 25 } catch { 26 return NextResponse.json({ error: 'Invalid URL' }, { status: 400 }); 27 } 28 29 let html: string; 30 try { 31 const response = await fetch(targetUrl.toString(), { 32 headers: { 33 // Mimic a real browser to avoid blocks 34 'User-Agent': 35 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', 36 Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 37 'Accept-Language': 'en-US,en;q=0.5', 38 }, 39 // 10 second timeout 40 signal: AbortSignal.timeout(10_000), 41 }); 42 43 if (!response.ok) { 44 return NextResponse.json( 45 { error: `Failed to fetch URL: ${response.status} ${response.statusText}` }, 46 { status: 502 }, 47 ); 48 } 49 50 html = await response.text(); 51 } catch (err) { 52 const message = err instanceof Error ? err.message : 'Unknown error'; 53 return NextResponse.json({ error: `Failed to fetch URL: ${message}` }, { status: 502 }); 54 } 55 56 // Parse with jsdom, passing the URL so Readability can resolve relative links 57 const dom = new JSDOM(html, { url: targetUrl.toString() }); 58 const reader = new Readability(dom.window.document); 59 const article = reader.parse(); 60 61 if (!article) { 62 return NextResponse.json( 63 { error: 'Could not extract readable content from this page' }, 64 { status: 422 }, 65 ); 66 } 67 68 // Sanitize the extracted HTML to prevent XSS 69 const sanitizedContent = DOMPurify.sanitize(article.content ?? '', { 70 USE_PROFILES: { html: true }, 71 // Allow common article elements but strip scripts/iframes/etc. 72 FORBID_TAGS: ['script', 'style', 'iframe', 'form', 'input', 'button'], 73 FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover'], 74 }); 75 76 const result: ReaderContent = { 77 title: article.title ?? null, 78 content: sanitizedContent, 79 byline: article.byline ?? null, 80 siteName: article.siteName ?? null, 81 excerpt: article.excerpt ?? null, 82 }; 83 84 return NextResponse.json(result); 85}