/** * Landing page proxy. * * Fetches pre-rendered HTML from standalone r*.online domains, transforms it * with Bun's HTMLRewriter (rewrite asset URLs, strip scripts, inject rSpace * shell header), caches the result, and serves it on rspace.online/{moduleId}. * * Falls back to the generic landing page if the fetch fails. */ import type { ModuleInfo } from "../shared/module"; import { escapeHtml, escapeAttr } from "./shell"; // ── Cache ── interface CacheEntry { html: string; fetchedAt: number; } const cache = new Map(); const CACHE_TTL = 10 * 60 * 1000; // 10 minutes // ── Ecosystem link rewriting ── /** Map standalone domains → rspace.online paths for footer links */ function buildEcosystemMap(modules: ModuleInfo[]): Map { const map = new Map(); for (const m of modules) { if (m.standaloneDomain) { map.set(`https://${m.standaloneDomain}`, `https://rspace.online/${m.id}`); // Also catch without trailing slash map.set(`https://www.${m.standaloneDomain}`, `https://rspace.online/${m.id}`); } } return map; } // ── Shell header HTML ── function renderShellHeader(moduleId: string, modules: ModuleInfo[]): string { const moduleListJSON = JSON.stringify(modules); const demoUrl = `https://demo.rspace.online/${escapeAttr(moduleId)}`; return `
`; } // ── Main export ── export async function fetchLandingPage( mod: { id: string; standaloneDomain?: string }, modules: ModuleInfo[], ): Promise { const domain = mod.standaloneDomain; if (!domain) return null; const now = Date.now(); const cached = cache.get(domain); // Return fresh cache if (cached && now - cached.fetchedAt < CACHE_TTL) { return cached.html; } try { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 5000); const res = await fetch(`https://${domain}`, { headers: { "User-Agent": "rSpace-Proxy/1.0" }, signal: controller.signal, }); clearTimeout(timeout); if (!res.ok) { // Return stale cache if available if (cached) return cached.html; return null; } const rawHtml = await res.text(); // Transform with HTMLRewriter const html = await transformWithRewriter(rawHtml, domain, mod.id, modules); cache.set(domain, { html, fetchedAt: now }); return html; } catch (e) { // Network error / timeout — return stale cache if available if (cached) return cached.html; console.error(`[LandingProxy] Failed to fetch ${domain}:`, (e as Error).message); return null; } } /** Async HTMLRewriter transform (Bun's HTMLRewriter works on Response objects) */ async function transformWithRewriter( rawHtml: string, domain: string, moduleId: string, modules: ModuleInfo[], ): Promise { const ecosystemMap = buildEcosystemMap(modules); const origin = `https://${domain}`; let headerInjected = false; const rewriter = new HTMLRewriter(); // 1. Rewrite _next asset URLs to absolute (CSS, fonts, images) rewriter.on('link[href^="/_next/"]', { element(el) { const href = el.getAttribute("href"); if (href) el.setAttribute("href", `${origin}${href}`); }, }); rewriter.on('img[src^="/_next/"]', { element(el) { const src = el.getAttribute("src"); if (src) el.setAttribute("src", `${origin}${src}`); }, }); // 2. Strip all `, { html: true }, ); }, }); // 5. If no