feat(landing): overhaul homepage — interop messaging, flow cards, reduced dashboard flash

- Remove "Try Demo" header button for consistency with other pages
- Reduce hero top padding (136px → 88px)
- Replace nostalgia copy with interop-focused messaging
- Replace 6-card feature grid with 4 interop flow cards (rCal→rTasks→rChats, etc.)
- Promote ecosystem grid from section 6 to section 2
- Remove ASCII interop diagram (replaced by flow cards)
- Trim philosophy section
- Redirect non-demo unauth subdomain visitors to rspace.online/ (eliminates dashboard flash)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-04-12 16:18:38 -04:00
parent f06852dd3b
commit 58586334bf
2 changed files with 130 additions and 214 deletions

View File

@ -3566,7 +3566,11 @@ app.get("/:space", (c) => {
if (token) { if (token) {
return c.redirect(`/${space}/rspace`, 302); return c.redirect(`/${space}/rspace`, 302);
} }
return c.html(renderSpaceDashboard(space, getModuleInfoList())); // Demo space: show dashboard; all others: redirect to main landing
if (space === "demo") {
return c.html(renderSpaceDashboard(space, getModuleInfoList()));
}
return c.redirect("/", 302);
}); });
// ── WebSocket types ── // ── WebSocket types ──
@ -3981,9 +3985,13 @@ const server = Bun.serve<WSData>({
if (token) { if (token) {
return Response.redirect(`${proto}//${url.host}/rspace`, 302); return Response.redirect(`${proto}//${url.host}/rspace`, 302);
} }
return new Response(renderSpaceDashboard(subdomain, getModuleInfoList()), { // Demo space: show dashboard; all others: redirect to main landing
headers: { "Content-Type": "text/html" }, if (subdomain === "demo") {
}); return new Response(renderSpaceDashboard(subdomain, getModuleInfoList()), {
headers: { "Content-Type": "text/html" },
});
}
return Response.redirect(`${proto}//rspace.online/`, 302);
} }
// Global routes pass through without subdomain prefix // Global routes pass through without subdomain prefix

View File

@ -71,7 +71,6 @@ export function renderMainLanding(modules: ModuleInfo[]): string {
<rstack-mi></rstack-mi> <rstack-mi></rstack-mi>
</div> </div>
<div class="rstack-header__right"> <div class="rstack-header__right">
<a class="rstack-header__demo-btn" href="${demoUrl}">Try Demo</a>
<rstack-identity></rstack-identity> <rstack-identity></rstack-identity>
</div> </div>
</header> </header>
@ -79,82 +78,112 @@ export function renderMainLanding(modules: ModuleInfo[]): string {
<!-- Hero --> <!-- Hero -->
<div class="rl-hero hero-glow-wrap"> <div class="rl-hero hero-glow-wrap">
<div class="hero-glow"></div> <div class="hero-glow"></div>
<span class="rl-tagline">Community Platform</span> <span class="rl-tagline">The rStack of rApps</span>
<h1 class="rl-heading main-wordmark">(you)r<span class="main-wordmark__accent">Space</span></h1> <h1 class="rl-heading main-wordmark">(you)r<span class="main-wordmark__accent">Space</span></h1>
<p class="rl-subtitle"> <p class="rl-subtitle">
Remember back when the internet was <span class="accent-cool">cool</span>? One platform. Every tool your community needs. All talking to each other.
</p>
<p class="rl-subtext" style="font-style:italic;opacity:0.85;margin-bottom:0.75rem">
We may not have <em>My</em>Space anymore, but we have <strong>(you)rSpace</strong>.
</p> </p>
<p class="rl-subtext"> <p class="rl-subtext">
Build digital spaces to collaborate on improving your physical world. Data flows between apps automatically &mdash; no import/export rituals.
Local-first, zero-knowledge privacy, outside the walls of big tech. Local-first, encrypted, and yours.
</p> </p>
<div class="rl-cta-row"> <div class="rl-cta-row">
<a href="${demoUrl}" class="rl-cta-primary" id="ml-primary">Start (you)rSpace</a> <a href="${demoUrl}" class="rl-cta-primary" id="ml-primary">Start (you)rSpace</a>
<a href="/create-space" class="rl-cta-secondary">Create a Space</a> <a href="#ecosystem" class="rl-cta-secondary">Explore the rApps</a>
</div> </div>
<div class="main-badges"> <div class="main-badges">
<span class="rl-badge">Local-First</span> <span class="rl-badge">Local-First</span>
<span class="badge-sep" aria-hidden="true">&#9733;</span> <span class="badge-sep" aria-hidden="true">&#9733;</span>
<span class="rl-badge">Self-Hosted</span> <span class="rl-badge">Multiplayer</span>
<span class="badge-sep" aria-hidden="true">&#9733;</span>
<span class="rl-badge">Encrypted</span>
<span class="badge-sep" aria-hidden="true">&#9733;</span> <span class="badge-sep" aria-hidden="true">&#9733;</span>
<span class="rl-badge">Open Source</span> <span class="rl-badge">Open Source</span>
<span class="badge-sep" aria-hidden="true">&#9733;</span>
<span class="rl-badge">Offline-Ready</span>
</div> </div>
</div> </div>
<!-- Collaboration Features --> <!-- Interop Flow Cards -->
<section class="rl-section"> <section class="rl-section">
<div class="rl-container"> <div class="rl-container">
<h2 class="rl-heading" style="text-align:center">Everything Your Community Needs</h2> <h2 class="rl-heading" style="text-align:center">One rStack. Every Tool Connected.</h2>
<p class="rl-subtext" style="text-align:center"> <p class="rl-subtext" style="text-align:center">
Each tool works on its own, and they all work together. rApps talk to each other through one shared sync layer. Here&rsquo;s what that looks like.
</p>
<div class="flow-cards">
<div class="flow-card">
<div class="flow-card__pills">
<span class="flow-pill">&#128197; rCal</span>
<span class="flow-arrow" aria-hidden="true">&rarr;</span>
<span class="flow-pill">&#128203; rTasks</span>
<span class="flow-arrow" aria-hidden="true">&rarr;</span>
<span class="flow-pill">&#128172; rChats</span>
</div>
<p class="flow-card__desc">Schedule a meeting, auto-generates a task, notifies your space.</p>
</div>
<div class="flow-card">
<div class="flow-card__pills">
<span class="flow-pill">&#9745; rChoices</span>
<span class="flow-arrow" aria-hidden="true">&rarr;</span>
<span class="flow-pill">&#128176; rWallet</span>
</div>
<p class="flow-card__desc">Vote passes, budget allocation releases automatically.</p>
</div>
<div class="flow-card">
<div class="flow-card__pills">
<span class="flow-pill">&#128506; rMaps</span>
<span class="flow-arrow" aria-hidden="true">&rarr;</span>
<span class="flow-pill">&#128221; rDocs</span>
</div>
<p class="flow-card__desc">Pin a community location, shared doc created for that place.</p>
</div>
<div class="flow-card">
<div class="flow-card__pills">
<span class="flow-pill">&#9201; rTime</span>
<span class="flow-arrow" aria-hidden="true">&rarr;</span>
<span class="flow-pill">&#128202; rData</span>
</div>
<p class="flow-card__desc">Log commitments, analytics update in real-time.</p>
</div>
</div>
</div>
</section>
<!-- Ecosystem Grid (dynamic) promoted to section 2 -->
<section id="ecosystem" class="rl-section rl-section--alt">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">${modules.length} rApps and Growing</h2>
<p class="rl-subtext" style="text-align:center">
Each app is independent and composable. Use one, use all, mix and match.
</p>
<div class="rl-grid-4">
${ecosystemCards}
</div>
</div>
</section>
<!-- Offline-First -->
<section class="rl-section">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">Offline-First, Always</h2>
<p class="rl-subtext" style="text-align:center">
Your data lives on your device. Changes sync when you&rsquo;re online,
merge automatically when you&rsquo;re not.
</p> </p>
<div class="rl-grid-3"> <div class="rl-grid-3">
<div class="feat-card feat-card--teal"> <div class="rl-step">
<div class="feat-card__icon"> <div class="rl-step__num">1</div>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg> <h3>Local Persistence</h3>
</div> <p>Every document is stored in encrypted IndexedDB on your device. Works without internet.</p>
<h3>Community Funds</h3>
<p>Budget rivers, revenue splits, and enoughness thresholds. Transparent finances with quadratic funding built in.</p>
</div> </div>
<div class="feat-card feat-card--indigo"> <div class="rl-step">
<div class="feat-card__icon"> <div class="rl-step__num">2</div>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg> <h3>Auto-Merge CRDT</h3>
</div> <p>Automerge CRDTs resolve conflicts automatically. No &ldquo;someone else is editing&rdquo; lockouts.</p>
<h3>Messaging &amp; Forum</h3>
<p>Real-time chat, threaded discussions, and async forums. All synced across devices with CRDT magic.</p>
</div> </div>
<div class="feat-card feat-card--teal"> <div class="rl-step">
<div class="feat-card__icon"> <div class="rl-step__num">3</div>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/></svg> <h3>Incremental Sync</h3>
</div> <p>Only changed bytes travel over the wire. Reconnect after days offline and catch up in seconds.</p>
<h3>Files &amp; Media</h3>
<p>Upload, organize, and share with memory cards. Public links, folder trees, and metadata that travels with content.</p>
</div>
<div class="feat-card feat-card--indigo">
<div class="feat-card__icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
</div>
<h3>Passkey Identity</h3>
<p>One passwordless login for the entire ecosystem. EncryptID uses WebAuthn &mdash; no passwords to leak, no accounts to hack.</p>
</div>
<div class="feat-card feat-card--teal">
<div class="feat-card__icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 20V10"/><path d="M12 20V4"/><path d="M6 20v-6"/></svg>
</div>
<h3>Dashboards &amp; Data</h3>
<p>Community analytics, voting results, spatial canvases. See everything at a glance and drill into what matters.</p>
</div>
<div class="feat-card feat-card--indigo">
<div class="feat-card__icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
</div>
<h3>Privacy by Design</h3>
<p>Your data lives on your device first. End-to-end encrypted sync, per-document keys, zero-knowledge architecture.</p>
</div> </div>
</div> </div>
</div> </div>
@ -198,75 +227,14 @@ export function renderMainLanding(modules: ModuleInfo[]): string {
</div> </div>
</section> </section>
<!-- Offline-First -->
<section class="rl-section">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">Offline-First, Always</h2>
<p class="rl-subtext" style="text-align:center">
Your data lives on your device. Changes sync when you&rsquo;re online,
merge automatically when you&rsquo;re not.
</p>
<div class="rl-grid-3">
<div class="rl-step">
<div class="rl-step__num">1</div>
<h3>Local Persistence</h3>
<p>Every document is stored in encrypted IndexedDB on your device. Works without internet.</p>
</div>
<div class="rl-step">
<div class="rl-step__num">2</div>
<h3>Auto-Merge CRDT</h3>
<p>Automerge CRDTs resolve conflicts automatically. No &ldquo;someone else is editing&rdquo; lockouts.</p>
</div>
<div class="rl-step">
<div class="rl-step__num">3</div>
<h3>Incremental Sync</h3>
<p>Only changed bytes travel over the wire. Reconnect after days offline and catch up in seconds.</p>
</div>
</div>
<p class="offline-explain">
Under the hood: <strong>Automerge CRDTs</strong> model every document as a mergeable data structure.
<strong>IndexedDB</strong> persists the full state locally so you never lose work.
A <strong>Service Worker</strong> caches the app shell for instant loads &mdash; even without a network connection.
</p>
</div>
</section>
<!-- Interoperable by Design -->
<section class="rl-section rl-section--alt">
<div class="rl-container" style="max-width:820px;text-align:center">
<h2 class="rl-heading">Interoperable by Design</h2>
<p class="rl-subtext">
Every rApp reads and writes to the same sync layer.
No import/export rituals &mdash; data flows between tools automatically.
</p>
<div class="interop-diagram">
<pre class="interop-pre"><code> rDocs rCal rTasks rMaps rWallet ...
| | | | |
v v v v v
+--------------------------------------+
| Automerge Sync Layer |
| (encrypted, per-document keys) |
+--------------------------------------+
| | | | |
v v v v v
Your Your Your Your Your
Device Phone Laptop Server Backup</code></pre>
</div>
</div>
</section>
<!-- Philosophy --> <!-- Philosophy -->
<section class="rl-section"> <section class="rl-section">
<div class="rl-container" style="max-width:720px;text-align:center"> <div class="rl-container" style="max-width:720px;text-align:center">
<h2 class="rl-heading">The Internet as It Was Always Meant to Be</h2> <h2 class="rl-heading">Built for Communities, Not Corporations</h2>
<p class="rl-subtext" style="font-size:1.2rem;line-height:1.7"> <p class="rl-subtext" style="font-size:1.2rem;line-height:1.7">
No algorithms deciding what you see. No ads. No data harvesting. No algorithms deciding what you see. No ads. No data harvesting.
Just tools that work for you, run by you, owned by you. Just tools that work for you, run by you, owned by you.
</p> </p>
<p class="rl-subtext">
rSpace is infrastructure for communities who refuse to rent their digital commons
from landlords who read the mail, count the footsteps, and sell the maps.
</p>
<p class="philosophy-punch"> <p class="philosophy-punch">
Your space. Your community. Your rules. Your space. Your community. Your rules.
</p> </p>
@ -276,19 +244,6 @@ export function renderMainLanding(modules: ModuleInfo[]): string {
</div> </div>
</section> </section>
<!-- Ecosystem Grid (dynamic) -->
<section class="rl-section rl-section--alt">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">${modules.length} rApps and Growing</h2>
<p class="rl-subtext" style="text-align:center">
Each app is independent and composable. Use one, use all, mix and match.
</p>
<div class="rl-grid-4">
${ecosystemCards}
</div>
</div>
</section>
<!-- Footer --> <!-- Footer -->
<footer class="main-footer"> <footer class="main-footer">
<div class="rl-container" style="text-align:center"> <div class="rl-container" style="text-align:center">
@ -318,14 +273,12 @@ export function renderMainLanding(modules: ModuleInfo[]): string {
import '/shell.js'; import '/shell.js';
document.querySelector('rstack-app-switcher')?.setModules(${moduleListJSON}); document.querySelector('rstack-app-switcher')?.setModules(${moduleListJSON});
// Logged-in users: hide header demo btn, swap hero CTA + space switcher // Logged-in users: swap hero CTA + space switcher
try { try {
var raw = localStorage.getItem('encryptid_session'); var raw = localStorage.getItem('encryptid_session');
var sw = document.querySelector('rstack-space-switcher'); var sw = document.querySelector('rstack-space-switcher');
if (raw) { if (raw) {
var session = JSON.parse(raw); var session = JSON.parse(raw);
var hdrBtn = document.querySelector('.rstack-header__demo-btn');
if (hdrBtn) hdrBtn.setAttribute('data-hide', '');
if (session?.claims?.username) { if (session?.claims?.username) {
var username = session.claims.username.toLowerCase(); var username = session.claims.username.toLowerCase();
var primary = document.getElementById('ml-primary'); var primary = document.getElementById('ml-primary');
@ -550,6 +503,7 @@ body::before {
} }
/* ── Hero ── */ /* ── Hero ── */
.rl-hero { padding-top: 2rem; }
.hero-glow-wrap { position: relative; overflow: hidden; } .hero-glow-wrap { position: relative; overflow: hidden; }
.hero-glow { .hero-glow {
position: absolute; position: absolute;
@ -582,12 +536,6 @@ body::before {
background-clip: text; background-clip: text;
} }
.accent-cool {
color: var(--rs-accent);
font-weight: 700;
-webkit-text-fill-color: var(--rs-accent);
}
/* Badge bar with star separators */ /* Badge bar with star separators */
.main-badges { .main-badges {
display: flex; gap: 0.5rem; justify-content: center; flex-wrap: wrap; display: flex; gap: 0.5rem; justify-content: center; flex-wrap: wrap;
@ -604,43 +552,47 @@ body::before {
opacity: 0.6; opacity: 0.6;
} }
/* ── Feature Cards (SVG icons, border glow) ── */ /* ── Flow Cards (interop demos) ── */
.feat-card { .flow-cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1rem;
margin-top: 1.5rem;
}
.flow-card {
background: var(--rs-card-bg); background: var(--rs-card-bg);
border: 2px solid var(--rs-card-border); border: 2px solid var(--rs-card-border);
border-radius: 1rem; border-radius: 1rem;
padding: 1.75rem; padding: 1.5rem;
text-align: center;
transition: border-color 0.25s, box-shadow 0.25s, transform 0.2s; transition: border-color 0.25s, box-shadow 0.25s, transform 0.2s;
} }
.feat-card:hover { .flow-card:hover {
border-color: rgba(20,184,166,0.4);
box-shadow: 0 0 16px rgba(20,184,166,0.1);
transform: translateY(-2px); transform: translateY(-2px);
} }
.feat-card--teal:hover { .flow-card__pills {
border-color: rgba(20,184,166,0.5); display: flex; align-items: center; gap: 0.5rem;
box-shadow: 0 0 20px rgba(20,184,166,0.12); flex-wrap: wrap;
margin-bottom: 0.75rem;
} }
.feat-card--indigo:hover { .flow-pill {
border-color: rgba(99,102,241,0.5); font-size: 0.8rem; font-weight: 600;
box-shadow: 0 0 20px rgba(99,102,241,0.12); color: var(--rs-text-primary);
background: rgba(20,184,166,0.08);
border: 1px solid rgba(20,184,166,0.2);
border-radius: 2rem;
padding: 0.3rem 0.75rem;
white-space: nowrap;
} }
.feat-card__icon { .flow-arrow {
width: 3rem; height: 3rem; border-radius: 0.75rem; color: var(--rs-accent);
display: flex; align-items: center; justify-content: center; font-size: 1rem;
margin: 0 auto 1rem; opacity: 0.7;
font-size: 1.5rem;
} }
.feat-card--teal .feat-card__icon { .flow-card__desc {
background: rgba(20,184,166,0.12); color: #14b8a6; font-size: 0.85rem; color: var(--rs-text-secondary);
} line-height: 1.5; margin: 0;
.feat-card--indigo .feat-card__icon {
background: rgba(99,102,241,0.12); color: #6366f1;
}
.feat-card h3 {
font-size: 0.95rem; font-weight: 600; color: var(--rs-text-primary); margin-bottom: 0.5rem;
}
.feat-card p {
font-size: 0.875rem; color: var(--rs-text-secondary); line-height: 1.6;
} }
/* ── EncryptID Visual ── */ /* ── EncryptID Visual ── */
@ -658,45 +610,6 @@ body::before {
margin-top: 1rem; margin-top: 1rem;
} }
/* ── Offline-First Explainer ── */
.offline-explain {
text-align: center;
font-size: 0.85rem;
color: var(--rs-text-muted);
line-height: 1.65;
max-width: 640px;
margin: 2rem auto 0;
border-top: 1px solid var(--rs-border-subtle);
padding-top: 1.5rem;
}
.offline-explain strong {
color: var(--rs-text-secondary);
font-weight: 600;
}
/* ── Interop Diagram ── */
.interop-diagram {
margin-top: 2rem;
}
.interop-pre {
display: inline-block;
text-align: left;
font-family: "SF Mono", "Fira Code", "Cascadia Code", monospace;
font-size: 0.78rem;
line-height: 1.6;
color: var(--rs-text-secondary);
background: var(--rs-card-bg);
border: 1px solid var(--rs-card-border);
border-radius: 0.75rem;
padding: 1.5rem 2rem;
overflow-x: auto;
max-width: 100%;
box-shadow: 0 2px 12px rgba(0,0,0,0.15);
}
[data-theme="light"] .interop-pre {
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
}
/* ── Philosophy Punch ── */ /* ── Philosophy Punch ── */
.philosophy-punch { .philosophy-punch {
font-size: 1.3rem; font-size: 1.3rem;
@ -782,16 +695,11 @@ body::before {
color: #64748b; font-size: 0.75rem; margin: 0; color: #64748b; font-size: 0.75rem; margin: 0;
} }
/* ── Retro Shadow Utility ── */
.retro-shadow {
box-shadow: 2px 2px 0 var(--rs-card-border);
}
/* ── Responsive ── */ /* ── Responsive ── */
@media (max-width: 480px) { @media (max-width: 480px) {
.main-wordmark { font-size: 2.75rem; } .main-wordmark { font-size: 2.75rem; }
.hero-glow { width: 350px; height: 350px; } .hero-glow { width: 350px; height: 350px; }
.interop-pre { font-size: 0.65rem; padding: 1rem; } .flow-cards { grid-template-columns: 1fr; }
.philosophy-punch { font-size: 1.1rem; } .philosophy-punch { font-size: 1.1rem; }
.footer-quicklinks { gap: 0.75rem; } .footer-quicklinks { gap: 0.75rem; }
} }