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; } }