feat: r-prefix module slugs, landing page, clickable rStack header

- Rename all 23 module IDs to r-prefixed slugs (canvas→rspace, notes→rnotes, etc.)
- Root rspace.online/ now serves the landing page instead of redirecting to demo
- rStack header in app switcher dropdown is now a clickable link to rstack.online
- Update all internal navigation links, badge maps, and URL helpers
- Space root redirects to /rspace instead of /canvas

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-02-25 19:04:22 -08:00
parent a2e3e2cb9c
commit 4895af19db
33 changed files with 181 additions and 172 deletions

View File

@ -235,7 +235,7 @@ export class FolkBookReader extends HTMLElement {
${this.getStyles()}
<div class="reader-container">
<div class="rapp-nav">
<a class="rapp-nav__back" href="/${window.location.pathname.split('/')[1]}/books">\u2190 Library</a>
<a class="rapp-nav__back" href="/${window.location.pathname.split('/')[1]}/rbooks">\u2190 Library</a>
<span class="rapp-nav__title">${this.escapeHtml(this._title)}</span>
${this._author ? `<span class="rapp-nav__subtitle">by ${this.escapeHtml(this._author)}</span>` : ""}
<span class="rapp-nav__meta">

View File

@ -389,7 +389,7 @@ export class FolkBookShelf extends HTMLElement {
</div>`
: `<div class="grid">
${books.map((b) => `
<a class="book-card" href="/${this._spaceSlug}/books/read/${b.slug}">
<a class="book-card" href="/${this._spaceSlug}/rbooks/read/${b.slug}">
<div class="book-cover" style="background:${b.cover_color}">
<span class="book-cover-title">${this.escapeHtml(b.title)}</span>
${b.featured ? '<span class="featured-badge">Featured</span>' : ""}
@ -553,7 +553,7 @@ export class FolkBookShelf extends HTMLElement {
}
try {
const res = await fetch(`/${this._spaceSlug}/books/api/books`, {
const res = await fetch(`/${this._spaceSlug}/rbooks/api/books`, {
method: "POST",
headers: { Authorization: `Bearer ${token}` },
body: formData,
@ -565,7 +565,7 @@ export class FolkBookShelf extends HTMLElement {
}
// Navigate to the new book
window.location.href = `/${this._spaceSlug}/books/read/${data.slug}`;
window.location.href = `/${this._spaceSlug}/rbooks/read/${data.slug}`;
} catch (e: any) {
errorEl.textContent = e.message;
errorEl.hidden = false;

View File

@ -206,7 +206,7 @@ routes.get("/", (c) => {
const spaceSlug = c.req.param("space") || "personal";
return c.html(renderShell({
title: `${spaceSlug} — Library | rSpace`,
moduleId: "books",
moduleId: "rbooks",
spaceSlug,
modules: getModuleInfoList(),
theme: "dark",
@ -229,9 +229,9 @@ routes.get("/read/:id", async (c) => {
if (rows.length === 0) {
const html = renderShell({
title: "Book not found | rSpace",
moduleId: "books",
moduleId: "rbooks",
spaceSlug,
body: `<div style="padding:3rem;text-align:center;color:#94a3b8;"><h2>Book not found</h2><p><a href="/${spaceSlug}/books" style="color:#60a5fa;">Back to library</a></p></div>`,
body: `<div style="padding:3rem;text-align:center;color:#94a3b8;"><h2>Book not found</h2><p><a href="/${spaceSlug}/rbooks" style="color:#60a5fa;">Back to library</a></p></div>`,
modules: getModuleInfoList(),
});
return c.html(html, 404);
@ -246,11 +246,11 @@ routes.get("/read/:id", async (c) => {
);
// Build the PDF URL relative to this module's mount point
const pdfUrl = `/${spaceSlug}/books/api/books/${book.slug}/pdf`;
const pdfUrl = `/${spaceSlug}/rbooks/api/books/${book.slug}/pdf`;
const html = renderShell({
title: `${book.title} | rSpace`,
moduleId: "books",
moduleId: "rbooks",
spaceSlug,
body: `
<folk-book-reader
@ -295,7 +295,7 @@ function escapeAttr(s: string): string {
// ── Module export ──
export const booksModule: RSpaceModule = {
id: "books",
id: "rbooks",
name: "rBooks",
icon: "📚",
description: "Community PDF library with flipbook reader",

View File

@ -377,7 +377,7 @@ routes.get("/", (c) => {
const space = c.req.param("space") || "demo";
return c.html(renderShell({
title: `${space} — Calendar | rSpace`,
moduleId: "cal",
moduleId: "rcal",
spaceSlug: space,
modules: getModuleInfoList(),
theme: "dark",
@ -388,7 +388,7 @@ routes.get("/", (c) => {
});
export const calModule: RSpaceModule = {
id: "cal",
id: "rcal",
name: "rCal",
icon: "\u{1F4C5}",
description: "Temporal coordination calendar with lunar, solar, and seasonal systems",

View File

@ -38,7 +38,7 @@ routes.get("/", async (c) => {
const html = renderShell({
title: `${spaceSlug} — Canvas | rSpace`,
moduleId: "canvas",
moduleId: "rspace",
spaceSlug,
body: canvasBody,
modules: getModuleInfoList(),
@ -50,7 +50,7 @@ routes.get("/", async (c) => {
});
export const canvasModule: RSpaceModule = {
id: "canvas",
id: "rspace",
name: "rSpace",
icon: "🎨",
description: "Real-time collaborative canvas",

View File

@ -443,7 +443,7 @@ routes.get("/", (c) => {
const space = c.req.param("space") || "demo";
return c.html(renderShell({
title: `Shop | rSpace`,
moduleId: "cart",
moduleId: "rcart",
spaceSlug: space,
modules: getModuleInfoList(),
theme: "dark",
@ -454,7 +454,7 @@ routes.get("/", (c) => {
});
export const cartModule: RSpaceModule = {
id: "cart",
id: "rcart",
name: "rCart",
icon: "\u{1F6D2}",
description: "Cosmolocal print-on-demand shop",

View File

@ -78,7 +78,7 @@ class FolkChoicesDashboard extends HTMLElement {
<div class="rapp-nav">
<span class="rapp-nav__title">Choices</span>
<div class="create-btns">
<a class="create-btn" href="/${this.space}/canvas" title="Open canvas to create choices">\u2795 New on Canvas</a>
<a class="create-btn" href="/${this.space}/rspace" title="Open canvas to create choices">\u2795 New on Canvas</a>
</div>
</div>
@ -96,14 +96,14 @@ class FolkChoicesDashboard extends HTMLElement {
return `<div class="empty">
<div class="empty-icon">\u2611</div>
<p>No choices in this space yet.</p>
<p>Open the <a href="/${this.space}/canvas" style="color:#818cf8">canvas</a> and use the Poll, Rank, or Spider buttons to create one.</p>
<p>Open the <a href="/${this.space}/rspace" style="color:#818cf8">canvas</a> and use the Poll, Rank, or Spider buttons to create one.</p>
</div>`;
}
private renderGrid(icons: Record<string, string>, labels: Record<string, string>): string {
return `<div class="grid">
${this.choices.map((ch) => `
<a class="card" href="/${this.space}/canvas">
<a class="card" href="/${this.space}/rspace">
<div class="card-icon">${icons[ch.type] || "\u2611"}</div>
<div class="card-type">${labels[ch.type] || ch.type}</div>
<h3 class="card-title">${this.esc(ch.title)}</h3>

View File

@ -50,7 +50,7 @@ routes.get("/", (c) => {
const spaceSlug = c.req.param("space") || "demo";
return c.html(renderShell({
title: `${spaceSlug} — Choices | rSpace`,
moduleId: "choices",
moduleId: "rchoices",
spaceSlug,
modules: getModuleInfoList(),
theme: "dark",
@ -61,7 +61,7 @@ routes.get("/", (c) => {
});
export const choicesModule: RSpaceModule = {
id: "choices",
id: "rchoices",
name: "rChoices",
icon: "☑",
description: "Polls, rankings, and multi-criteria scoring",

View File

@ -122,7 +122,7 @@ routes.get("/", (c) => {
const space = c.req.param("space") || "demo";
return c.html(renderShell({
title: `${space} — Data | rSpace`,
moduleId: "data",
moduleId: "rdata",
spaceSlug: space,
modules: getModuleInfoList(),
theme: "dark",
@ -133,7 +133,7 @@ routes.get("/", (c) => {
});
export const dataModule: RSpaceModule = {
id: "data",
id: "rdata",
name: "rData",
icon: "\u{1F4CA}",
description: "Privacy-first analytics for the r* ecosystem",

View File

@ -368,7 +368,7 @@ routes.get("/", (c) => {
const spaceSlug = c.req.param("space") || "demo";
return c.html(renderShell({
title: `${spaceSlug} — Files | rSpace`,
moduleId: "files",
moduleId: "rfiles",
spaceSlug,
modules: getModuleInfoList(),
theme: "dark",
@ -379,7 +379,7 @@ routes.get("/", (c) => {
});
export const filesModule: RSpaceModule = {
id: "files",
id: "rfiles",
name: "rFiles",
icon: "\uD83D\uDCC1",
description: "File sharing, share links, and memory cards",

View File

@ -159,7 +159,7 @@ routes.get("/", (c) => {
const spaceSlug = c.req.param("space") || "demo";
return c.html(renderShell({
title: `${spaceSlug} — Forum | rSpace`,
moduleId: "forum",
moduleId: "rforum",
spaceSlug,
modules: getModuleInfoList(),
theme: "dark",
@ -170,7 +170,7 @@ routes.get("/", (c) => {
});
export const forumModule: RSpaceModule = {
id: "forum",
id: "rforum",
name: "rForum",
icon: "\uD83D\uDCAC",
description: "Deploy and manage Discourse forums",

View File

@ -198,7 +198,7 @@ routes.get("/", (c) => {
const spaceSlug = c.req.param("space") || "demo";
return c.html(renderShell({
title: `rFunds — TBFF Flow Funding | rSpace`,
moduleId: "funds",
moduleId: "rfunds",
spaceSlug,
modules: getModuleInfoList(),
theme: "dark",
@ -213,7 +213,7 @@ routes.get("/demo", (c) => {
const spaceSlug = c.req.param("space") || "demo";
return c.html(renderShell({
title: `TBFF Demo — rFunds | rSpace`,
moduleId: "funds",
moduleId: "rfunds",
spaceSlug,
modules: getModuleInfoList(),
theme: "dark",
@ -229,7 +229,7 @@ routes.get("/flow/:flowId", (c) => {
const flowId = c.req.param("flowId");
return c.html(renderShell({
title: `Flow — rFunds | rSpace`,
moduleId: "funds",
moduleId: "rfunds",
spaceSlug,
modules: getModuleInfoList(),
theme: "dark",
@ -240,7 +240,7 @@ routes.get("/flow/:flowId", (c) => {
});
export const fundsModule: RSpaceModule = {
id: "funds",
id: "rfunds",
name: "rFunds",
icon: "\uD83C\uDF0A",
description: "Budget flows, river visualization, and treasury management",

View File

@ -532,7 +532,7 @@ routes.get("/", (c) => {
const space = c.req.param("space") || "demo";
return c.html(renderShell({
title: `${space} — Inbox | rSpace`,
moduleId: "inbox",
moduleId: "rinbox",
spaceSlug: space,
modules: getModuleInfoList(),
theme: "dark",
@ -543,7 +543,7 @@ routes.get("/", (c) => {
});
export const inboxModule: RSpaceModule = {
id: "inbox",
id: "rinbox",
name: "rInbox",
icon: "\u{1F4E8}",
description: "Collaborative email with multisig approval",

View File

@ -135,7 +135,7 @@ routes.get("/", (c) => {
const space = c.req.param("space") || "demo";
return c.html(renderShell({
title: `${space} — Maps | rSpace`,
moduleId: "maps",
moduleId: "rmaps",
spaceSlug: space,
modules: getModuleInfoList(),
theme: "dark",
@ -151,7 +151,7 @@ routes.get("/:room", (c) => {
const room = c.req.param("room");
return c.html(renderShell({
title: `${room} — Maps | rSpace`,
moduleId: "maps",
moduleId: "rmaps",
spaceSlug: space,
modules: getModuleInfoList(),
theme: "dark",
@ -162,7 +162,7 @@ routes.get("/:room", (c) => {
});
export const mapsModule: RSpaceModule = {
id: "maps",
id: "rmaps",
name: "rMaps",
icon: "\u{1F5FA}",
description: "Real-time collaborative location sharing and indoor/outdoor maps",

View File

@ -218,7 +218,7 @@ routes.get("/", (c) => {
const space = c.req.param("space") || "demo";
return c.html(renderShell({
title: `${space} — Network | rSpace`,
moduleId: "network",
moduleId: "rnetwork",
spaceSlug: space,
modules: getModuleInfoList(),
theme: "dark",
@ -229,7 +229,7 @@ routes.get("/", (c) => {
});
export const networkModule: RSpaceModule = {
id: "network",
id: "rnetwork",
name: "rNetwork",
icon: "\u{1F310}",
description: "Community relationship graph visualization with CRM sync",

View File

@ -363,7 +363,7 @@ routes.get("/", (c) => {
const space = c.req.param("space") || "demo";
return c.html(renderShell({
title: `${space} — Notes | rSpace`,
moduleId: "notes",
moduleId: "rnotes",
spaceSlug: space,
modules: getModuleInfoList(),
theme: "dark",
@ -374,7 +374,7 @@ routes.get("/", (c) => {
});
export const notesModule: RSpaceModule = {
id: "notes",
id: "rnotes",
name: "rNotes",
icon: "\u{1F4DD}",
description: "Notebooks with rich-text notes, voice transcription, and collaboration",

View File

@ -110,7 +110,7 @@ routes.get("/", (c) => {
const spaceSlug = c.req.param("space") || "demo";
return c.html(renderShell({
title: `${spaceSlug} — Photos | rSpace`,
moduleId: "photos",
moduleId: "rphotos",
spaceSlug,
modules: getModuleInfoList(),
theme: "dark",
@ -121,7 +121,7 @@ routes.get("/", (c) => {
});
export const photosModule: RSpaceModule = {
id: "photos",
id: "rphotos",
name: "rPhotos",
icon: "📸",
description: "Community photo commons",
@ -129,7 +129,7 @@ export const photosModule: RSpaceModule = {
standaloneDomain: "rphotos.online",
feeds: [
{
id: "photos",
id: "rphotos",
name: "Recent Photos",
kind: "data",
description: "Stream of recently uploaded photos",

View File

@ -350,7 +350,7 @@ routes.get("/", (c) => {
const space = c.req.param("space") || "demo";
return c.html(renderShell({
title: `Providers | rSpace`,
moduleId: "providers",
moduleId: "rproviders",
spaceSlug: space,
modules: getModuleInfoList(),
theme: "dark",
@ -361,7 +361,7 @@ routes.get("/", (c) => {
});
export const providersModule: RSpaceModule = {
id: "providers",
id: "rproviders",
name: "rProviders",
icon: "\u{1F3ED}",
description: "Local provider directory for cosmolocal production",

View File

@ -323,7 +323,7 @@ routes.get("/", (c) => {
const spaceSlug = c.req.param("space") || "personal";
return c.html(renderShell({
title: `${spaceSlug} — rPubs Editor | rSpace`,
moduleId: "pubs",
moduleId: "rpubs",
spaceSlug,
modules: getModuleInfoList(),
theme: "dark",
@ -336,7 +336,7 @@ routes.get("/", (c) => {
// ── Module export ──
export const pubsModule: RSpaceModule = {
id: "pubs",
id: "rpubs",
name: "rPubs",
icon: "📖",
description: "Drop in a document, get a pocket book",

View File

@ -80,7 +80,7 @@ export class FolkSplatViewer extends HTMLElement {
const status = s.processing_status || "ready";
const isReady = status === "ready";
const tag = isReady ? "a" : "div";
const href = isReady ? ` href="/${this._spaceSlug}/splat/view/${s.slug}"` : "";
const href = isReady ? ` href="/${this._spaceSlug}/rsplat/view/${s.slug}"` : "";
const statusClass = !isReady ? ` splat-card--${status}` : "";
let overlay = "";
@ -259,7 +259,7 @@ export class FolkSplatViewer extends HTMLElement {
try {
const token = localStorage.getItem("encryptid_token") || "";
const res = await fetch(`/${this._spaceSlug}/splat/api/splats`, {
const res = await fetch(`/${this._spaceSlug}/rsplat/api/splats`, {
method: "POST",
headers: token ? { Authorization: `Bearer ${token}` } : {},
body: formData,
@ -287,7 +287,7 @@ export class FolkSplatViewer extends HTMLElement {
const splat = await res.json() as SplatItem;
status.textContent = "Uploaded!";
setTimeout(() => {
window.location.href = `/${this._spaceSlug}/splat/view/${splat.slug}`;
window.location.href = `/${this._spaceSlug}/rsplat/view/${splat.slug}`;
}, 500);
} catch (e) {
status.textContent = "Network error";
@ -347,7 +347,7 @@ export class FolkSplatViewer extends HTMLElement {
try {
const token = localStorage.getItem("encryptid_token") || "";
const res = await fetch(`/${this._spaceSlug}/splat/api/splats/from-media`, {
const res = await fetch(`/${this._spaceSlug}/rsplat/api/splats/from-media`, {
method: "POST",
headers: token ? { Authorization: `Bearer ${token}` } : {},
body: formData,
@ -374,7 +374,7 @@ export class FolkSplatViewer extends HTMLElement {
status.textContent = "Uploaded! Queued for processing.";
setTimeout(() => {
window.location.href = `/${this._spaceSlug}/splat`;
window.location.href = `/${this._spaceSlug}/rsplat`;
}, 1000);
} catch (e) {
status.textContent = "Network error";
@ -393,7 +393,7 @@ export class FolkSplatViewer extends HTMLElement {
<div class="splat-loading__text">Loading splat...</div>
</div>
<div class="splat-viewer__controls">
<a class="splat-viewer__back" href="/${this._spaceSlug}/splat"> Gallery</a>
<a class="splat-viewer__back" href="/${this._spaceSlug}/rsplat"> Gallery</a>
</div>
${this._splatTitle ? `
<div class="splat-viewer__info">

View File

@ -429,7 +429,7 @@ routes.get("/", async (c) => {
const html = renderShell({
title: `${spaceSlug} — rSplat | rSpace`,
moduleId: "splat",
moduleId: "rsplat",
spaceSlug,
body: `<folk-splat-viewer id="gallery" mode="gallery"></folk-splat-viewer>`,
modules: getModuleInfoList(),
@ -464,9 +464,9 @@ routes.get("/view/:id", async (c) => {
if (rows.length === 0) {
const html = renderShell({
title: "Splat not found | rSpace",
moduleId: "splat",
moduleId: "rsplat",
spaceSlug,
body: `<div style="padding:3rem;text-align:center;color:#94a3b8;"><h2>Splat not found</h2><p><a href="/${spaceSlug}/splat" style="color:#818cf8;">Back to gallery</a></p></div>`,
body: `<div style="padding:3rem;text-align:center;color:#94a3b8;"><h2>Splat not found</h2><p><a href="/${spaceSlug}/rsplat" style="color:#818cf8;">Back to gallery</a></p></div>`,
modules: getModuleInfoList(),
theme: "dark",
});
@ -481,11 +481,11 @@ routes.get("/view/:id", async (c) => {
[splat.id]
);
const fileUrl = `/${spaceSlug}/splat/api/splats/${splat.slug}/${splat.slug}.${splat.file_format}`;
const fileUrl = `/${spaceSlug}/rsplat/api/splats/${splat.slug}/${splat.slug}.${splat.file_format}`;
const html = renderShell({
title: `${splat.title} | rSplat`,
moduleId: "splat",
moduleId: "rsplat",
spaceSlug,
body: `
<folk-splat-viewer
@ -534,7 +534,7 @@ async function initDB(): Promise<void> {
// ── Module export ──
export const splatModule: RSpaceModule = {
id: "splat",
id: "rsplat",
name: "rSplat",
icon: "🔮",
description: "3D Gaussian splat viewer",

View File

@ -230,7 +230,7 @@ routes.get("/", (c) => {
const space = c.req.param("space") || "demo";
return c.html(renderShell({
title: `Swag Designer | rSpace`,
moduleId: "swag",
moduleId: "rswag",
spaceSlug: space,
modules: getModuleInfoList(),
theme: "dark",
@ -241,7 +241,7 @@ routes.get("/", (c) => {
});
export const swagModule: RSpaceModule = {
id: "swag",
id: "rswag",
name: "rSwag",
icon: "\u{1F3A8}",
description: "Design print-ready swag: stickers, posters, tees",

View File

@ -240,7 +240,7 @@ routes.get("/routes", (c) => {
const space = c.req.param("space") || "demo";
return c.html(renderShell({
title: `${space} — Route Planner | rTrips`,
moduleId: "trips",
moduleId: "rtrips",
spaceSlug: space,
modules: getModuleInfoList(),
theme: "dark",
@ -255,7 +255,7 @@ routes.get("/", (c) => {
const space = c.req.param("space") || "demo";
return c.html(renderShell({
title: `${space} — Trips | rSpace`,
moduleId: "trips",
moduleId: "rtrips",
spaceSlug: space,
modules: getModuleInfoList(),
theme: "dark",
@ -266,7 +266,7 @@ routes.get("/", (c) => {
});
export const tripsModule: RSpaceModule = {
id: "trips",
id: "rtrips",
name: "rTrips",
icon: "\u{2708}\u{FE0F}",
description: "Collaborative trip planner with itinerary, bookings, and expense splitting",

View File

@ -193,7 +193,7 @@ routes.get("/", (c) => {
const space = c.req.param("space") || "demo";
return c.html(renderShell({
title: `${space} — Tube | rSpace`,
moduleId: "tube",
moduleId: "rtube",
spaceSlug: space,
modules: getModuleInfoList(),
theme: "dark",
@ -204,7 +204,7 @@ routes.get("/", (c) => {
});
export const tubeModule: RSpaceModule = {
id: "tube",
id: "rtube",
name: "rTube",
icon: "\u{1F3AC}",
description: "Community video hosting & live streaming",

View File

@ -329,7 +329,7 @@ routes.get("/", (c) => {
const space = c.req.param("space") || "demo";
return c.html(renderShell({
title: `${space} — Vote | rSpace`,
moduleId: "vote",
moduleId: "rvote",
spaceSlug: space,
modules: getModuleInfoList(),
theme: "dark",
@ -340,7 +340,7 @@ routes.get("/", (c) => {
});
export const voteModule: RSpaceModule = {
id: "vote",
id: "rvote",
name: "rVote",
icon: "\u{1F5F3}",
description: "Conviction voting engine for collaborative governance",

View File

@ -96,7 +96,7 @@ routes.get("/", (c) => {
const spaceSlug = c.req.param("space") || "demo";
return c.html(renderShell({
title: `${spaceSlug} — Wallet | rSpace`,
moduleId: "wallet",
moduleId: "rwallet",
spaceSlug,
modules: getModuleInfoList(),
theme: "dark",
@ -107,7 +107,7 @@ routes.get("/", (c) => {
});
export const walletModule: RSpaceModule = {
id: "wallet",
id: "rwallet",
name: "rWallet",
icon: "\uD83D\uDCB0",
description: "Multichain Safe wallet visualization and treasury management",

View File

@ -219,7 +219,7 @@ routes.get("/", (c) => {
const space = c.req.param("space") || "demo";
return c.html(renderShell({
title: `${space} — Work | rSpace`,
moduleId: "work",
moduleId: "rwork",
spaceSlug: space,
modules: getModuleInfoList(),
theme: "dark",
@ -230,7 +230,7 @@ routes.get("/", (c) => {
});
export const workModule: RSpaceModule = {
id: "work",
id: "rwork",
name: "rWork",
icon: "\u{1F4CB}",
description: "Kanban workspace boards for collaborative task management",

View File

@ -209,7 +209,7 @@ function generateFallbackResponse(
}
if (q.includes("help") || q.includes("what can")) {
return `rSpace has ${modules.length} apps you can use. Some popular ones: **rSpace** (canvas), **rNotes** (notes), **rChat** (messaging), **rFunds** (community funding), and **rVote** (governance). What would you like to explore?`;
return `rSpace has ${modules.length} apps you can use. Some popular ones: **rSpace** (spatial canvas), **rNotes** (notes), **rChat** (messaging), **rFunds** (community funding), and **rVote** (governance). What would you like to explore?`;
}
if (q.includes("search") || q.includes("find")) {
@ -474,8 +474,14 @@ for (const mod of getAllModules()) {
// ── Page routes ──
// Landing page: rspace.online/ → redirect to demo canvas (overlay shows there)
app.get("/", (c) => c.redirect("/demo/canvas", 302));
// Landing page: rspace.online/ → serve marketing/info page
app.get("/", async (c) => {
const file = Bun.file(resolve(DIST_DIR, "index.html"));
if (await file.exists()) {
return new Response(file, { headers: { "Content-Type": "text/html" } });
}
return c.text("rSpace", 200);
});
// About/info page (full landing content)
app.get("/about", async (c) => {
@ -507,12 +513,12 @@ app.get("/admin", async (c) => {
return c.text("Admin", 200);
});
// Space root: /:space → redirect to /:space/canvas
// Space root: /:space → redirect to /:space/rspace
app.get("/:space", (c) => {
const space = c.req.param("space");
// Don't redirect for static file paths
if (space.includes(".")) return c.notFound();
return c.redirect(`/${space}/canvas`);
return c.redirect(`/${space}/rspace`);
});
// ── WebSocket types ──
@ -743,9 +749,9 @@ const server = Bun.serve<WSData>({
if (subdomain) {
const pathSegments = url.pathname.split("/").filter(Boolean);
// Root: redirect to default module (canvas)
// Root: redirect to default module (rspace)
if (pathSegments.length === 0) {
return Response.redirect(`${url.protocol}//${host}/canvas`, 302);
return Response.redirect(`${url.protocol}//${host}/rspace`, 302);
}
// Global routes pass through without subdomain prefix

View File

@ -19,76 +19,76 @@ export interface AppSwitcherModule {
// Pastel badge abbreviations & colors for each module
const MODULE_BADGES: Record<string, { badge: string; color: string }> = {
// Creating
canvas: { badge: "rS", color: "#5eead4" }, // teal-300
notes: { badge: "rN", color: "#fcd34d" }, // amber-300
pubs: { badge: "rP", color: "#fda4af" }, // rose-300
swag: { badge: "rSw", color: "#fda4af" }, // rose-300
splat: { badge: "r3", color: "#d8b4fe" }, // purple-300
rspace: { badge: "rS", color: "#5eead4" }, // teal-300
rnotes: { badge: "rN", color: "#fcd34d" }, // amber-300
rpubs: { badge: "rP", color: "#fda4af" }, // rose-300
rswag: { badge: "rSw", color: "#fda4af" }, // rose-300
rsplat: { badge: "r3", color: "#d8b4fe" }, // purple-300
// Planning
cal: { badge: "rC", color: "#7dd3fc" }, // sky-300
trips: { badge: "rT", color: "#6ee7b7" }, // emerald-300
maps: { badge: "rM", color: "#86efac" }, // green-300
rcal: { badge: "rC", color: "#7dd3fc" }, // sky-300
rtrips: { badge: "rT", color: "#6ee7b7" }, // emerald-300
rmaps: { badge: "rM", color: "#86efac" }, // green-300
// Communicating
chats: { badge: "rCh", color: "#6ee7b7" }, // emerald-200
inbox: { badge: "rI", color: "#a5b4fc" }, // indigo-300
mail: { badge: "rMa", color: "#93c5fd" }, // blue-200
forum: { badge: "rFo", color: "#fcd34d" }, // amber-200
rchats: { badge: "rCh", color: "#6ee7b7" }, // emerald-200
rinbox: { badge: "rI", color: "#a5b4fc" }, // indigo-300
rmail: { badge: "rMa", color: "#93c5fd" }, // blue-200
rforum: { badge: "rFo", color: "#fcd34d" }, // amber-200
// Deciding
choices: { badge: "rCo", color: "#f0abfc" }, // fuchsia-300
vote: { badge: "rV", color: "#c4b5fd" }, // violet-300
rchoices: { badge: "rCo", color: "#f0abfc" }, // fuchsia-300
rvote: { badge: "rV", color: "#c4b5fd" }, // violet-300
// Funding & Commerce
funds: { badge: "rF", color: "#bef264" }, // lime-300
wallet: { badge: "rW", color: "#fde047" }, // yellow-300
cart: { badge: "rCt", color: "#fdba74" }, // orange-300
auctions: { badge: "rA", color: "#fca5a5" }, // red-300
providers: { badge: "rPr", color: "#fdba74" }, // orange-300
tube: { badge: "rTu", color: "#f9a8d4" }, // pink-300
rfunds: { badge: "rF", color: "#bef264" }, // lime-300
rwallet: { badge: "rW", color: "#fde047" }, // yellow-300
rcart: { badge: "rCt", color: "#fdba74" }, // orange-300
rauctions: { badge: "rA", color: "#fca5a5" }, // red-300
rproviders: { badge: "rPr", color: "#fdba74" }, // orange-300
rtube: { badge: "rTu", color: "#f9a8d4" }, // pink-300
// Sharing
photos: { badge: "rPh", color: "#f9a8d4" }, // pink-200
network: { badge: "rNe", color: "#93c5fd" }, // blue-300
socials: { badge: "rSo", color: "#7dd3fc" }, // sky-200
files: { badge: "rFi", color: "#67e8f9" }, // cyan-300
books: { badge: "rB", color: "#fda4af" }, // rose-300
rphotos: { badge: "rPh", color: "#f9a8d4" }, // pink-200
rnetwork: { badge: "rNe", color: "#93c5fd" }, // blue-300
rsocials: { badge: "rSo", color: "#7dd3fc" }, // sky-200
rfiles: { badge: "rFi", color: "#67e8f9" }, // cyan-300
rbooks: { badge: "rB", color: "#fda4af" }, // rose-300
// Observing
data: { badge: "rD", color: "#d8b4fe" }, // purple-300
rdata: { badge: "rD", color: "#d8b4fe" }, // purple-300
// Work & Productivity
work: { badge: "rWo", color: "#cbd5e1" }, // slate-300
rwork: { badge: "rWo", color: "#cbd5e1" }, // slate-300
// Identity & Infrastructure
ids: { badge: "rId", color: "#6ee7b7" }, // emerald-300
stack: { badge: "r*", color: "" }, // gradient (handled separately)
rids: { badge: "rId", color: "#6ee7b7" }, // emerald-300
rstack: { badge: "r*", color: "" }, // gradient (handled separately)
};
// Category definitions for the rApp dropdown (display-only grouping)
const MODULE_CATEGORIES: Record<string, string> = {
canvas: "Creating",
notes: "Creating",
pubs: "Creating",
tube: "Creating",
swag: "Creating",
splat: "Creating",
cal: "Planning",
trips: "Planning",
maps: "Planning",
chats: "Communicating",
inbox: "Communicating",
mail: "Communicating",
forum: "Communicating",
choices: "Deciding",
vote: "Deciding",
funds: "Funding & Commerce",
wallet: "Funding & Commerce",
cart: "Funding & Commerce",
auctions: "Funding & Commerce",
providers: "Funding & Commerce",
photos: "Sharing",
network: "Sharing",
socials: "Sharing",
files: "Sharing",
books: "Sharing",
data: "Observing",
work: "Work & Productivity",
ids: "Identity & Infrastructure",
stack: "Identity & Infrastructure",
rspace: "Creating",
rnotes: "Creating",
rpubs: "Creating",
rtube: "Creating",
rswag: "Creating",
rsplat: "Creating",
rcal: "Planning",
rtrips: "Planning",
rmaps: "Planning",
rchats: "Communicating",
rinbox: "Communicating",
rmail: "Communicating",
rforum: "Communicating",
rchoices: "Deciding",
rvote: "Deciding",
rfunds: "Funding & Commerce",
rwallet: "Funding & Commerce",
rcart: "Funding & Commerce",
rauctions: "Funding & Commerce",
rproviders: "Funding & Commerce",
rphotos: "Sharing",
rnetwork: "Sharing",
rsocials: "Sharing",
rfiles: "Sharing",
rbooks: "Sharing",
rdata: "Observing",
rwork: "Work & Productivity",
rids: "Identity & Infrastructure",
rstack: "Identity & Infrastructure",
};
const CATEGORY_ORDER = [
@ -150,15 +150,15 @@ export class RStackAppSwitcher extends HTMLElement {
}
}
// rStack header
// rStack header (clickable)
let html = `
<div class="rstack-header">
<a class="rstack-header" href="https://rstack.online" target="_blank" rel="noopener">
<span class="rstack-badge">r*</span>
<div class="rstack-info">
<span class="rstack-title">rStack</span>
<span class="rstack-subtitle">Self-hosted community app suite</span>
</div>
</div>
</a>
`;
for (const cat of CATEGORY_ORDER) {
@ -304,10 +304,13 @@ const STYLES = `
}
/* rStack header */
.rstack-header {
a.rstack-header {
display: flex; align-items: center; gap: 10px;
padding: 12px 14px; border-bottom: 1px solid rgba(128,128,128,0.15);
text-decoration: none; color: inherit; cursor: pointer;
transition: background 0.12s;
}
a.rstack-header:hover { background: rgba(255,255,255,0.05); }
.rstack-badge {
display: flex; align-items: center; justify-content: center;
width: 28px; height: 28px; border-radius: 8px;

View File

@ -146,7 +146,7 @@ function _getCurrentSpace(): string {
}
function _getCurrentModule(): string {
const parts = window.location.pathname.split("/").filter(Boolean);
return _isSubdomain() ? (parts[0] || "canvas") : (parts[1] || "canvas");
return _isSubdomain() ? (parts[0] || "rspace") : (parts[1] || "rspace");
}
function _navUrl(space: string, moduleId: string): string {
const h = window.location.host.split(":")[0].split(".");

View File

@ -25,33 +25,33 @@ import { FLOW_COLORS, FLOW_LABELS } from "../../lib/layer-types";
// Re-export badge info so the tab bar can show module colors
const MODULE_BADGES: Record<string, { badge: string; color: string }> = {
canvas: { badge: "rS", color: "#5eead4" },
notes: { badge: "rN", color: "#fcd34d" },
pubs: { badge: "rP", color: "#fda4af" },
swag: { badge: "rSw", color: "#fda4af" },
splat: { badge: "r3", color: "#d8b4fe" },
cal: { badge: "rC", color: "#7dd3fc" },
trips: { badge: "rT", color: "#6ee7b7" },
maps: { badge: "rM", color: "#86efac" },
chats: { badge: "rCh", color: "#6ee7b7" },
inbox: { badge: "rI", color: "#a5b4fc" },
mail: { badge: "rMa", color: "#93c5fd" },
forum: { badge: "rFo", color: "#fcd34d" },
choices: { badge: "rCo", color: "#f0abfc" },
vote: { badge: "rV", color: "#c4b5fd" },
funds: { badge: "rF", color: "#bef264" },
wallet: { badge: "rW", color: "#fde047" },
cart: { badge: "rCt", color: "#fdba74" },
auctions: { badge: "rA", color: "#fca5a5" },
providers: { badge: "rPr", color: "#fdba74" },
tube: { badge: "rTu", color: "#f9a8d4" },
photos: { badge: "rPh", color: "#f9a8d4" },
network: { badge: "rNe", color: "#93c5fd" },
socials: { badge: "rSo", color: "#7dd3fc" },
files: { badge: "rFi", color: "#67e8f9" },
books: { badge: "rB", color: "#fda4af" },
data: { badge: "rD", color: "#d8b4fe" },
work: { badge: "rWo", color: "#cbd5e1" },
rspace: { badge: "rS", color: "#5eead4" },
rnotes: { badge: "rN", color: "#fcd34d" },
rpubs: { badge: "rP", color: "#fda4af" },
rswag: { badge: "rSw", color: "#fda4af" },
rsplat: { badge: "r3", color: "#d8b4fe" },
rcal: { badge: "rC", color: "#7dd3fc" },
rtrips: { badge: "rT", color: "#6ee7b7" },
rmaps: { badge: "rM", color: "#86efac" },
rchats: { badge: "rCh", color: "#6ee7b7" },
rinbox: { badge: "rI", color: "#a5b4fc" },
rmail: { badge: "rMa", color: "#93c5fd" },
rforum: { badge: "rFo", color: "#fcd34d" },
rchoices: { badge: "rCo", color: "#f0abfc" },
rvote: { badge: "rV", color: "#c4b5fd" },
rfunds: { badge: "rF", color: "#bef264" },
rwallet: { badge: "rW", color: "#fde047" },
rcart: { badge: "rCt", color: "#fdba74" },
rauctions: { badge: "rA", color: "#fca5a5" },
rproviders: { badge: "rPr", color: "#fdba74" },
rtube: { badge: "rTu", color: "#f9a8d4" },
rphotos: { badge: "rPh", color: "#f9a8d4" },
rnetwork: { badge: "rNe", color: "#93c5fd" },
rsocials: { badge: "rSo", color: "#7dd3fc" },
rfiles: { badge: "rFi", color: "#67e8f9" },
rbooks: { badge: "rB", color: "#fda4af" },
rdata: { badge: "rD", color: "#d8b4fe" },
rwork: { badge: "rWo", color: "#cbd5e1" },
};
export class RStackTabBar extends HTMLElement {

View File

@ -35,9 +35,9 @@ export function getCurrentSpace(): string {
export function getCurrentModule(): string {
const parts = window.location.pathname.split("/").filter(Boolean);
if (isSubdomain()) {
return parts[0] || "canvas";
return parts[0] || "rspace";
}
return parts[1] || "canvas";
return parts[1] || "rspace";
}
/**

View File

@ -379,7 +379,7 @@
<div class="cta-buttons">
<a href="/create-space" class="cta-primary" id="cta-primary">Create a Space</a>
<a href="/demo/canvas" class="cta-secondary" id="cta-demo">Try the Demo</a>
<a href="/demo/rspace" class="cta-secondary" id="cta-demo">Try the Demo</a>
</div>
<div class="features">