This repository has no description
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}