import type { APIRoute } from "astro"; import CryptoJSW from "@originjs/crypto-js-wasm"; // In-memory cache with expiration const CACHE: Record< string, { data: ArrayBuffer; contentType: string; timestamp: number } > = {}; const CACHE_DURATION = 7 * 24 * 60 * 60 * 1000; // 7 days in milliseconds export const GET: APIRoute = async ({ params, request }) => { const siteDomain = params.siteFavicon; if (!siteDomain) { return new Response(JSON.stringify({ error: "Missing site domain" }), { status: 400, headers: { "Content-Type": "application/json", }, }); } await CryptoJSW.MD5.loadWasm(); const cacheKey = CryptoJSW.MD5(siteDomain).toString(); const now = Date.now(); // Check if we have a valid cached response if (CACHE[cacheKey] && now - CACHE[cacheKey].timestamp < CACHE_DURATION) { console.log(`Serving cached favicon for ${siteDomain}`); return new Response(CACHE[cacheKey].data, { status: 200, headers: { "Content-Type": CACHE[cacheKey].contentType, "Cache-Control": "public, max-age=604800", // 7 days "Access-Control-Allow-Origin": "*", // Allow CORS "X-Cache": "HIT", }, }); } try { // First try to fetch favicon directly from the website const siteUrl = `https://${siteDomain}`; console.log(`Trying to fetch favicon directly from ${siteUrl}`); // Fetch with a reasonable timeout const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 3000); // Fetch the website's HTML const siteResponse = await fetch(siteUrl, { headers: { "User-Agent": "Mozilla/5.0 (compatible; FaviconFetcher/1.0)", }, redirect: "follow", signal: controller.signal, }).catch(() => null); clearTimeout(timeout); // If we got a response, try to parse it for favicon links if (siteResponse && siteResponse.ok) { const html = await siteResponse.text(); // Look for favicon in HTML const faviconMatch = html.match( /]*rel=["'](?:shortcut )?icon["'][^>]*href=["']([^"']+)["']/i ) || html.match( /]*href=["']([^"']+)["'][^>]*rel=["'](?:shortcut )?icon["']/i ) || html.match( /]*rel=["']apple-touch-icon["'][^>]*href=["']([^"']+)["']/i ) || html.match( /]*href=["']([^"']+)["'][^>]*rel=["']apple-touch-icon["']/i ); if (faviconMatch && faviconMatch[1]) { let faviconUrl = faviconMatch[1]; // Make sure the URL is absolute if (!faviconUrl.startsWith("http")) { if (faviconUrl.startsWith("/")) { faviconUrl = `https://${siteDomain}${faviconUrl}`; } else { faviconUrl = `https://${siteDomain}/${faviconUrl}`; } } // Try to fetch the favicon console.log(`Found favicon URL: ${faviconUrl}`); const faviconResponse = await fetch(faviconUrl, { redirect: "follow", }).catch(() => null); if (faviconResponse && faviconResponse.ok) { // Get content type and the binary data const contentType = faviconResponse.headers.get("Content-Type") || "image/x-icon"; const imageBuffer = await faviconResponse.arrayBuffer(); // Store in cache CACHE[cacheKey] = { data: imageBuffer, contentType, timestamp: now, }; // Return the favicon with proper headers return new Response(imageBuffer, { status: 200, headers: { "Content-Type": contentType, "Cache-Control": "public, max-age=604800", // 7 days "Access-Control-Allow-Origin": "*", // Allow CORS "X-Cache": "MISS", "X-Source": "direct", }, }); } } // If no favicon found or fetch failed, try default favicon console.log(`Trying default favicon.ico for ${siteDomain}`); const defaultFaviconUrl = `https://${siteDomain}/favicon.ico`; const defaultFaviconResponse = await fetch(defaultFaviconUrl).catch( () => null ); if (defaultFaviconResponse && defaultFaviconResponse.ok) { const contentType = defaultFaviconResponse.headers.get("Content-Type") || "image/x-icon"; const imageBuffer = await defaultFaviconResponse.arrayBuffer(); // Cache the result CACHE[cacheKey] = { data: imageBuffer, contentType, timestamp: now, }; // Return the default favicon return new Response(imageBuffer, { status: 200, headers: { "Content-Type": contentType, "Cache-Control": "public, max-age=604800", // 7 days "Access-Control-Allow-Origin": "*", // Allow CORS "X-Cache": "MISS", "X-Source": "default", }, }); } } // Fallback to Google's favicon service if direct fetching fails console.log(`Falling back to Google's favicon service for ${siteDomain}`); const googleResponse = await fetch( `https://www.google.com/s2/favicons?domain=${decodeURIComponent( siteDomain )}&sz=64` ); if (!googleResponse.ok) { // If Google's service fails, just return a 404 return new Response(JSON.stringify({ error: "Favicon not found" }), { status: 404, headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*", // Allow CORS }, }); } // Get content type and the binary data const contentType = googleResponse.headers.get("Content-Type") || "image/x-icon"; const imageBuffer = await googleResponse.arrayBuffer(); // Store in cache CACHE[cacheKey] = { data: imageBuffer, contentType, timestamp: now, }; // Return the favicon with proper headers return new Response(imageBuffer, { status: 200, headers: { "Content-Type": contentType, "Cache-Control": "public, max-age=604800", // 7 days "Access-Control-Allow-Origin": "*", // Allow CORS "X-Cache": "MISS", "X-Source": "google", }, }); } catch (error) { console.error("Error fetching favicon:", error); // Return a JSON error response return new Response( JSON.stringify({ error: "Failed to fetch favicon", details: error instanceof Error ? error.message : String(error), }), { status: 500, headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*", // Allow CORS }, } ); } };