// 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>
${body}
`; } function header(basePath) { const homeHref = basePath ? `${basePath}/` : "/"; const connectivityHtml = dev ? `
offline
` : ''; return `
${connectivityHtml}
`; } function footer() { return ` `; } function renderPostRow(post, idx, basePath) { const title = autoLinkHandles(escapeHtml(post.title || "(untitled)")); const url = post.url ? escapeHtml(post.url) : ""; // Show URL with reasonable truncation for homepage list const displayUrl = post.url ? (() => { try { const u = new URL(post.url); let path = u.hostname.replace(/^www\./, "") + u.pathname; if (path.endsWith("/")) path = path.slice(0, -1); if (path.length > 40) path = path.slice(0, 37) + "..."; return path; } catch { return ""; } })() : ""; const itemUrl = `${basePath}/${post.code}`; const ext = post.externalAttribution; const rowClass = ext ? `news-row news-row--external news-row--external-${escapeHtml(ext.source || 'bsky')}` : "news-row"; return `
${idx + 1}.
${title} ${displayUrl ? `(${displayUrl})` : ""}
by ${renderHandle(post.handle, ext ? { external: true, profileUrl: ext.profileUrl, sourceLabel: ext.sourceLabel, } : undefined)} ${formatDate(post.when)} ${post.commentCount || 0} comments
`; } function renderComment(comment) { const commentId = comment._id?.toString() || ''; return `
${renderHandle(comment.handle)} ${formatDate(comment.when)}
${autoLinkHandles(escapeHtml(comment.text || ""))}
`; } async function applyCommentCounts(database, posts) { if (!posts || posts.length === 0) return posts || []; const comments = database.db.collection("news-comments"); const codes = posts.map((post) => post.code).filter(Boolean); const counts = await comments .aggregate([ { $match: { postCode: { $in: codes }, status: { $ne: "dead" } } }, { $group: { _id: "$postCode", count: { $sum: 1 } } }, ]) .toArray(); const countMap = new Map(counts.map((row) => [row._id, row.count])); return posts.map((post) => ({ ...post, commentCount: countMap.get(post.code) || 0, })); } async function fetchPosts(database, { sort = "new", limit = 30 }) { const posts = database.db.collection("news-posts"); const sortSpec = { when: -1 }; // Chronological (newest first) const docs = await posts.find({ status: { $ne: "dead" } }).sort(sortSpec).limit(limit).toArray(); return applyCommentCounts(database, docs); } // Short display form of a Bluesky handle: "artistnewsnetwork.bsky.social" → "artistnewsnetwork" function shortBskyHandle(handle) { if (!handle) return "anon"; return handle.replace(/\.bsky\.social$/i, ""); } async function hydrateHandles(database, docs) { const handles = database.db.collection("@handles"); const subs = docs.map((doc) => doc.user).filter(Boolean); const handleDocs = subs.length ? await handles.find({ _id: { $in: subs } }).toArray() : []; const map = new Map(handleDocs.map((h) => [h._id, h.handle])); return docs.map((doc) => { // External source takes precedence — attribute to the Bluesky handle/DID // rather than an AC @handle, and mark the doc so renderers link externally. if (doc.external?.source === "bsky" && doc.external?.handle) { const short = shortBskyHandle(doc.external.handle); return { ...doc, handle: `@${short}`, externalAttribution: { source: "bsky", sourceLabel: "bsky", did: doc.external.did, fullHandle: doc.external.handle, profileUrl: `https://bsky.app/profile/${doc.external.handle}`, postUrl: doc.external.postUri ? `https://bsky.app/profile/${doc.external.did}/post/${doc.external.postUri.split("/").pop()}` : null, }, }; } return { ...doc, handle: doc.user ? `@${map.get(doc.user) || "anon"}` : "@anon", }; }); } async function renderFrontPage(database, basePath, sort) { const posts = await fetchPosts(database, { sort, limit: 30 }); const hydrated = await hydrateHandles(database, posts); const rows = hydrated.map((post, idx) => renderPostRow(post, idx, basePath)).join("\n"); const emptyState = `

No posts yet.

`; const reportLink = ` `; return ` ${header(basePath)}
${rows || emptyState}
${reportLink}
${footer()}`; } async function renderItemPage(database, basePath, code) { const posts = database.db.collection("news-posts"); const comments = database.db.collection("news-comments"); const post = await posts.findOne({ code }); if (!post) { return { title: "Not Found | Aesthetic News", body: `${header(basePath)}

Post not found.

${footer()}` }; } const hydratedPost = (await hydrateHandles(database, [post]))[0]; const commentDocs = await comments .find({ postCode: code, status: { $ne: "dead" } }) .sort({ when: 1 }) .toArray(); const hydratedComments = await hydrateHandles(database, commentDocs); const commentsHtml = hydratedComments.map((c) => renderComment(c)).join("\n"); const postTitle = autoLinkHandles(escapeHtml(hydratedPost.title || "(untitled)")); const pageTitle = `${hydratedPost.title || "(untitled)"} | Aesthetic News`; const url = hydratedPost.url ? escapeHtml(hydratedPost.url) : ""; // Show full URL const displayUrl = hydratedPost.url ? (() => { try { const u = new URL(hydratedPost.url); let path = u.hostname.replace(/^www\./, "") + u.pathname; if (path.endsWith("/")) path = path.slice(0, -1); return path; } catch { return ""; } })() : ""; // Generate ATProto permalink if available const atLinks = atprotoPermalink(hydratedPost.atproto); const atLinkHtml = atLinks ? ` 🔗 AT ` : ''; // For external posts without body text, still show attribution so readers // can see it came from a trusted third-party source. const externalAttribHtml = (hydratedPost.externalAttribution && !hydratedPost.text) ? ` via ${renderHandle(hydratedPost.handle, { external: true, profileUrl: hydratedPost.externalAttribution.profileUrl, sourceLabel: hydratedPost.externalAttribution.sourceLabel, })} ${formatDate(hydratedPost.when)}${hydratedPost.externalAttribution.postUrl ? ` · original` : ''}` : ''; // Check for YouTube embed const youtubeId = parseYouTubeUrl(hydratedPost.url); const youtubeEmbedHtml = youtubeId ? ` ` : ''; // Check for KidLisp preview const kidlispCode = parseKidlispUrl(hydratedPost.url); const kidlispPreviewHtml = kidlispCode ? ` ` : ''; // Check for aesthetic.computer piece preview const acPiece = parseAestheticComputerUrl(hydratedPost.url); const acPreviewHtml = acPiece ? ` ` : ''; // Check for Imgur images const imgurUrl = parseImgurUrl(hydratedPost.url); const imgurPreviewHtml = imgurUrl ? ` ` : ''; // Check for direct image URLs const directImageUrl = !imgurUrl ? parseDirectImageUrl(hydratedPost.url) : null; const directImagePreviewHtml = directImageUrl ? ` ` : ''; // Check for e-flux article preview const efluxUrl = parseEfluxUrl(hydratedPost.url); let efluxPreviewHtml = ''; if (efluxUrl && !youtubeId && !kidlispCode && !acPiece && !imgurUrl && !directImageUrl) { const meta = await fetchEfluxMeta(efluxUrl); if (meta) { const sectionBadge = meta.section ? `e-flux ${escapeHtml(meta.section)}${meta.issue ? ` ${escapeHtml(meta.issue)}` : ''}` : 'e-flux'; efluxPreviewHtml = ` `; } } // Check for Instagram preview (try Graph API first, fall back to og:image) const instagramUrl = parseInstagramUrl(hydratedPost.url); let instagramPreviewHtml = ''; if (instagramUrl && !youtubeId && !kidlispCode && !acPiece && !imgurUrl && !directImageUrl && !efluxPreviewHtml) { // Try Instagram Graph API first const embedData = await fetchInstagramEmbed(instagramUrl, database); if (embedData && embedData.html) { // Use official Instagram embed HTML instagramPreviewHtml = `
${embedData.html}
`; } else { // Fallback to og:image const ogMeta = await fetchOgMeta(instagramUrl); if (ogMeta && ogMeta.image) { instagramPreviewHtml = ` `; } } } // Generic og:image fallback for any other URL let ogPreviewHtml = ''; if (hydratedPost.url && !youtubeId && !kidlispCode && !acPiece && !imgurUrl && !directImageUrl && !efluxPreviewHtml && !instagramPreviewHtml) { const ogMeta = await fetchOgMeta(hydratedPost.url); if (ogMeta && ogMeta.image) { ogPreviewHtml = ` `; } } const hasMedia = youtubeId || kidlispCode || acPiece || imgurUrl || directImageUrl || efluxPreviewHtml || instagramPreviewHtml || ogPreviewHtml; const mediaHtml = youtubeEmbedHtml || kidlispPreviewHtml || acPreviewHtml || imgurPreviewHtml || directImagePreviewHtml || efluxPreviewHtml || instagramPreviewHtml || ogPreviewHtml; const itemExt = hydratedPost.externalAttribution; const mainClass = itemExt ? `news-main news-item--external news-item--external-${escapeHtml(itemExt.source || 'bsky')}` : "news-main"; const body = ` ${header(basePath)} ${hasMedia ? `
${mediaHtml}
` : ''}
${postTitle} ${displayUrl ? `(${displayUrl})` : ""}
${externalAttribHtml} ${atLinkHtml}
${hydratedPost.text ? `
${renderHandle(hydratedPost.handle, itemExt ? { external: true, profileUrl: itemExt.profileUrl, sourceLabel: itemExt.sourceLabel, } : undefined)} ${formatDate(hydratedPost.when)}${itemExt?.postUrl ? ` · original` : ""}
${renderMarkdown(hydratedPost.text)}
` : ""}
${commentsHtml || ''}
${youtubeId ? `` : ''}
${footer()}`; // Build og metadata for social cards. const ogDescription = hydratedPost.text ? hydratedPost.text.slice(0, 200).replace(/\n/g, " ") + (hydratedPost.text.length > 200 ? "..." : "") : ""; const ogImage = `https://oven.aesthetic.computer/news-og/${encodeURIComponent(code)}.png`; const ogUrl = `https://news.aesthetic.computer/${code}`; const og = { title: hydratedPost.title || "(untitled)", description: ogDescription, image: ogImage, url: ogUrl, }; return { title: pageTitle, body, og }; } async function renderReportPage(basePath) { return ` ${header(basePath)}

Report the News

English
Dansk

Be curious. Be kind. Keep it weird.

${footer()}`; } export function createHandler({ connect: connectFn = connect, respond: respondFn = respond } = {}) { return async function handler(event) { if (event.httpMethod !== "GET") { return respondFn(405, { error: "Method not allowed" }); } const isSub = isSubdomainRequest(event); const basePath = isSub ? "" : "/news.aesthetic.computer"; const assetBase = "/news.aesthetic.computer"; const host = event.headers?.host || ""; const isLocalHost = host.includes("localhost") || host.startsWith("127.0.0.1") || host.startsWith("0.0.0.0"); // Only skip origin on localhost; subdomain requests always need the // full origin since news.aesthetic.computer doesn't serve static files. const assetOrigin = isLocalHost ? "" : "https://aesthetic.computer"; const route = parseRoute(event); console.log("[news] Route debug:", { path: event.path, queryStringParameters: event.queryStringParameters, parsedRoute: route }); let title = "Aesthetic News"; let database; try { database = await connectFn(); let body; let og; if (!route || route === "") { title = "Aesthetic News"; body = await renderFrontPage(database, basePath, "top"); } else if (route === "new") { title = "New | Aesthetic News"; body = await renderFrontPage(database, basePath, "new"); } else if (route.startsWith("item/")) { // Backward compat: redirect old /item/CODE to /nCODE const oldCode = route.slice(5); await database.disconnect(); return respondFn(301, "", { Location: `${basePath}/n${oldCode}`, "Content-Type": "text/html" }); } else if (route === "report" || route === "submit") { title = "Report · Aesthetic News"; body = await renderReportPage(basePath); } else if (route === "guidelines") { // handled elsewhere, but don't 404 on it title = "Not Found | Aesthetic News"; body = `${header(basePath)}

Page not found.

${footer()}`; } else { // Treat any other route as a potential news code (n-prefixed or legacy) const result = await renderItemPage(database, basePath, route); body = result.body || result; title = result.title || "Aesthetic News"; og = result.og; } const html = layout({ title, body, assetBase, assetOrigin, og }); await database.disconnect(); return respondFn(200, html, { "Content-Type": "text/html" }); } catch (error) { console.error("News render error:", error); if (database) await database.disconnect(); return respondFn(500, "News render error", { "Content-Type": "text/plain" }); } }; } export const handler = createHandler();