// News (HN-style), 2026.01.15 // Server-rendered HTML for news.aesthetic.computer import { connect } from "../../backend/database.mjs"; import { respond } from "../../backend/http.mjs"; // Dev mode: show live reload indicator const dev = process.env.CONTEXT === "dev" || process.env.NETLIFY_DEV === "true" || process.env.NODE_ENV === "development"; // Detect KidLisp URLs and extract the $code function parseKidlispUrl(url) { if (!url) return null; try { const u = new URL(url); // Match kidlisp.com/$CODE or *.kidlisp.com/$CODE if (!u.hostname.includes('kidlisp.com')) return null; // Path should be /$CODE (with dollar sign in URL) const match = u.pathname.match(/^\/\$?([a-zA-Z0-9_-]+)$/); if (!match) return null; return match[1]; // Return just the code without $ } catch { return null; } } // Detect e-flux article URLs function parseEfluxUrl(url) { if (!url) return null; try { const u = new URL(url); if (!u.hostname.includes('e-flux.com')) return null; // Match /journal/ISSUE/ID/SLUG or /notes/ID/SLUG or /architecture/*/ID/SLUG const match = u.pathname.match(/^\/(journal|notes|architecture)\//i); if (!match) return null; return url; // Return the full URL for fetching } catch { return null; } } // Detect Instagram URLs function parseInstagramUrl(url) { if (!url) return null; try { const u = new URL(url); // Match instagram.com/p/POST_ID or instagram.com/reel/REEL_ID if (!u.hostname.includes('instagram.com')) return null; const match = u.pathname.match(/^\/(p|reel|reels)\/([a-zA-Z0-9_-]+)/); if (!match) return null; return url; // Return the full URL for fetching } catch { return null; } } // Fetch Instagram post via Graph API (requires Facebook App credentials from database) async function fetchInstagramEmbed(instagramUrl, database) { if (!database) { console.log('[news] No database connection, falling back to og:image'); return null; } try { const secrets = await database.db.collection("secrets").findOne({ _id: "facebook" }); if (!secrets || !secrets.appId || !secrets.appSecret) { console.log('[news] Facebook credentials not found in database, falling back to og:image'); return null; } const appId = secrets.appId; const appSecret = secrets.appSecret; // Get app access token const tokenUrl = `https://graph.facebook.com/oauth/access_token?client_id=${appId}&client_secret=${appSecret}&grant_type=client_credentials`; const tokenRes = await fetch(tokenUrl); const tokenData = await tokenRes.json(); if (!tokenData.access_token) { console.error('[news] Failed to get Facebook access token'); return null; } // Fetch Instagram oEmbed const oembedUrl = `https://graph.facebook.com/v21.0/instagram_oembed?url=${encodeURIComponent(instagramUrl)}&access_token=${tokenData.access_token}`; const res = await fetch(oembedUrl); const data = await res.json(); if (data.error) { console.log('[news] Instagram API error:', data.error.message); return null; } return { html: data.html, authorName: data.author_name, mediaType: data.media_type, // 'IMAGE' or 'VIDEO' }; } catch (e) { console.error('[news] Instagram API error:', e.message); return null; } } // Fetch generic OpenGraph metadata from any URL async function fetchOgMeta(url) { try { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 5000); const res = await fetch(url, { signal: controller.signal, headers: { 'User-Agent': 'AestheticNewsBot/1.0 (+https://news.aesthetic.computer)', 'Accept': 'text/html', }, redirect: 'follow', }); clearTimeout(timeout); if (!res.ok) return null; // Read first 64KB for meta tags const reader = res.body.getReader(); const decoder = new TextDecoder(); let html = ''; while (html.length < 65536) { const { done, value } = await reader.read(); if (done) break; html += decoder.decode(value, { stream: true }); } reader.cancel().catch(() => {}); const getOg = (prop) => { const m = html.match(new RegExp(`]+(?:property|name)=["']${prop}["'][^>]+content=["']([^"']+)["']`, 'i')) || html.match(new RegExp(`]+content=["']([^"']+)["'][^>]+(?:property|name)=["']${prop}["']`, 'i')); return m ? m[1].replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, "'").trim() : ''; }; const title = getOg('og:title'); const description = getOg('og:description') || getOg('description'); const image = getOg('og:image'); const siteName = getOg('og:site_name'); if (!image) return null; // Only show preview if there's an image return { title, description, image, siteName }; } catch (e) { console.error('[news] og:image fetch error:', e.message); return null; } } // Fetch e-flux article metadata from og: tags async function fetchEfluxMeta(url) { try { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 5000); const res = await fetch(url, { signal: controller.signal, headers: { 'User-Agent': 'AestheticNewsBot/1.0 (+https://news.aesthetic.computer)', 'Accept': 'text/html', }, redirect: 'follow', }); clearTimeout(timeout); if (!res.ok) return null; // Read first 64KB for meta tags + some body text const reader = res.body.getReader(); const decoder = new TextDecoder(); let html = ''; while (html.length < 65536) { const { done, value } = await reader.read(); if (done) break; html += decoder.decode(value, { stream: true }); } reader.cancel().catch(() => {}); const getOg = (prop) => { const m = html.match(new RegExp(`]+(?:property|name)=["']${prop}["'][^>]+content=["']([^"']+)["']`, 'i')) || html.match(new RegExp(`]+content=["']([^"']+)["'][^>]+(?:property|name)=["']${prop}["']`, 'i')); return m ? m[1].replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, "'").trim() : ''; }; const title = getOg('og:title'); const description = getOg('og:description') || getOg('description'); const image = getOg('og:image'); // Try to extract author from RSC payload: "displayname":"NAME" let author = ''; const authorMatch = html.match(/"displayname"\s*:\s*"([^"]+)"/i); if (authorMatch) author = authorMatch[1]; // Try to extract issue & section from og:title (e.g. "Title - Journal #160") let section = ''; let issue = ''; const titleParts = title.match(/^(.+?)\s*-\s*(Journal|Notes|Architecture)\s*(#\d+)?$/i); if (titleParts) { section = titleParts[2] || ''; issue = titleParts[3] || ''; } // Extract first body paragraph from RSC payload let excerpt = ''; const bodyChunks = []; const rscRegex = /self\.__next_f\.push\(\[1,"[0-9a-f]+:T[0-9a-f]+,"\]\)<\/script>