From 82d4e28d62702a27d5eab99d8af6b8551f4e98ee Mon Sep 17 00:00:00 2001 From: Arun Kumar Thiagarajan Date: Sat, 27 Jun 2026 23:22:08 +0530 Subject: [PATCH] Serve a branded homepage fallback when the here.now shell is unavailable The homepage is rendered by fetching the here.now shell, injecting the catalog, and serving it. When that fetch throws or the shell returns a 5xx, the Worker passed the upstream failure straight through (loop-routes.js), so visitors hit an unbranded, catalog-less dead end even though the Worker already holds every loop record in its own database. Render a minimal branded fallback homepage from that catalog instead: it keeps the site chrome, lists every published loop with working links, and points agents at catalog.json and llms.txt. The real failure status is preserved (the upstream 5xx, or 502 when the shell is unreachable) so the outage stays honest and the page is marked noindex. Covered by two tests. --- loop-library/worker/src/loop-routes.js | 29 ++++++++- loop-library/worker/src/render-loops.js | 63 ++++++++++++++++++++ loop-library/worker/test/loop-routes.test.js | 47 +++++++++++++++ 3 files changed, 138 insertions(+), 1 deletion(-) diff --git a/loop-library/worker/src/loop-routes.js b/loop-library/worker/src/loop-routes.js index 8365d3f..2bf655c 100644 --- a/loop-library/worker/src/loop-routes.js +++ b/loop-library/worker/src/loop-routes.js @@ -9,6 +9,7 @@ import { renderAgentInstructions, renderCatalogMarkdown, renderFeed, + renderHomepageFallback, renderLoopPage, renderSitemap, } from "./render-loops.js"; @@ -159,7 +160,33 @@ export async function handleLoopRoute( conditional: false, range: false, }); - const originResponse = await dependencies.fetch(originRequest); + let originResponse; + try { + originResponse = await dependencies.fetch(originRequest); + } catch { + // The here.now shell is unreachable. Serve a branded fallback built from + // the catalog the Worker already holds so every loop stays reachable, + // rather than leaking the upstream failure. Report it honestly as 502. + return textResponse( + renderHomepageFallback(loops), + "text/html; charset=utf-8", + 502, + CACHE_HEADERS, + request.method, + ); + } + + if (originResponse.status >= 500) { + // Upstream shell error: serve the same fallback and preserve the real + // failure status instead of passing through the upstream error page. + return textResponse( + renderHomepageFallback(loops), + "text/html; charset=utf-8", + originResponse.status, + CACHE_HEADERS, + request.method, + ); + } if (!originResponse.ok) { return originResponse; diff --git a/loop-library/worker/src/render-loops.js b/loop-library/worker/src/render-loops.js index 9dcf477..8f45fd1 100644 --- a/loop-library/worker/src/render-loops.js +++ b/loop-library/worker/src/render-loops.js @@ -384,6 +384,69 @@ export function renderLoopPage(loop, loops) { `; } +export function renderHomepageFallback(loops) { + const items = loops + .map( + (loop) => + `
  • ${escapeHtml(loop.title)}

    ${escapeHtml(loop.summary)}

  • `, + ) + .join("\n"); + + return ` + + + + + + + + + + + + + + + ${escapeHtml(SITE.name)} — temporarily unavailable + + + + +
    +

    The Loop Library is briefly unavailable

    +

    The full homepage could not be loaded right now. Every published loop is still listed below, and the machine-readable catalog.json and agent instructions remain available.

    +

    Showing ${loops.length} loops.

    + +
    + + +`; +} + function shareActions(loop, url) { const text = `Try "${loop.title}" from the Loop Library: ${loop.summary}`; return `
    `; diff --git a/loop-library/worker/test/loop-routes.test.js b/loop-library/worker/test/loop-routes.test.js index 9df6e3c..6aaf479 100644 --- a/loop-library/worker/test/loop-routes.test.js +++ b/loop-library/worker/test/loop-routes.test.js @@ -472,6 +472,53 @@ test("renders the mounted homepage through a here.now proxy", async () => { assert.match(await response.text(), /The database publishing loop/); }); +test("serves a branded fallback homepage when the here.now shell errors", async () => { + const env = makeEnv(); + await handleRequest(adminRequest(exampleLoop()), env); + const response = await handleRequest( + new Request(`${SITE_ORIGIN}/loop-library/`), + env, + undefined, + { + async fetch() { + return new Response("upstream is down", { status: 503 }); + }, + }, + ); + const html = await response.text(); + + // Preserves the real failure status instead of masking it as 200. + assert.equal(response.status, 503); + assert.equal(response.headers.get("Cache-Control"), "no-store"); + // Branded fallback that still lists every loop, not the upstream error body. + assert.doesNotMatch(html, /upstream is down/); + assert.match(html, /briefly unavailable/); + assert.match(html, /The database publishing loop/); + assert.match(html, /loops\/database-publishing-loop\//); + assert.match(html, /catalog\.json/); + assert.match(html, / { + const env = makeEnv(); + await handleRequest(adminRequest(exampleLoop()), env); + const response = await handleRequest( + new Request(`${SITE_ORIGIN}/loop-library/`), + env, + undefined, + { + async fetch() { + throw new Error("connection refused"); + }, + }, + ); + const html = await response.text(); + + assert.equal(response.status, 502); + assert.match(html, /briefly unavailable/); + assert.match(html, /The database publishing loop/); +}); + test("normalizes legacy Forward Future domains on every public catalog surface", async () => { const env = makeEnv(); await handleRequest(