type FeedSource = { name: string; url: string; }; type FeedItem = { source: string; title: string; url: string; publishedAt: string | null; }; const feeds: FeedSource[] = [ { name: "OPENAI", url: "https://openai.com/news/rss.xml" }, { name: "GOOGLE AI", url: "https://blog.google/technology/ai/rss/" }, { name: "DEEPMIND", url: "https://deepmind.google/blog/rss.xml" }, { name: "NVIDIA", url: "https://nvidianews.nvidia.com/cats/generative_al.xml" }, { name: "ARS TECHNICA", url: "https://feeds.arstechnica.com/arstechnica/technology-lab" }, { name: "WIRED", url: "https://www.wired.com/feed/tag/ai/latest/rss" }, ]; const maxFeedBytes = 1024 * 1024; export const dynamic = "force-dynamic"; function decodeEntities(value: string) { return value .replace(//g, "$1") .replace(/<[^>]+>/g, " ") .replace(/&/g, "&") .replace(/"/g, '"') .replace(/'|'/g, "'") .replace(/</g, "<") .replace(/>/g, ">") .replace(/&#(\d+);/g, (_, code: string) => String.fromCodePoint(Number(code))) .replace(/\s+/g, " ") .trim(); } function extract(block: string, names: string[]) { for (const name of names) { const match = block.match(new RegExp(`<${name}(?:\\s[^>]*)?>([\\s\\S]*?)<\\/${name}>`, "i")); if (match?.[1]) return decodeEntities(match[1]); } return ""; } function extractLink(block: string) { const atomLink = block.match(/]+href=["']([^"']+)["'][^>]*>/i)?.[1]; if (atomLink) return decodeEntities(atomLink); return extract(block, ["link", "guid"]); } function parseFeed(xml: string, source: string): FeedItem[] { const blocks = xml.match(/]*)?>[\s\S]*?<\/item>/gi) ?? xml.match(/]*)?>[\s\S]*?<\/entry>/gi) ?? []; return blocks.slice(0, 6).flatMap((block) => { const title = extract(block, ["title"]); const url = extractLink(block); const rawDate = extract(block, ["pubDate", "published", "updated", "dc:date"]); const parsedDate = rawDate ? new Date(rawDate) : null; if (!title || !url) return []; return [{ source, title: title.slice(0, 180), url, publishedAt: parsedDate && !Number.isNaN(parsedDate.getTime()) ? parsedDate.toISOString() : null, }]; }); } async function readLimitedText(response: Response) { const declaredLength = Number(response.headers.get("content-length") ?? 0); if (declaredLength > maxFeedBytes) throw new Error("Headline feed is too large"); if (!response.body) return ""; const reader = response.body.getReader(); const decoder = new TextDecoder(); let total = 0; let text = ""; while (true) { const chunk = await reader.read(); if (chunk.done) break; total += chunk.value.byteLength; if (total > maxFeedBytes) { await reader.cancel(); throw new Error("Headline feed is too large"); } text += decoder.decode(chunk.value, { stream: true }); } return text + decoder.decode(); } async function readFeed(feed: FeedSource) { const response = await fetch(feed.url, { headers: { Accept: "application/atom+xml, application/rss+xml, application/xml, text/xml" }, signal: AbortSignal.timeout(6_000), }); if (!response.ok) throw new Error(`${feed.name} returned ${response.status}`); return parseFeed(await readLimitedText(response), feed.name); } export async function GET() { const results = await Promise.allSettled(feeds.map(readFeed)); const items = results .flatMap((result) => result.status === "fulfilled" ? result.value : []) .filter((item, index, all) => all.findIndex((candidate) => candidate.url === item.url || candidate.title === item.title) === index) .sort((a, b) => (Date.parse(b.publishedAt ?? "") || 0) - (Date.parse(a.publishedAt ?? "") || 0)) .slice(0, 10); return Response.json( { items, updatedAt: new Date().toISOString(), feedsAvailable: results.filter((result) => result.status === "fulfilled").length, feedsTotal: feeds.length, }, { headers: { "Cache-Control": "public, max-age=300, stale-while-revalidate=900", }, }, ); }