/** * Landing-page response for `GET /` (and `/index.html`) on the registry. * * This is the polite-301 from the design doc: * (apps/celilo/designs/REGISTRY_BROWSE_UI.md, decision D2). The response * carries `301 Moved Permanently` with `Location:` pointing at the * celilo.computer site's `/modules/` browse UI — interactive browsers * follow the redirect. Clients that don't follow by default (curl * without `-L`, naive bots, bare-bones HTTP libraries) see the body, * which carries the install one-liner, a link to the browse UI, and * the API endpoints they probably actually wanted. * * Single endpoint, no Astro dep, no client JS, no asset pipeline. */ /** * HTML-escape `<`, `>`, `&`, `"` so values interpolated into the body * can't inject markup. Operator-controlled values (publicUrl) are * trusted in practice, but defensive escaping costs nothing. */ function escapeHtml(s: string): string { return s .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } /** * Build the polite-301 response. * * @param publicUrl The registry's public URL (typically * `https://celilo.computer/registry`). The site's * origin is derived by stripping the path. The * `/modules/` browse URL on that origin is the * redirect target. */ export function landingResponse(publicUrl: string): Response { const u = new URL(publicUrl); const siteOrigin = u.origin; const browseUrl = `${siteOrigin}/modules/`; const installUrl = `${siteOrigin}/install.sh`; const apiBase = publicUrl.replace(/\/$/, ''); const body = `
Home-lab orchestration modules — published, versioned, install via the Celilo CLI.
If you're a person, head to browse the modules →
Install the Celilo CLI:
curl -fsSL ${escapeHtml(installUrl)} | bash
If you're a tool, the JSON API is here:
GET ${escapeHtml(apiBase)}/api/v1/modules
GET ${escapeHtml(apiBase)}/api/v1/modules/{name}
GET ${escapeHtml(apiBase)}/api/v1/modules/{name}/{version}/download
This URL is a 301 to ${escapeHtml(browseUrl)}. Most browsers followed it before you saw this.
`; return new Response(body, { status: 301, headers: { Location: browseUrl, 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'public, max-age=300', }, }); }