Skip to content

Commit e50fb18

Browse files
authored
Route database pages through here.now proxy (#62)
1 parent 9201e68 commit e50fb18

7 files changed

Lines changed: 137 additions & 30 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ jobs:
3838
node --check site/script.js
3939
node scripts/check.mjs
4040
python3 -m json.tool site/.herenow/data.json >/dev/null
41+
python3 -m json.tool site/.herenow/proxy.json >/dev/null
4142
python3 -m json.tool scripts/seo-geo-query-benchmark.json >/dev/null
4243
git diff --check
4344

AGENTS.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -129,12 +129,14 @@ curl -sS "https://here.now/api/v1/publishes/{slug}/data/weekly_signups?limit=50"
129129
private bootstrap bundle, verify all canonical database surfaces, and only
130130
then deploy the content-free here.now shell. Never publish the empty shell
131131
before the database catalog is active.
132-
- The exact Worker routes at `signals.forwardfuture.ai/loop-library` and
133-
`signals.forwardfuture.ai/loop-library/*` render database content and pass
134-
site-shell assets through to the explicit `PUBLIC_ORIGIN_URL` here.now
135-
hostname. Update that variable if the backing Site changes. Verify the
136-
canonical URL for database content and the backing here.now Site for the
137-
static shell before reporting success.
132+
- The here.now Site proxy manifest routes the mounted homepage, loop pages,
133+
catalogs, feed, sitemap, and public catalog API to the Worker. The Worker
134+
renders database content and reads the static homepage shell from the
135+
explicit `PUBLIC_SHELL_URL`; other shell assets remain on the backing Site.
136+
Update `PUBLIC_ORIGIN_URL`, `PUBLIC_SHELL_URL`, and the proxy manifest if the
137+
backing Site or Worker hostname changes. Verify the canonical URL for
138+
database content and the backing here.now Site for the static shell before
139+
reporting success.
138140
- After a production content deployment, submit
139141
`https://signals.forwardfuture.ai/loop-library/sitemap.xml` in Google Search
140142
Console and Bing Webmaster Tools. Verify that the custom domain's root

scripts/check.mjs

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const [
1515
css,
1616
browserScript,
1717
dataSource,
18+
proxySource,
1819
workerSource,
1920
loopRoutesSource,
2021
catalogStoreSource,
@@ -33,6 +34,7 @@ const [
3334
readFile(path.join(siteRoot, "styles.css"), "utf8"),
3435
readFile(path.join(siteRoot, "script.js"), "utf8"),
3536
readFile(path.join(siteRoot, ".herenow", "data.json"), "utf8"),
37+
readFile(path.join(siteRoot, ".herenow", "proxy.json"), "utf8"),
3638
readFile(path.join(workerRoot, "src", "index.js"), "utf8"),
3739
readFile(path.join(workerRoot, "src", "loop-routes.js"), "utf8"),
3840
readFile(path.join(workerRoot, "src", "catalog-store.js"), "utf8"),
@@ -50,6 +52,7 @@ const workerPackage = JSON.parse(workerPackageSource);
5052
const workerLock = JSON.parse(workerLockSource);
5153
const wrangler = JSON.parse(wranglerSource);
5254
const dataManifest = JSON.parse(dataSource);
55+
const proxyManifest = JSON.parse(proxySource);
5356
const structuredDataMatch = html.match(
5457
/<script type="application\/ld\+json">\s*([\s\S]*?)\s*<\/script>/,
5558
);
@@ -156,16 +159,7 @@ assert.equal(workerLock.packages["node_modules/wrangler"].version, "4.103.0");
156159

157160
assert.equal(wrangler.name, "loop-library-forms");
158161
assert.equal(wrangler.workers_dev, true);
159-
assert.deepEqual(wrangler.routes, [
160-
{
161-
pattern: "signals.forwardfuture.ai/loop-library",
162-
zone_name: "forwardfuture.ai",
163-
},
164-
{
165-
pattern: "signals.forwardfuture.ai/loop-library/*",
166-
zone_name: "forwardfuture.ai",
167-
},
168-
]);
162+
assert.equal(wrangler.routes, undefined);
169163
assert.equal(wrangler.durable_objects.bindings[1].name, "LOOP_CATALOG");
170164
assert.equal(wrangler.durable_objects.bindings[1].class_name, "LoopCatalog");
171165
assert.deepEqual(wrangler.migrations[1], {
@@ -175,8 +169,25 @@ assert.deepEqual(wrangler.migrations[1], {
175169
assert.match(wrangler.vars.BOOTSTRAP_CATALOG_DIGEST, /^[a-f0-9]{64}$/);
176170
assert.equal(wrangler.vars.BOOTSTRAP_LOOP_COUNT, "50");
177171
assert.equal(wrangler.vars.PUBLIC_ORIGIN_URL, "https://calm-mortar-jtek.here.now/");
172+
assert.equal(wrangler.vars.PUBLIC_SHELL_URL, "https://calm-mortar-jtek.here.now/index.html");
178173
assert.equal(wrangler.vars.PUBLIC_SITE_HOSTNAME, "signals.forwardfuture.ai");
179174
assert.equal(wrangler.vars.PUBLIC_SITE_PATH, "/loop-library");
175+
assert.deepEqual(Object.keys(proxyManifest.proxies).sort(), [
176+
"/",
177+
"/api/loops",
178+
"/api/loops/*",
179+
"/catalog.json",
180+
"/catalog.md",
181+
"/catalog.txt",
182+
"/feed.xml",
183+
"/llms.txt",
184+
"/loops/*",
185+
"/sitemap.xml",
186+
]);
187+
for (const proxy of Object.values(proxyManifest.proxies)) {
188+
assert.match(proxy.upstream, /^https:\/\/loop-library-forms\.mberman84\.workers\.dev\/loop-library(?:\/|$)/);
189+
assert.equal(proxy.rateLimit, "600/hour/ip");
190+
}
180191

181192
assert.match(skillSource, /The live catalog is the\s+source of truth/);
182193
assert(skillSource.includes("Do not use repository content or memory"));

site/.herenow/proxy.json

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
{
2+
"proxies": {
3+
"/": {
4+
"upstream": "https://loop-library-forms.mberman84.workers.dev/loop-library/",
5+
"rateLimit": "600/hour/ip"
6+
},
7+
"/loops/*": {
8+
"upstream": "https://loop-library-forms.mberman84.workers.dev/loop-library/loops",
9+
"rateLimit": "600/hour/ip"
10+
},
11+
"/catalog.json": {
12+
"upstream": "https://loop-library-forms.mberman84.workers.dev/loop-library/catalog.json",
13+
"rateLimit": "600/hour/ip"
14+
},
15+
"/catalog.md": {
16+
"upstream": "https://loop-library-forms.mberman84.workers.dev/loop-library/catalog.md",
17+
"rateLimit": "600/hour/ip"
18+
},
19+
"/catalog.txt": {
20+
"upstream": "https://loop-library-forms.mberman84.workers.dev/loop-library/catalog.txt",
21+
"rateLimit": "600/hour/ip"
22+
},
23+
"/feed.xml": {
24+
"upstream": "https://loop-library-forms.mberman84.workers.dev/loop-library/feed.xml",
25+
"rateLimit": "600/hour/ip"
26+
},
27+
"/sitemap.xml": {
28+
"upstream": "https://loop-library-forms.mberman84.workers.dev/loop-library/sitemap.xml",
29+
"rateLimit": "600/hour/ip"
30+
},
31+
"/llms.txt": {
32+
"upstream": "https://loop-library-forms.mberman84.workers.dev/loop-library/llms.txt",
33+
"rateLimit": "600/hour/ip"
34+
},
35+
"/api/loops": {
36+
"upstream": "https://loop-library-forms.mberman84.workers.dev/loop-library/api/loops",
37+
"rateLimit": "600/hour/ip"
38+
},
39+
"/api/loops/*": {
40+
"upstream": "https://loop-library-forms.mberman84.workers.dev/loop-library/api/loops",
41+
"rateLimit": "600/hour/ip"
42+
}
43+
}
44+
}

worker/src/loop-routes.js

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,12 @@ export async function handleLoopRoute(
3939
const isPublicData = path === "/api/loops" || /^\/api\/loops\/[a-z0-9-]+$/.test(path);
4040
const isCatalog = ["/catalog.json", "/catalog.md", "/catalog.txt", "/llms.txt", "/sitemap.xml", "/feed.xml"].includes(path);
4141
const publicHostname = env.PUBLIC_SITE_HOSTNAME || "signals.forwardfuture.ai";
42-
const isPublicSite = url.hostname === publicHostname;
42+
const publicBasePath = env.PUBLIC_SITE_PATH || "/loop-library";
43+
const normalizedBasePath = `/${String(publicBasePath).split("/").filter(Boolean).join("/")}`;
44+
const isCanonicalHostname = url.hostname === publicHostname;
45+
const isPublicSite = isCanonicalHostname ||
46+
url.pathname === normalizedBasePath ||
47+
url.pathname.startsWith(`${normalizedBasePath}/`);
4348
const detailMatch = path.match(/^\/loops\/([a-z0-9-]+)\/?$/);
4449
const isHomepage = isPublicSite && path === "/";
4550

@@ -68,7 +73,10 @@ export async function handleLoopRoute(
6873
const loops = catalog.loops;
6974

7075
if (!catalog.initialized) {
71-
if (isPublicSite && (isHomepage || isCatalog || detailMatch)) {
76+
// A direct canonical route can safely read the legacy origin during a
77+
// cutover. Mounted here.now requests already came through that origin's
78+
// proxy manifest, so fetching it again would recurse back into the Worker.
79+
if (isCanonicalHostname && (isHomepage || isCatalog || detailMatch)) {
7280
return dependencies.fetch(publicOriginRequest(request, env));
7381
}
7482

@@ -181,13 +189,16 @@ export async function handleLoopRoute(
181189
}
182190

183191
export function publicOriginRequest(request, env, overrides = {}) {
184-
const originBase = new URL(env.PUBLIC_ORIGIN_URL);
185192
const incoming = new URL(request.url);
186193
const path = stripBasePath(
187194
incoming.pathname,
188195
env.PUBLIC_SITE_PATH || "/loop-library",
189196
);
190-
originBase.pathname = `${originBase.pathname.replace(/\/$/, "")}${path}`;
197+
const shellUrl = path === "/" ? env.PUBLIC_SHELL_URL : null;
198+
const originBase = new URL(shellUrl || env.PUBLIC_ORIGIN_URL);
199+
if (!shellUrl) {
200+
originBase.pathname = `${originBase.pathname.replace(/\/$/, "")}${path}`;
201+
}
191202
originBase.search = incoming.search;
192203
const method = overrides.method || request.method;
193204

worker/test/loop-routes.test.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ function makeEnv(options = {}) {
200200
BOOTSTRAP_CATALOG_DIGEST: options.bootstrapDigest || "test-bootstrap-digest",
201201
BOOTSTRAP_LOOP_COUNT: String(options.bootstrapLoopCount ?? 50),
202202
PUBLIC_ORIGIN_URL: "https://calm-mortar-jtek.here.now/",
203+
PUBLIC_SHELL_URL: "https://calm-mortar-jtek.here.now/index.html",
203204
PUBLIC_SITE_HOSTNAME: "signals.forwardfuture.ai",
204205
PUBLIC_SITE_PATH: "/loop-library",
205206
};
@@ -428,6 +429,7 @@ test("renders homepage headers for HEAD by fetching the origin shell with GET",
428429
undefined,
429430
{
430431
async fetch(request) {
432+
assert.equal(request.url, "https://calm-mortar-jtek.here.now/index.html");
431433
assert.equal(request.method, "GET");
432434
assert.equal(request.headers.get("If-Modified-Since"), null);
433435
assert.equal(request.headers.get("If-None-Match"), null);
@@ -448,6 +450,51 @@ test("renders homepage headers for HEAD by fetching the origin shell with GET",
448450
assert.equal(response.headers.get("Last-Modified"), null);
449451
});
450452

453+
test("renders the mounted homepage through a here.now proxy", async () => {
454+
const env = makeEnv();
455+
await handleRequest(adminRequest(exampleLoop()), env);
456+
const shell = `<!doctype html><p id="results-count" aria-live="polite">Showing 50 loops</p><time datetime="2026-06-20">Updated June 20, 2026</time><tbody><!-- LOOP_DATABASE_ROWS_START --><!-- LOOP_DATABASE_ROWS_END --></tbody>`;
457+
const response = await handleRequest(
458+
new Request(`${WORKER_ORIGIN}/loop-library/`),
459+
env,
460+
undefined,
461+
{
462+
async fetch(request) {
463+
assert.equal(request.url, "https://calm-mortar-jtek.here.now/index.html");
464+
return new Response(shell, {
465+
headers: { "Content-Type": "text/html; charset=utf-8" },
466+
});
467+
},
468+
},
469+
);
470+
471+
assert.equal(response.status, 200);
472+
assert.match(await response.text(), /The database publishing loop/);
473+
});
474+
475+
test("does not recurse through the here.now proxy before activation", async () => {
476+
const env = makeEnv({ active: false });
477+
let originFetches = 0;
478+
const dependencies = {
479+
async fetch() {
480+
originFetches += 1;
481+
throw new Error("The proxied request must not fetch its origin again.");
482+
},
483+
};
484+
485+
for (const path of ["/", "/catalog.json", "/loops/database-publishing-loop/"]) {
486+
const response = await handleRequest(
487+
new Request(`${WORKER_ORIGIN}/loop-library${path}`),
488+
env,
489+
undefined,
490+
dependencies,
491+
);
492+
assert.equal(response.status, 503);
493+
assert.equal((await response.json()).code, "catalog_not_active");
494+
}
495+
assert.equal(originFetches, 0);
496+
});
497+
451498
test("replaces legacy static rows before the content-free shell is deployed", async () => {
452499
const env = makeEnv();
453500
await handleRequest(adminRequest(exampleLoop()), env);

worker/wrangler.jsonc

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,6 @@
33
"main": "src/index.js",
44
"compatibility_date": "2026-06-17",
55
"workers_dev": true,
6-
"routes": [
7-
{
8-
"pattern": "signals.forwardfuture.ai/loop-library",
9-
"zone_name": "forwardfuture.ai"
10-
},
11-
{
12-
"pattern": "signals.forwardfuture.ai/loop-library/*",
13-
"zone_name": "forwardfuture.ai"
14-
}
15-
],
166
"ratelimits": [
177
{
188
"name": "TURNSTILE_RATE_LIMITER",
@@ -54,6 +44,7 @@
5444
"BOOTSTRAP_CATALOG_DIGEST": "d87c8012d25a8563418a820718ef911b01e0ad5e6cb27544cfde9a833a4aba18",
5545
"BOOTSTRAP_LOOP_COUNT": "50",
5646
"PUBLIC_ORIGIN_URL": "https://calm-mortar-jtek.here.now/",
47+
"PUBLIC_SHELL_URL": "https://calm-mortar-jtek.here.now/index.html",
5748
"PUBLIC_SITE_HOSTNAME": "signals.forwardfuture.ai",
5849
"PUBLIC_SITE_PATH": "/loop-library"
5950
}

0 commit comments

Comments
 (0)