feat: replace static landing page with server-rendered main landing
Replace the 177KB static Next.js landing with a server-rendered page using the same shell, theme, and rl-* CSS utilities as all module landings. Fixes the 354KB logo.png → 1.6KB favicon.png glitch across all pages. Dynamic ecosystem grid is always in sync with registered modules. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
186a1695c1
commit
32ed06d43a
|
|
@ -65,6 +65,7 @@ import { photosModule } from "../modules/photos/mod";
|
|||
import { socialsModule } from "../modules/rsocials/mod";
|
||||
import { spaces } from "./spaces";
|
||||
import { renderShell, renderModuleLanding } from "./shell";
|
||||
import { renderMainLanding } from "./landing";
|
||||
import { fetchLandingPage } from "./landing-proxy";
|
||||
import { syncServer } from "./sync-instance";
|
||||
import { loadAllDocs } from "./local-first/doc-persistence";
|
||||
|
|
@ -808,18 +809,9 @@ for (const mod of getAllModules()) {
|
|||
|
||||
// ── Page routes ──
|
||||
|
||||
// Landing page: rspace.online/ → serve original rSpace-website landing
|
||||
app.get("/", async (c) => {
|
||||
const landing = Bun.file(resolve(DIST_DIR, "landing.html"));
|
||||
if (await landing.exists()) {
|
||||
return new Response(landing, { headers: { "Content-Type": "text/html" } });
|
||||
}
|
||||
// Fallback to index.html
|
||||
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);
|
||||
// Landing page: rspace.online/ → server-rendered main landing
|
||||
app.get("/", (c) => {
|
||||
return c.html(renderMainLanding(getModuleInfoList()));
|
||||
});
|
||||
|
||||
// About/info page (full landing content)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,263 @@
|
|||
/**
|
||||
* Main landing page for rspace.online/
|
||||
*
|
||||
* Server-rendered using the same shell (header, CSS, theme) as all module
|
||||
* landing pages. Content is ported from the old static Next.js export and
|
||||
* adapted to use the shared rl-* utility classes.
|
||||
*/
|
||||
|
||||
import type { ModuleInfo } from "../shared/module";
|
||||
import { escapeHtml, escapeAttr, MODULE_LANDING_CSS, RICH_LANDING_CSS } from "./shell";
|
||||
|
||||
export function renderMainLanding(modules: ModuleInfo[]): string {
|
||||
const moduleListJSON = JSON.stringify(modules);
|
||||
const demoUrl = "https://demo.rspace.online/rspace";
|
||||
|
||||
// Build the ecosystem grid dynamically from registered modules
|
||||
const ecosystemCards = modules
|
||||
.map(
|
||||
(m) => `
|
||||
<a href="/${escapeAttr(m.id)}" class="rl-card rl-card--center" style="text-decoration:none;color:inherit">
|
||||
<div class="rl-icon-box">${m.icon}</div>
|
||||
<h3>${escapeHtml(m.name)}</h3>
|
||||
<p>${escapeHtml(m.description)}</p>
|
||||
</a>`,
|
||||
)
|
||||
.join("\n");
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🌌</text></svg>">
|
||||
<title>rSpace — Community Platform</title>
|
||||
<meta name="description" content="A collaborative, local-first community platform with 22+ interoperable tools. Design global, manufacture local.">
|
||||
<link rel="stylesheet" href="/shell.css">
|
||||
<style>${MODULE_LANDING_CSS}</style>
|
||||
<style>${RICH_LANDING_CSS}</style>
|
||||
<style>${MAIN_LANDING_CSS}</style>
|
||||
<script defer src="https://rdata.online/collect.js" data-website-id="6ee7917b-0ed7-44cb-a4c8-91037638526b"></script>
|
||||
</head>
|
||||
<body data-theme="dark">
|
||||
<header class="rstack-header" data-theme="dark">
|
||||
<div class="rstack-header__left">
|
||||
<a href="/" style="display:flex;align-items:center;margin-right:4px"><img src="/favicon.png" alt="rSpace" class="rstack-header__logo"></a>
|
||||
<rstack-app-switcher current=""></rstack-app-switcher>
|
||||
</div>
|
||||
<div class="rstack-header__center">
|
||||
<rstack-mi></rstack-mi>
|
||||
</div>
|
||||
<div class="rstack-header__right">
|
||||
<a class="rstack-header__demo-btn" href="${demoUrl}">Try Demo</a>
|
||||
<rstack-identity></rstack-identity>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Hero -->
|
||||
<div class="rl-hero">
|
||||
<span class="rl-tagline">Community Platform</span>
|
||||
<h1 class="rl-heading main-wordmark">(ou)r<span class="main-wordmark__accent">Space</span></h1>
|
||||
<p class="rl-subtitle">Remember back when the internet was cool?</p>
|
||||
<p class="rl-subtext">
|
||||
A collaborative, local-first platform with ${modules.length}+ interoperable tools.
|
||||
Own your data, run your community, connect your world — no landlords required.
|
||||
</p>
|
||||
<div class="rl-cta-row">
|
||||
<a href="${demoUrl}" class="rl-cta-primary" id="ml-primary">Try the Demo</a>
|
||||
<a href="/create-space" class="rl-cta-secondary">Create a Space</a>
|
||||
</div>
|
||||
<div class="main-badges">
|
||||
<span class="rl-badge">Local-First</span>
|
||||
<span class="rl-badge">Self-Hosted</span>
|
||||
<span class="rl-badge">Open Source</span>
|
||||
<span class="rl-badge">Offline-Ready</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Collaboration Features -->
|
||||
<section class="rl-section">
|
||||
<div class="rl-container">
|
||||
<h2 class="rl-heading" style="text-align:center">Everything Your Community Needs</h2>
|
||||
<p class="rl-subtext" style="text-align:center">
|
||||
Each tool works on its own, and they all work together.
|
||||
</p>
|
||||
<div class="rl-grid-3">
|
||||
<div class="rl-card rl-card--center">
|
||||
<div class="rl-icon-box">💰</div>
|
||||
<h3>Community Funds</h3>
|
||||
<p>Budget rivers, revenue splits, and enoughness thresholds. Transparent finances with quadratic funding built in.</p>
|
||||
</div>
|
||||
<div class="rl-card rl-card--center">
|
||||
<div class="rl-icon-box">💬</div>
|
||||
<h3>Messaging & Forum</h3>
|
||||
<p>Real-time chat, threaded discussions, and async forums. All synced across devices with CRDT magic.</p>
|
||||
</div>
|
||||
<div class="rl-card rl-card--center">
|
||||
<div class="rl-icon-box">📁</div>
|
||||
<h3>Files & Media</h3>
|
||||
<p>Upload, organize, and share with memory cards. Public links, folder trees, and metadata that travels with content.</p>
|
||||
</div>
|
||||
<div class="rl-card rl-card--center">
|
||||
<div class="rl-icon-box">🔐</div>
|
||||
<h3>Passkey Identity</h3>
|
||||
<p>One passwordless login for the entire ecosystem. EncryptID uses WebAuthn — no passwords to leak, no accounts to hack.</p>
|
||||
</div>
|
||||
<div class="rl-card rl-card--center">
|
||||
<div class="rl-icon-box">📊</div>
|
||||
<h3>Dashboards & Data</h3>
|
||||
<p>Community analytics, voting results, spatial canvases. See everything at a glance and drill into what matters.</p>
|
||||
</div>
|
||||
<div class="rl-card rl-card--center">
|
||||
<div class="rl-icon-box">🛡</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>
|
||||
</section>
|
||||
|
||||
<!-- EncryptID -->
|
||||
<section class="rl-section rl-section--alt">
|
||||
<div class="rl-container">
|
||||
<div class="rl-grid-2">
|
||||
<div>
|
||||
<span class="rl-tagline">EncryptID</span>
|
||||
<h2 class="rl-heading">One Passkey for Everything</h2>
|
||||
<p class="rl-subtext" style="margin-bottom:1.5rem">
|
||||
Sign in once with your fingerprint or device PIN. Your passkey works
|
||||
across every rApp — no passwords, no email verification loops,
|
||||
no third-party auth providers.
|
||||
</p>
|
||||
<ul class="rl-check-list">
|
||||
<li><strong>WebAuthn passkeys</strong> — phishing-resistant by default</li>
|
||||
<li><strong>Guardian recovery</strong> — 2-of-3 trusted contacts restore access</li>
|
||||
<li><strong>Device linking</strong> — QR scan adds your phone or tablet</li>
|
||||
<li><strong>One RP ID</strong> — shared across all r*.online domains</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;justify-content:center">
|
||||
<div class="rl-card" style="max-width:320px;text-align:center">
|
||||
<div class="rl-icon-box" style="margin:0 auto 1rem;font-size:2rem">🔒</div>
|
||||
<h3 style="margin-bottom:0.5rem">Passwordless Login</h3>
|
||||
<p>Touch your fingerprint sensor, tap your security key, or use your device PIN. That’s it.</p>
|
||||
</div>
|
||||
</div>
|
||||
</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’re online,
|
||||
merge automatically when you’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 “someone else is editing” 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>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Philosophy -->
|
||||
<section class="rl-section rl-section--alt">
|
||||
<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>
|
||||
<p class="rl-subtext">
|
||||
No algorithms deciding what you see. No ads. No data harvesting.
|
||||
Just tools that work for you, run by you, owned by you.
|
||||
rSpace is infrastructure for communities who refuse to rent their digital commons.
|
||||
</p>
|
||||
<div class="rl-cta-row">
|
||||
<a href="/about" class="rl-cta-secondary">Learn More</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Ecosystem Grid (dynamic) -->
|
||||
<section class="rl-section">
|
||||
<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 class="main-footer">
|
||||
<div class="rl-container" style="text-align:center">
|
||||
<p style="color:#64748b;font-size:0.85rem;margin-bottom:0.5rem">
|
||||
Built with Bun, Hono, Automerge & WebAuthn
|
||||
</p>
|
||||
<div style="display:flex;gap:1.5rem;justify-content:center;flex-wrap:wrap">
|
||||
<a href="/about" style="color:#94a3b8;font-size:0.8rem;text-decoration:none">About</a>
|
||||
<a href="/create-space" style="color:#94a3b8;font-size:0.8rem;text-decoration:none">Create a Space</a>
|
||||
<a href="https://auth.rspace.online" style="color:#94a3b8;font-size:0.8rem;text-decoration:none">EncryptID</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script type="module">
|
||||
import '/shell.js';
|
||||
document.querySelector('rstack-app-switcher')?.setModules(${moduleListJSON});
|
||||
|
||||
// Logged-in users: swap CTA to "Go to My Space"
|
||||
try {
|
||||
var raw = localStorage.getItem('encryptid_session');
|
||||
if (raw) {
|
||||
var session = JSON.parse(raw);
|
||||
if (session?.claims?.username) {
|
||||
var username = session.claims.username.toLowerCase();
|
||||
var primary = document.getElementById('ml-primary');
|
||||
if (primary) {
|
||||
primary.textContent = 'Go to My Space';
|
||||
primary.href = 'https://' + username + '.rspace.online/rspace';
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch(e) {}
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
const MAIN_LANDING_CSS = `
|
||||
/* Main landing page extras (on top of rl-* utilities) */
|
||||
.main-wordmark {
|
||||
font-size: 3.5rem;
|
||||
}
|
||||
@media (min-width: 640px) { .main-wordmark { font-size: 4.5rem; } }
|
||||
.main-wordmark__accent {
|
||||
background: linear-gradient(135deg, #14b8a6, #22d3ee);
|
||||
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
.main-badges {
|
||||
display: flex; gap: 0.5rem; justify-content: center; flex-wrap: wrap;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
.main-footer {
|
||||
border-top: 1px solid rgba(255,255,255,0.06);
|
||||
padding: 2.5rem 1.5rem;
|
||||
}
|
||||
`;
|
||||
|
|
@ -72,7 +72,7 @@ export function renderShell(opts: ShellOptions): string {
|
|||
<body data-theme="${theme}">
|
||||
<header class="rstack-header" data-theme="${theme}">
|
||||
<div class="rstack-header__left">
|
||||
<a href="/" style="display:flex;align-items:center;margin-right:4px"><img src="/logo.png" alt="rSpace" class="rstack-header__logo"></a>
|
||||
<a href="/" style="display:flex;align-items:center;margin-right:4px"><img src="/favicon.png" alt="rSpace" class="rstack-header__logo"></a>
|
||||
<rstack-app-switcher current="${escapeAttr(moduleId)}"></rstack-app-switcher>
|
||||
<rstack-space-switcher current="${escapeAttr(spaceSlug)}" name="${escapeAttr(spaceName || spaceSlug)}"></rstack-space-switcher>
|
||||
</div>
|
||||
|
|
@ -468,7 +468,7 @@ export function renderModuleLanding(opts: ModuleLandingOptions): string {
|
|||
<body data-theme="${theme}">
|
||||
<header class="rstack-header" data-theme="${theme}">
|
||||
<div class="rstack-header__left">
|
||||
<a href="/" style="display:flex;align-items:center;margin-right:4px"><img src="/logo.png" alt="rSpace" class="rstack-header__logo"></a>
|
||||
<a href="/" style="display:flex;align-items:center;margin-right:4px"><img src="/favicon.png" alt="rSpace" class="rstack-header__logo"></a>
|
||||
<rstack-app-switcher current="${escapeAttr(mod.id)}"></rstack-app-switcher>
|
||||
<a class="rstack-header__demo-btn" href="${demoUrl}">Try Demo</a>
|
||||
</div>
|
||||
|
|
@ -502,7 +502,7 @@ export function renderModuleLanding(opts: ModuleLandingOptions): string {
|
|||
</html>`;
|
||||
}
|
||||
|
||||
const MODULE_LANDING_CSS = `
|
||||
export const MODULE_LANDING_CSS = `
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
|
||||
|
|
@ -543,7 +543,7 @@ body {
|
|||
@media (max-width: 600px) { .ml-name { font-size: 2rem; } .ml-icon { font-size: 3rem; } }
|
||||
`;
|
||||
|
||||
const RICH_LANDING_CSS = `
|
||||
export const RICH_LANDING_CSS = `
|
||||
/* ── Rich Landing Page Utilities ── */
|
||||
.rl-section {
|
||||
border-top: 1px solid rgba(255,255,255,0.06);
|
||||
|
|
|
|||
Loading…
Reference in New Issue