Compare commits

..

2 Commits

Author SHA1 Message Date
Jeff Emmett 9b9170c90b Merge branch 'dev'
CI/CD / deploy (push) Failing after 2m9s Details
2026-04-16 12:47:14 -04:00
Jeff Emmett c5a58e1908 feat(rfeeds): add landing page and standalone domain (rfeeds.online)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 12:47:05 -04:00
2 changed files with 146 additions and 0 deletions

143
modules/rfeeds/landing.ts Normal file
View File

@ -0,0 +1,143 @@
/**
* rFeeds landing page body.
* Returned by landingPage() in the module export;
* the shell wraps it with header, CSS, and analytics.
*/
export function renderLanding(): string {
return `
<!-- Hero -->
<div class="rl-hero">
<span class="rl-tagline">rFeeds</span>
<h1 class="rl-heading">Your community&rsquo;s pulse, one feed.</h1>
<p class="rl-subtitle">RSS Dashboard &amp; Activity Aggregator</p>
<p class="rl-subtext">
Subscribe to external RSS/Atom feeds, pull activity from your rSpace modules,
curate what matters, and republish as a unified Atom feed &mdash;
all from one local-first dashboard.
</p>
<div class="rl-cta-row">
<a href="https://demo.rspace.online/rfeeds" class="rl-cta-primary">View Demo</a>
<a href="/create-space" class="rl-cta-secondary">Create a Space</a>
</div>
</div>
<!-- Features -->
<section class="rl-section">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">What rFeeds Does</h2>
<div class="rl-grid-4">
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128225;</div>
<h3>RSS &amp; Atom Import</h3>
<p>Subscribe to any RSS or Atom feed. Bulk import via OPML. Background sync keeps items fresh.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128268;</div>
<h3>rApp Activity</h3>
<p>Pull activity from Calendar, Tasks, Docs, Wallet, Photos, and more &mdash; directly from Automerge docs.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128172;</div>
<h3>Manual Posts</h3>
<p>Write short posts that appear in the timeline alongside imported items. Quick updates for your community.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128752;</div>
<h3>Reshare &amp; Republish</h3>
<p>Curate the best items and reshare them into your space&rsquo;s public Atom feed for others to subscribe to.</p>
</div>
</div>
</div>
</section>
<!-- How It Works -->
<section class="rl-section rl-section--alt">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">How It Works</h2>
<div class="rl-grid-3">
<div class="rl-step">
<div class="rl-step__num">1</div>
<h3>Add Sources</h3>
<p>Paste an RSS URL, import an OPML file, or enable rApp activity adapters to pull from your modules.</p>
</div>
<div class="rl-step">
<div class="rl-step__num">2</div>
<h3>Curate &amp; Reshare</h3>
<p>Browse the unified timeline. Mark the best items as &ldquo;Reshared&rdquo; to include them in your outbound feed.</p>
</div>
<div class="rl-step">
<div class="rl-step__num">3</div>
<h3>Subscribe Anywhere</h3>
<p>Your space publishes an Atom feed. Other spaces, RSS readers, or newsletters can subscribe to it.</p>
</div>
</div>
</div>
</section>
<!-- Personal Feeds -->
<section class="rl-section">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">Personal Feed Profiles</h2>
<p class="rl-subtext" style="text-align:center">
Every member can create a personal feed profile that aggregates their activity across modules.
</p>
<div class="rl-grid-3">
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128100;</div>
<h3>Your Profile</h3>
<p>Set a display name, bio, and choose which modules appear in your personal feed.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128240;</div>
<h3>Personal Atom Feed</h3>
<p>Each profile gets a unique Atom URL. Others subscribe via any RSS reader or rFeeds instance.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#127760;</div>
<h3>Cross-Space</h3>
<p>Subscribe to someone&rsquo;s personal feed from a different space &mdash; it&rsquo;s just a standard Atom URL.</p>
</div>
</div>
</div>
</section>
<!-- Built On -->
<section class="rl-section rl-section--alt">
<div class="rl-container">
<h2 class="rl-heading" style="text-align:center">Built on Open Standards</h2>
<div class="rl-grid-3">
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128218;</div>
<h3>Atom 1.0</h3>
<p>All feeds published as standard Atom XML. Compatible with every RSS reader on the planet.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#129520;</div>
<h3>Automerge CRDT</h3>
<p>Feed data stored in local-first Automerge documents. Works offline, syncs on reconnect.</p>
</div>
<div class="rl-card rl-card--center">
<div class="rl-icon-box">&#128274;</div>
<h3>EncryptID</h3>
<p>Passkey-authenticated profiles. No passwords, no seeds &mdash; just biometric or hardware key sign-in.</p>
</div>
</div>
</div>
</section>
<!-- CTA -->
<section class="rl-section">
<div class="rl-container" style="text-align:center">
<h2 class="rl-heading">Your community&rsquo;s pulse, one feed.</h2>
<p class="rl-subtext">Try the demo or create a space to start curating.</p>
<div class="rl-cta-row">
<a href="https://demo.rspace.online/rfeeds" class="rl-cta-primary">View Demo</a>
<a href="/create-space" class="rl-cta-secondary">Create a Space</a>
</div>
</div>
</section>
<div class="rl-back">
<a href="/">&larr; Back to rSpace</a>
</div>`;
}

View File

@ -20,6 +20,7 @@ import { feedsSchema, feedsDocId, MAX_ITEMS_PER_SOURCE, MAX_ACTIVITY_CACHE, DEFA
import type { FeedsDoc, FeedSource, FeedItem, ManualPost, ActivityCacheEntry, ActivityModuleConfig, UserFeedProfile } from "./schemas";
import { parseFeed, parseOPML } from "./lib/feed-parser";
import { adapterRegistry } from "./adapters/index";
import { renderLanding } from "./landing";
let _syncServer: SyncServer | null = null;
@ -965,6 +966,8 @@ export const feedsModule: RSpaceModule = {
routes,
scoping: { defaultScope: "space", userConfigurable: false },
docSchemas: [feedsSchema as any],
standaloneDomain: "rfeeds.online",
landingPage: renderLanding,
onInit: async ({ syncServer }) => {
_syncServer = syncServer;