From b32d7528587327164fe3357679c3d3094410ef04 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Fri, 27 Feb 2026 17:51:57 -0800 Subject: [PATCH] feat: redirect all r*.online domains to rspace.online/r* MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace internal rewrites with 301 redirects for all standalone domains. Handles bare domains, subdomains, paths, and query strings: - rnotes.online/ → rspace.online/rnotes - rnotes.online/alice/path → alice.rspace.online/rnotes/path - alice.rnotes.online/path → alice.rspace.online/rnotes/path - rnotes.online/api/... → rspace.online/rnotes/api/... Traefik labels updated to also match *.r*.online subdomains. Co-Authored-By: Claude Opus 4.6 --- docker-compose.yml | 47 ++++++++++++++-------------- server/index.ts | 76 ++++++++++++++++++++++++++-------------------- 2 files changed, 67 insertions(+), 56 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 070aacd..2485cec 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -51,92 +51,93 @@ services: - "traefik.http.routers.rspace-canvas.rule=HostRegexp(`{subdomain:[a-z0-9-]+}.rspace.online`) && !Host(`rspace.online`) && !Host(`www.rspace.online`) && !Host(`auth.rspace.online`)" - "traefik.http.routers.rspace-canvas.entrypoints=web" - "traefik.http.routers.rspace-canvas.priority=100" - # ── Standalone domain routing (priority 120) ── - - "traefik.http.routers.rspace-rbooks.rule=Host(`rbooks.online`)" + # ── Standalone domain routing (priority 120) — redirect to rspace.online ── + # Each rule matches bare domain + any subdomain (e.g. rnotes.online, alice.rnotes.online) + - "traefik.http.routers.rspace-rbooks.rule=Host(`rbooks.online`) || HostRegexp(`{sub:[a-z0-9-]+}.rbooks.online`)" - "traefik.http.routers.rspace-rbooks.entrypoints=web" - "traefik.http.routers.rspace-rbooks.priority=120" - "traefik.http.routers.rspace-rbooks.service=rspace-online" - - "traefik.http.routers.rspace-rpubs.rule=Host(`rpubs.online`)" + - "traefik.http.routers.rspace-rpubs.rule=Host(`rpubs.online`) || HostRegexp(`{sub:[a-z0-9-]+}.rpubs.online`)" - "traefik.http.routers.rspace-rpubs.entrypoints=web" - "traefik.http.routers.rspace-rpubs.priority=120" - "traefik.http.routers.rspace-rpubs.service=rspace-online" - - "traefik.http.routers.rspace-rchoices.rule=Host(`rchoices.online`)" + - "traefik.http.routers.rspace-rchoices.rule=Host(`rchoices.online`) || HostRegexp(`{sub:[a-z0-9-]+}.rchoices.online`)" - "traefik.http.routers.rspace-rchoices.entrypoints=web" - "traefik.http.routers.rspace-rchoices.priority=120" - "traefik.http.routers.rspace-rchoices.service=rspace-online" - - "traefik.http.routers.rspace-rfunds.rule=Host(`rfunds.online`)" + - "traefik.http.routers.rspace-rfunds.rule=Host(`rfunds.online`) || HostRegexp(`{sub:[a-z0-9-]+}.rfunds.online`)" - "traefik.http.routers.rspace-rfunds.entrypoints=web" - "traefik.http.routers.rspace-rfunds.priority=120" - "traefik.http.routers.rspace-rfunds.service=rspace-online" - - "traefik.http.routers.rspace-rforum.rule=Host(`rforum.online`)" + - "traefik.http.routers.rspace-rforum.rule=Host(`rforum.online`) || HostRegexp(`{sub:[a-z0-9-]+}.rforum.online`)" - "traefik.http.routers.rspace-rforum.entrypoints=web" - "traefik.http.routers.rspace-rforum.priority=120" - "traefik.http.routers.rspace-rforum.service=rspace-online" - - "traefik.http.routers.rspace-rvote.rule=Host(`rvote.online`)" + - "traefik.http.routers.rspace-rvote.rule=Host(`rvote.online`) || HostRegexp(`{sub:[a-z0-9-]+}.rvote.online`)" - "traefik.http.routers.rspace-rvote.entrypoints=web" - "traefik.http.routers.rspace-rvote.priority=120" - "traefik.http.routers.rspace-rvote.service=rspace-online" - - "traefik.http.routers.rspace-rwork.rule=Host(`rwork.online`)" + - "traefik.http.routers.rspace-rwork.rule=Host(`rwork.online`) || HostRegexp(`{sub:[a-z0-9-]+}.rwork.online`)" - "traefik.http.routers.rspace-rwork.entrypoints=web" - "traefik.http.routers.rspace-rwork.priority=120" - "traefik.http.routers.rspace-rwork.service=rspace-online" - - "traefik.http.routers.rspace-rcal.rule=Host(`rcal.online`)" + - "traefik.http.routers.rspace-rcal.rule=Host(`rcal.online`) || HostRegexp(`{sub:[a-z0-9-]+}.rcal.online`)" - "traefik.http.routers.rspace-rcal.entrypoints=web" - "traefik.http.routers.rspace-rcal.priority=120" - "traefik.http.routers.rspace-rcal.service=rspace-online" - - "traefik.http.routers.rspace-rtrips.rule=Host(`rtrips.online`)" + - "traefik.http.routers.rspace-rtrips.rule=Host(`rtrips.online`) || HostRegexp(`{sub:[a-z0-9-]+}.rtrips.online`)" - "traefik.http.routers.rspace-rtrips.entrypoints=web" - "traefik.http.routers.rspace-rtrips.priority=120" - "traefik.http.routers.rspace-rtrips.service=rspace-online" - - "traefik.http.routers.rspace-rwallet.rule=Host(`rwallet.online`)" + - "traefik.http.routers.rspace-rwallet.rule=Host(`rwallet.online`) || HostRegexp(`{sub:[a-z0-9-]+}.rwallet.online`)" - "traefik.http.routers.rspace-rwallet.entrypoints=web" - "traefik.http.routers.rspace-rwallet.priority=120" - "traefik.http.routers.rspace-rwallet.service=rspace-online" - - "traefik.http.routers.rspace-rdata.rule=Host(`rdata.online`)" + - "traefik.http.routers.rspace-rdata.rule=Host(`rdata.online`) || HostRegexp(`{sub:[a-z0-9-]+}.rdata.online`)" - "traefik.http.routers.rspace-rdata.entrypoints=web" - "traefik.http.routers.rspace-rdata.priority=120" - "traefik.http.routers.rspace-rdata.service=rspace-online" - - "traefik.http.routers.rspace-rnetwork.rule=Host(`rnetwork.online`)" + - "traefik.http.routers.rspace-rnetwork.rule=Host(`rnetwork.online`) || HostRegexp(`{sub:[a-z0-9-]+}.rnetwork.online`)" - "traefik.http.routers.rspace-rnetwork.entrypoints=web" - "traefik.http.routers.rspace-rnetwork.priority=120" - "traefik.http.routers.rspace-rnetwork.service=rspace-online" - - "traefik.http.routers.rspace-rtube.rule=Host(`rtube.online`)" + - "traefik.http.routers.rspace-rtube.rule=Host(`rtube.online`) || HostRegexp(`{sub:[a-z0-9-]+}.rtube.online`)" - "traefik.http.routers.rspace-rtube.entrypoints=web" - "traefik.http.routers.rspace-rtube.priority=120" - "traefik.http.routers.rspace-rtube.service=rspace-online" - - "traefik.http.routers.rspace-rmaps.rule=Host(`rmaps.online`)" + - "traefik.http.routers.rspace-rmaps.rule=Host(`rmaps.online`) || HostRegexp(`{sub:[a-z0-9-]+}.rmaps.online`)" - "traefik.http.routers.rspace-rmaps.entrypoints=web" - "traefik.http.routers.rspace-rmaps.priority=120" - "traefik.http.routers.rspace-rmaps.service=rspace-online" - - "traefik.http.routers.rspace-rnotes.rule=Host(`rnotes.online`)" + - "traefik.http.routers.rspace-rnotes.rule=Host(`rnotes.online`) || HostRegexp(`{sub:[a-z0-9-]+}.rnotes.online`)" - "traefik.http.routers.rspace-rnotes.entrypoints=web" - "traefik.http.routers.rspace-rnotes.priority=120" - "traefik.http.routers.rspace-rnotes.service=rspace-online" - - "traefik.http.routers.rspace-rfiles.rule=Host(`rfiles.online`)" + - "traefik.http.routers.rspace-rfiles.rule=Host(`rfiles.online`) || HostRegexp(`{sub:[a-z0-9-]+}.rfiles.online`)" - "traefik.http.routers.rspace-rfiles.entrypoints=web" - "traefik.http.routers.rspace-rfiles.priority=120" - "traefik.http.routers.rspace-rfiles.service=rspace-online" - - "traefik.http.routers.rspace-rphotos.rule=Host(`rphotos.online`)" + - "traefik.http.routers.rspace-rphotos.rule=Host(`rphotos.online`) || HostRegexp(`{sub:[a-z0-9-]+}.rphotos.online`)" - "traefik.http.routers.rspace-rphotos.entrypoints=web" - "traefik.http.routers.rspace-rphotos.priority=120" - "traefik.http.routers.rspace-rphotos.service=rspace-online" - - "traefik.http.routers.rspace-rinbox.rule=Host(`rinbox.online`)" + - "traefik.http.routers.rspace-rinbox.rule=Host(`rinbox.online`) || HostRegexp(`{sub:[a-z0-9-]+}.rinbox.online`)" - "traefik.http.routers.rspace-rinbox.entrypoints=web" - "traefik.http.routers.rspace-rinbox.priority=120" - "traefik.http.routers.rspace-rinbox.service=rspace-online" - - "traefik.http.routers.rspace-rcart.rule=Host(`rcart.online`)" + - "traefik.http.routers.rspace-rcart.rule=Host(`rcart.online`) || HostRegexp(`{sub:[a-z0-9-]+}.rcart.online`)" - "traefik.http.routers.rspace-rcart.entrypoints=web" - "traefik.http.routers.rspace-rcart.priority=120" - "traefik.http.routers.rspace-rcart.service=rspace-online" - - "traefik.http.routers.rspace-rsplat.rule=Host(`rsplat.online`)" + - "traefik.http.routers.rspace-rsplat.rule=Host(`rsplat.online`) || HostRegexp(`{sub:[a-z0-9-]+}.rsplat.online`)" - "traefik.http.routers.rspace-rsplat.entrypoints=web" - "traefik.http.routers.rspace-rsplat.priority=120" - "traefik.http.routers.rspace-rsplat.service=rspace-online" - - "traefik.http.routers.rspace-rswag.rule=Host(`rswag.online`)" + - "traefik.http.routers.rspace-rswag.rule=Host(`rswag.online`) || HostRegexp(`{sub:[a-z0-9-]+}.rswag.online`)" - "traefik.http.routers.rspace-rswag.entrypoints=web" - "traefik.http.routers.rspace-rswag.priority=120" - "traefik.http.routers.rspace-rswag.service=rspace-online" - - "traefik.http.routers.rspace-rsocials.rule=Host(`rsocials.online`)" + - "traefik.http.routers.rspace-rsocials.rule=Host(`rsocials.online`) || HostRegexp(`{sub:[a-z0-9-]+}.rsocials.online`)" - "traefik.http.routers.rspace-rsocials.entrypoints=web" - "traefik.http.routers.rspace-rsocials.priority=120" - "traefik.http.routers.rspace-rsocials.service=rspace-online" diff --git a/server/index.ts b/server/index.ts index 3999ebd..60bee06 100644 --- a/server/index.ts +++ b/server/index.ts @@ -982,8 +982,22 @@ const server = Bun.serve({ const hostClean = host?.split(":")[0] || ""; const subdomain = getSubdomain(host); - // ── Standalone domain → internal rewrite to module routes ── - const standaloneModuleId = domainToModule.get(hostClean); + // ── Standalone domain → 301 redirect to rspace.online ── + // Check both bare domain and subdomain variants (e.g. rnotes.online, alice.rnotes.online) + let standaloneModuleId = domainToModule.get(hostClean); + let standaloneSub: string | null = null; + if (!standaloneModuleId) { + // Check if this is a subdomain of a standalone domain (e.g. alice.rnotes.online) + const hostParts = hostClean.split("."); + if (hostParts.length >= 3) { + const baseDomain = hostParts.slice(-2).join("."); + const candidate = domainToModule.get(baseDomain); + if (candidate) { + standaloneModuleId = candidate; + standaloneSub = hostParts.slice(0, -2).join("."); + } + } + } if (standaloneModuleId) { // Self-fetch detection: landing proxy uses this User-Agent; // return 404 to break circular fetch so the generic fallback is used @@ -991,41 +1005,37 @@ const server = Bun.serve({ return new Response("Not found", { status: 404 }); } - // Static assets pass through - if (url.pathname !== "/" && !url.pathname.startsWith("/api/") && !url.pathname.startsWith("/ws/")) { - const assetPath = url.pathname.slice(1); - if (assetPath.includes(".")) { - const staticResponse = await serveStatic(assetPath); - if (staticResponse) return staticResponse; - } - } - - // Root path → redirect to rspace.online/{moduleId} landing page - if (url.pathname === "/") { - return Response.redirect(`https://rspace.online/${standaloneModuleId}`, 302); - } - - // Sub-paths: rewrite internally → /{space}/{moduleId}/... + // Determine the space and remaining path const pathParts = url.pathname.split("/").filter(Boolean); - let space = "demo"; - let suffix = ""; + let space = standaloneSub || null; // subdomain on standalone domain = space + let remainingPath = ""; - if ( - pathParts.length > 0 && - !pathParts[0].includes(".") && - pathParts[0] !== "api" && - pathParts[0] !== "ws" - ) { - space = pathParts[0]; - suffix = pathParts.length > 1 ? "/" + pathParts.slice(1).join("/") : ""; - } else if (url.pathname !== "/") { - suffix = url.pathname; + if (pathParts.length > 0 && !pathParts[0].includes(".") && + pathParts[0] !== "api" && pathParts[0] !== "ws") { + // First path segment is the space (if no subdomain already set it) + if (!space) { + space = pathParts[0]; + remainingPath = pathParts.length > 1 ? "/" + pathParts.slice(1).join("/") : ""; + } else { + remainingPath = url.pathname; + } + } else { + remainingPath = url.pathname === "/" ? "" : url.pathname; } - const rewrittenPath = `/${space}/${standaloneModuleId}${suffix}`; - const rewrittenUrl = new URL(rewrittenPath + url.search, `http://localhost:${PORT}`); - const rewrittenReq = new Request(rewrittenUrl, req); - return app.fetch(rewrittenReq); + // Build redirect URL + let redirectUrl: string; + if (space) { + // Space-qualified: alice.rnotes.online/path or rnotes.online/alice/path + // → alice.rspace.online/rnotes/path + redirectUrl = `https://${space}.rspace.online/${standaloneModuleId}${remainingPath}`; + } else { + // No space: rnotes.online/ or rnotes.online/api/... + // → rspace.online/rnotes or rspace.online/rnotes/api/... + redirectUrl = `https://rspace.online/${standaloneModuleId}${remainingPath}`; + } + if (url.search) redirectUrl += url.search; + return Response.redirect(redirectUrl, 301); } // ── WebSocket upgrade ──