From e13059658431d7f75ef75a9cc120b3377c3d024f Mon Sep 17 00:00:00 2001 From: Local Dev Date: Sun, 20 Sep 2026 19:34:42 +0200 Subject: [PATCH] =?UTF-8?q?feat(gateway):=20clean=20URLs=20=E2=80=94=20ser?= =?UTF-8?q?ve=20.html=20+=20strip=20.html=20from=20hrefs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two symmetric changes so a site published as flat .html files (sirius.x has tld.html, portal.html, market.html, register.html, studio.html) reads with clean URLs to the visitor. 1. S3 handler fallback order for extensionless requests: /tld → tld.html (new — first) /tld → tld/index.html (existing — second) /... → index.html (existing — SPA last-resort) Requests with a real file extension (.js, .svg, .json) are never rescued — those must never be routed by a client-side router and swallowing them would hide real bugs. 2. transformHtml() gains stripHtmlExt(): rewrites href="foo.html" (relative) to href="foo", preserving anchor and query. Absolute URLs (https://silentmode.st/tlds/) are left alone — external references are none of our business. Backwards-compatible: existing /bns//foo.html links still 200. Verified end-to-end on sirius.x: /tld, /portal, /market, /register, /studio all serve; assets under js/, assets/ unchanged; direct .html requests still work. --- src/gateway/public-gateway.mjs | 42 +++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/src/gateway/public-gateway.mjs b/src/gateway/public-gateway.mjs index b6c45ff..c83eacd 100644 --- a/src/gateway/public-gateway.mjs +++ b/src/gateway/public-gateway.mjs @@ -317,11 +317,27 @@ function rewriteBnsLinks(html) { } ); } -// Apply both transforms (base first, then link rewrite) — order doesn't -// matter because they touch disjoint constructs, but grouping them here -// means every text/html response goes through the same path. +// Strip `.html` from relative link targets in served HTML — so a link +// written as href="tld.html" reads as href="tld" in the browser bar. +// Only touches non-absolute URLs (leaves `https://…/x.html` alone — +// those are external and often out of our control). Preserves anchor +// and query, and skips the ".html" that's part of a filename like +// "foo.htmlx" (regex requires end/? / #). +function stripHtmlExt(html) { + return html.replace( + /\b(href\s*=\s*")([^"#?][^"#?]*)\.html(?=["#?])/gi, + (m, prefix, path) => { + if (/^\s*[a-z][a-z0-9+.-]*:/i.test(path)) return m; // absolute URL — leave alone + return prefix + path; + } + ); +} +// Apply the three transforms (base first, link rewrite, extension strip). +// Order doesn't matter across them because they touch disjoint constructs, +// but grouping them here means every text/html response goes through the +// same path. function transformHtml(html, baseHref) { - return rewriteBnsLinks(injectBase(html, baseHref)); + return stripHtmlExt(rewriteBnsLinks(injectBase(html, baseHref))); } const nx = (h) => `

NXDOMAIN

${h} is not registered on the Bitcoin Cash chain.

`; @@ -487,15 +503,25 @@ async function serve(res, name, reqPath, baseHref) { // 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 !== "/") { - // Try directory-index first + const cleanBase = base.endsWith("/") ? base : base + "/"; + const cleanPath = reqPath.slice(1).replace(/\/+$/, ""); + // 1) Extensionless-URL support: /tld -> tld.html. Lets sites + // publish clean URLs without keeping mirror files around. if (!reqPath.endsWith("/")) { - const dirKey = (base.endsWith("/") ? base : base + "/") + reqPath.slice(1).replace(/\/+$/, '') + "/index.html"; + const htmlKey = cleanBase + cleanPath + ".html"; + const htmlHit = await aws.fetch(`${S3_ENDPOINT}/${htmlKey}`); + if (htmlHit.ok) { up = htmlHit; } + } + // 2) Directory-index: /docs -> docs/index.html + if (up.status === 404 && !reqPath.endsWith("/")) { + const dirKey = cleanBase + cleanPath + "/index.html"; const dir = await aws.fetch(`${S3_ENDPOINT}/${dirKey}`); if (dir.ok) { up = dir; } } - // Then SPA root fallback + // 3) SPA root fallback — last resort so client-side routers + // can pick up /katalogos, /anthemus, etc. if (up.status === 404) { - const fallbackKey = (base.endsWith("/") ? base : base + "/") + "index.html"; + const fallbackKey = cleanBase + "index.html"; const fb = await aws.fetch(`${S3_ENDPOINT}/${fallbackKey}`); if (fb.ok) { up = fb; } }