From a732478f8513cdd8e822c99b98bcdfcba943ab4b Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Wed, 25 Feb 2026 19:22:06 -0800 Subject: [PATCH] =?UTF-8?q?feat:=20bare-domain=20module=20routing=20?= =?UTF-8?q?=E2=80=94=20rspace.online/{moduleId}=20as=20default?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit App dropdown links now go to rspace.online/r* (bare domain) instead of demo.rspace.online/r*. Only the "Try Demo" button links to the explicit demo subdomain. Server internally rewrites bare-domain module paths to /demo/{moduleId} while preserving the browser URL. Co-Authored-By: Claude Opus 4.6 --- server/index.ts | 18 +++++++++++++++++ server/shell.ts | 13 ++++++++++++- shared/url-helpers.ts | 45 ++++++++++++++++++++++++++++--------------- 3 files changed, 60 insertions(+), 16 deletions(-) diff --git a/server/index.ts b/server/index.ts index 5e9a7ed..ef3f556 100644 --- a/server/index.ts +++ b/server/index.ts @@ -779,6 +779,24 @@ const server = Bun.serve({ return app.fetch(rewrittenReq); } + // ── Bare-domain module routes: rspace.online/{moduleId} → internal rewrite ── + // When on the bare domain (no subdomain), if the first path segment is a + // known module ID, rewrite internally to /demo/{moduleId}/... so Hono's + // /:space/:moduleId routes handle it. The browser URL stays as-is. + if (!subdomain && hostClean.includes("rspace.online")) { + const pathSegments = url.pathname.split("/").filter(Boolean); + if (pathSegments.length >= 1) { + const firstSegment = pathSegments[0]; + const knownModuleIds = new Set(getAllModules().map((m) => m.id)); + if (knownModuleIds.has(firstSegment)) { + const rewrittenPath = `/demo${url.pathname}`; + const rewrittenUrl = new URL(rewrittenPath + url.search, `http://localhost:${PORT}`); + const rewrittenReq = new Request(rewrittenUrl, req); + return app.fetch(rewrittenReq); + } + } + } + // ── Hono handles everything else ── const response = await app.fetch(req); diff --git a/server/shell.ts b/server/shell.ts index 1103f27..f28671c 100644 --- a/server/shell.ts +++ b/server/shell.ts @@ -78,7 +78,7 @@ export function renderShell(opts: ShellOptions): string {
- ${spaceSlug !== "demo" ? `Try Demo` : ""} + Try Demo
@@ -96,6 +96,17 @@ export function renderShell(opts: ShellOptions): string { // Provide module list to app switcher document.querySelector('rstack-app-switcher')?.setModules(${moduleListJSON}); + // ── Bare-domain "Try Demo" button visibility ── + // On rspace.online (bare domain), the server internally rewrites to demo space, + // but we still want the "Try Demo" button visible since it links to the explicit demo subdomain. + (function() { + var host = window.location.host.split(':')[0]; + if (host === 'rspace.online' || host === 'www.rspace.online') { + var btn = document.querySelector('.rstack-header__demo-btn'); + if (btn) btn.removeAttribute('data-hide'); + } + })(); + // ── Auto-space resolution ── // Logged-in users on demo space → redirect to personal space (function() { diff --git a/shared/url-helpers.ts b/shared/url-helpers.ts index 91407f1..fa0eb1f 100644 --- a/shared/url-helpers.ts +++ b/shared/url-helpers.ts @@ -2,6 +2,7 @@ * Subdomain-aware URL helpers for rSpace navigation. * * Canonical URL pattern: {space}.rspace.online/{moduleId} + * Bare domain pattern: rspace.online/{moduleId} (implicitly demo space) * Fallback (localhost): /{space}/{moduleId} */ @@ -17,26 +18,37 @@ export function isSubdomain(): boolean { ); } +/** Detect if the current page is on the bare rspace.online domain (no subdomain) */ +export function isBareDomain(): boolean { + const host = window.location.host.split(":")[0]; + return host === "rspace.online" || host === "www.rspace.online"; +} + /** Get the current space from subdomain or path */ export function getCurrentSpace(): string { - const hostParts = window.location.host.split(":")[0].split("."); - if ( - hostParts.length >= 3 && - hostParts.slice(-2).join(".") === "rspace.online" && - !RESERVED_SUBDOMAINS.includes(hostParts[0]) - ) { - return hostParts[0]; + if (isSubdomain()) { + return window.location.host.split(":")[0].split(".")[0]; } + // Bare domain: space is implicit (demo by default, until auto-provision) + if (isBareDomain()) { + return "demo"; + } + // Path-based (localhost): /{space}/{moduleId} const pathParts = window.location.pathname.split("/").filter(Boolean); return pathParts[0] || "demo"; } -/** Get the current module from the path (works for both subdomain and path routing) */ +/** Get the current module from the path (works for subdomain, bare domain, and path routing) */ export function getCurrentModule(): string { const parts = window.location.pathname.split("/").filter(Boolean); if (isSubdomain()) { return parts[0] || "rspace"; } + // Bare domain: path is /{moduleId} + if (isBareDomain()) { + return parts[0] || "rspace"; + } + // Path-based (localhost): /{space}/{moduleId} return parts[1] || "rspace"; } @@ -45,7 +57,9 @@ export function getCurrentModule(): string { * * On subdomains: same-space links use /{moduleId}, cross-space links * switch the subdomain to {newSpace}.rspace.online/{moduleId}. - * On bare domain or localhost: uses /{space}/{moduleId}. + * On bare domain (rspace.online): stays on bare domain as /{moduleId} + * for default (demo) space, subdomain for explicit spaces. + * On localhost: uses /{space}/{moduleId}. */ export function rspaceNavUrl(space: string, moduleId: string): string { const hostParts = window.location.host.split(":")[0].split("."); @@ -64,12 +78,13 @@ export function rspaceNavUrl(space: string, moduleId: string): string { return `${window.location.protocol}//${space}.${baseDomain}/${moduleId}`; } - // Bare domain (rspace.online) or localhost → use /{space}/{moduleId} - const onRspace = - window.location.host.includes("rspace.online") && - !window.location.host.startsWith("www"); - if (onRspace) { - // Prefer subdomain routing + // Bare domain (rspace.online) + if (isBareDomain()) { + // Default space → stay on bare domain: /{moduleId} + if (space === "demo") { + return `/${moduleId}`; + } + // Explicit space → switch to subdomain return `${window.location.protocol}//${space}.rspace.online/${moduleId}`; }