Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion loop-library/worker/src/loop-routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
renderAgentInstructions,
renderCatalogMarkdown,
renderFeed,
renderHomepageFallback,
renderLoopPage,
renderSitemap,
} from "./render-loops.js";
Expand Down Expand Up @@ -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;
Expand Down
63 changes: 63 additions & 0 deletions loop-library/worker/src/render-loops.js
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,69 @@ export function renderLoopPage(loop, loops) {
</html>`;
}

export function renderHomepageFallback(loops) {
const items = loops
.map(
(loop) =>
` <li class="fallback-loop"><a href="${SITE.baseUrl}loops/${escapeHtml(loop.slug)}/">${escapeHtml(loop.title)}</a><p>${escapeHtml(loop.summary)}</p></li>`,
)
.join("\n");

return `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="${escapeHtml(SITE.description)}" />
<meta name="robots" content="noindex" />
<meta name="theme-color" content="#faf8f7" />
<meta name="color-scheme" content="light dark" />
<script>
(() => {
const storageKey = "loop-library-theme";
let storedTheme;
try { storedTheme = window.localStorage.getItem(storageKey); } catch { storedTheme = null; }
const theme = storedTheme === "light" || storedTheme === "dark"
? storedTheme
: window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
document.documentElement.dataset.theme = theme;
document.querySelector('meta[name="theme-color"]')
.setAttribute("content", theme === "dark" ? "#101010" : "#faf8f7");
})();
</script>
<link rel="canonical" href="${SITE.baseUrl}" />
<link rel="alternate" type="application/json" title="Loop Library catalog" href="${SITE.baseUrl}catalog.json" />
<link rel="alternate" type="text/plain" title="${SITE.name} agent instructions" href="${SITE.baseUrl}llms.txt" />
<link rel="icon" type="image/png" href="${SITE.baseUrl}assets/favicon.png" />
<link rel="stylesheet" href="${SITE.baseUrl}styles.css" />
<title>${escapeHtml(SITE.name)} — temporarily unavailable</title>
</head>
<body>
<a class="skip-link" href="#main">Skip to content</a>
<header class="site-header">
<a class="brand-lockup" href="${SITE.baseUrl}" aria-label="Forward Future Loop Library home">
<img class="brand-mark" src="${SITE.baseUrl}assets/favicon.png" width="32" height="32" alt="" />
<span class="brand-name">Forward Future</span>
<span class="brand-product">Loop Library</span>
</a>
<nav class="site-nav" aria-label="Primary navigation">
<a href="${SITE.baseUrl}learn/">Learn</a>
<a href="${SITE.baseUrl}agents/">For agents</a>
</nav>
</header>
<main class="detail-main page-width" id="main">
<h1>The Loop Library is briefly unavailable</h1>
<p>The full homepage could not be loaded right now. Every published loop is still listed below, and the machine-readable <a href="${SITE.baseUrl}catalog.json">catalog.json</a> and <a href="${SITE.baseUrl}llms.txt">agent instructions</a> remain available.</p>
<p>Showing ${loops.length} loops.</p>
<ul class="fallback-loop-list">
${items}
</ul>
</main>
<footer class="site-footer"><div class="page-width footer-inner"><p><strong>Forward Future</strong> <span>Make the future legible.</span></p><p><a href="${SITE.baseUrl}">Loop Library</a> <a href="https://forwardfuture.com/" rel="noopener">forwardfuture.com</a></p></div></footer>
</body>
</html>`;
}

function shareActions(loop, url) {
const text = `Try "${loop.title}" from the Loop Library: ${loop.summary}`;
return `<div class="share-actions" aria-label="Share this loop"><button class="share-action share-action-primary" type="button" data-copy-social-post data-post-text="${escapeHtml(text)}" data-post-url="${escapeHtml(url)}" aria-label="Copy a social post about ${escapeHtml(loop.title)}"><svg class="share-copy-icon" viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="11" height="11"></rect><path d="M16 8V5a1 1 0 0 0-1-1H5a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h3"></path></svg><span>Share on social</span></button></div>`;
Expand Down
47 changes: 47 additions & 0 deletions loop-library/worker/test/loop-routes.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, /<meta name="robots" content="noindex"/);
});

test("serves the fallback homepage when the here.now shell is unreachable", async () => {
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(
Expand Down
Loading