// Search uses DuckDuckGo's static HTML endpoint via plain fetch — it serves // results to a fetch-shaped client but bot-walls headless Chromium, so the // browser is reserved for what actually needs it (opening/reading pages). import { sanitize } from "./sanitize.ts"; import { extractUrl } from "./net.ts"; export async function duckduckgoSearch( query: string, maxResults: number, signal?: AbortSignal, ) { const params = new URLSearchParams({ q: query, kl: "us-en" }); const res = await fetch(`https://html.duckduckgo.com/html/?${params}`, { method: "GET", headers: { "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:120.0) Gecko/20100101 Firefox/120.0", "Accept-Language": "en-US,en;q=0.9", "Accept": "text/html,application/xhtml+xml", }, signal, }); if (!res.ok) throw new Error(`DuckDuckGo search failed: HTTP ${res.status}`); const html = await res.text(); const anchors = [...html.matchAll(/]*\bclass="[^"]*result__a\b)[^>]*\bhref="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi)]; const snippets = [...html.matchAll(/]*\bclass="[^"]*result__snippet\b[^"]*"[^>]*>([\s\S]*?)<\/a>/gi)]; return anchors.slice(0, maxResults).flatMap((m, i) => { const url = extractUrl(m[1]); if (!url) return []; return [{ title: sanitize(m[2]), url, snippet: sanitize(snippets[i]?.[1] ?? "") }]; }); }