From c68eaf53c2dd1f91e7f6987693a021afb9881255 Mon Sep 17 00:00:00 2001 From: Local Dev Date: Wed, 16 Sep 2026 19:24:29 +0200 Subject: [PATCH] fix(argus): try dir-index before SPA fallback for extensionless paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two-tier fallback for extensionless paths that miss in Sia. The previous change went straight to root index.html, which meant a real sub-site living at "/about/index.html" got shadowed by the SPA root — visitors requesting "/about" got served the app instead of the landing page. New chain (matches nginx try_files order): 1) request path → /about hit: bns/potidaea/about → 404 2) directory index attempt → /about/ hit: bns/potidaea/about/index.html → 200 · SERVE 3) if still 404, SPA root → bns/potidaea/index.html → 200 Trailing-slash requests still short-circuit to their own index.html via the existing `rel` derivation (unchanged), so /about/ has been working the whole time; only the bare /about was falling through to the SPA rescue. Deployed to silentmode 2026-09-16: scp Argus/src/gateway/public-gateway.mjs silentmode:/tmp/public-gateway.mjs sudo cp … /opt/bns-gateway/public-gateway.mjs sudo systemctl restart bns-gateway --- src/gateway/public-gateway.mjs | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/src/gateway/public-gateway.mjs b/src/gateway/public-gateway.mjs index a5627e6..4bf0dca 100644 --- a/src/gateway/public-gateway.mjs +++ b/src/gateway/public-gateway.mjs @@ -235,15 +235,28 @@ async function serve(res, name, reqPath, baseHref) { : reqPath.slice(1); const key = base.endsWith("/") ? base + rel : base; let up = await aws.fetch(`${S3_ENDPOINT}/${key}`); - // SPA fallback — if the extension-less path 404s, serve the domain's - // own index.html so client-side routers (History API) can pick up - // /katalogos, /anthemus, /artemision, etc. under any BNS mount. - // We only fall back when the requested path has no file extension — - // requests for missing images / json / js still 404 as they should. + // Fallback chain for missing extensionless paths (order matters): + // 1) directory-index: try "/index.html" — matches nginx + // try_files. This handles static sub-sites like /about that + // have their own index.html. + // 2) SPA fallback: serve the domain's ROOT index.html so + // client-side routers can pick up /katalogos, /anthemus, etc. + // Requests with a file extension (missing .png / .js / .json) are + // NOT rescued — those should never be routed by a client-side + // router and swallowing them would hide real bugs. if (up.status === 404 && !/\.[a-z0-9]+$/i.test(reqPath) && reqPath !== "/") { - const fallbackKey = (base.endsWith("/") ? base : base + "/") + "index.html"; - const fb = await aws.fetch(`${S3_ENDPOINT}/${fallbackKey}`); - if (fb.ok) { up = fb; } + // Try directory-index first + if (!reqPath.endsWith("/")) { + const dirKey = (base.endsWith("/") ? base : base + "/") + reqPath.slice(1).replace(/\/+$/, '') + "/index.html"; + const dir = await aws.fetch(`${S3_ENDPOINT}/${dirKey}`); + if (dir.ok) { up = dir; } + } + // Then SPA root fallback + if (up.status === 404) { + const fallbackKey = (base.endsWith("/") ? base : base + "/") + "index.html"; + const fb = await aws.fetch(`${S3_ENDPOINT}/${fallbackKey}`); + if (fb.ok) { up = fb; } + } } const buf = Buffer.from(await up.arrayBuffer()); let ct = up.headers.get("content-type"); if (!ct || ct === "application/octet-stream") ct = guess(key);