openvibers/openvibe.network Commits
Home page sites come from the frame's list (open only when the service serves its own domain): nine sites (Chat, Codes, Blog and Wiki joined), Opening next lists only what is not open yet, counts in words, JSON-LD and llms.txt follow; the Tools catalog is fetched at boot so a restart never shows the 49-tool fallback; the Chat entry no longer claims calls (they are still Live's)
Changes
@@@ -158,7 +158,7 @@ footer{border-top:1px solid var(--border);background:var(--bg-secondary)}<section class="hero"><div class="hero-badge"><span class="ov-mark" data-size="20" data-variant="network"></span> The OpenVibe Network — open source, community run</div><h1>The open network<br><span class="grad">for what you do online.</span></h1>-- <p class="sub">Live streaming, <!--OV:COUNT--> online tools, games, community pastes and a shared media library. Open source, run by the people who use it, and open to everyone without an account. Sign in once and every site already knows you.</p>++ <p class="sub">Live streaming, <!--OV:COUNT--> online tools, chat, games, community pastes, a wiki, a blog and a shared media library. Open source, run by the people who use it, and open to everyone without an account. Sign in once and every site already knows you.</p><div class="hero-ctas" id="hero-guest-ctas"><a class="btn btn-primary" href="/login"><i class="fa-solid fa-right-to-bracket"></i> Sign In</a><a class="btn btn-ghost" href="/login?tab=register"><i class="fa-solid fa-user-plus"></i> Create Account</a>@@@ -20,7 +20,7 @@ const RAW_SITES = [{ id: 'games', name: 'Games', icon: 'games', service: 'games', profile: 'games', tagline: 'Browser games', what: 'Scraplandia, a shared pixel canvas and other games that run in a tab and know your OpenVibe account.' },{ id: 'media', name: 'Media', icon: 'media', service: 'media', profile: 'hosting', tagline: 'VODs, clips, files', what: 'The public media library behind every site: recorded streams, clips, images and uploads.' },{ id: 'network', name: 'Network', icon: 'network', service: 'network', profile: 'account', tagline: 'Account and themes', what: 'The account behind every site: one sign-in, themes, notifications, history and linked services.' },-- { id: 'chat', name: 'Chat', icon: 'chat', service: 'chat', profile: 'ugc', tagline: 'One chat for the network', what: 'Rooms, stream chat, direct messages, calls and text-to-speech under one identity.' },++ { id: 'chat', name: 'Chat', icon: 'chat', service: 'chat', profile: 'ugc', tagline: 'One chat for the network', what: 'Global chat, rooms, stream chat and direct messages under one account, with or without JavaScript.' },{ id: 'codes', name: 'Codes', icon: 'codes', service: 'codes', profile: 'ugc', tagline: 'Build on OpenVibe', what: 'The developer portal: API and SDK docs, the service and capability registry, event schemas, credentials, playgrounds and mod publishing.' },{ id: 'blog', name: 'Blog', icon: 'blog', service: 'blog', profile: 'info', tagline: 'Long-form, yours to keep', what: 'The official OpenVibe blog and a blog for every member: drafts, scheduling, media, comments and feeds.' },{ id: 'wiki', name: 'Wiki', icon: 'wiki', service: 'wiki', profile: 'ugc', tagline: 'Knowledge, with sources', what: 'Wiki spaces with page trees, revision history, citations and discussion, editable together.' },@@@ -13,14 +13,15 @@ const SHELL = path.join(__dirname, '..', '..', 'public', 'index.html');const esc = seo.esc;const icon = (name, size) => `<span class="ov-icon" data-icon="${esc(name)}" data-ovi="${esc(icons.resolve(name))}" data-fx="none" style="--ovi-size:${size}px" aria-hidden="true">${icons.svg(name)}</span>`;--const SITES = [-- ['live', 'OpenVibe.Live', 'https://openvibe.live/', 'Live streaming', 'Go live from a browser, OBS or a phone. Restream to other platforms, clip moments, run chat games and keep your VODs.'],-- ['tools', 'OpenVibe.Tools', 'https://openvibe.tools/', 'Online tools', 'Converters, downloaders, PDF and image tools, developer utilities and network diagnostics, each on its own easy address.'],-- ['community', 'OpenVibe.Community', 'https://openvibe.community/', 'Pastes and posts', 'Share text and code with a link, comment, and follow what the people of OpenVibe are making.'],-- ['games', 'OpenVibe.Games', 'https://openvibe.games/', 'Browser games', 'Scraplandia and other games that run in a tab and know your OpenVibe account.'],-- ['media', 'OpenVibe.Media', 'https://openvibe.media/', 'VODs, clips and files', 'The media library behind every site: recorded streams, clips and uploads.'],--];--const SOON = [['chat', 'Chat'], ['codes', 'Codes'], ['blog', 'Blog'], ['wiki', 'Wiki'], ['news', 'News'], ['reviews', 'Reviews'], ['tips', 'Tips'], ['vip', 'VIP'], ['trade', 'Trade'], ['host', 'Host'], ['deals', 'Deals'], ['coupons', 'Coupons']];++// The sites come from the frame's list (server/frame/sites.js): a site is open only when its service's++// public domain serves the service itself (Network's exposure overlay), so the home page, the navbar and++// "Opening next" always agree with what is actually running.++const frameSites = require('../frame/sites').SITES;++const siteName = (site) => (site.name.includes('.') ? site.name : `OpenVibe.${site.name}`);++const OPEN = frameSites.filter((site) => site.status === 'open' && site.id !== 'network');++const SOON = frameSites.filter((site) => site.status !== 'open');++const WORDS = ['No', 'One', 'Two', 'Three', 'Four', 'Five', 'Six', 'Seven', 'Eight', 'Nine', 'Ten', 'Eleven', 'Twelve', 'Thirteen', 'Fourteen', 'Fifteen', 'Sixteen', 'Seventeen', 'Eighteen', 'Nineteen', 'Twenty'];++const inWords = (n) => WORDS[n] || String(n);const ACCOUNT = [['live', 'Go live and keep your VODs', 'Stream from a browser tab, clip the good parts, earn channel points and restream to other platforms.', 'https://openvibe.live/'],['tools', 'Tool results that stick around', 'Guests keep results for an hour. Signed in, they stay for a day and your recent tools follow you.', 'https://openvibe.tools/'],@@@ -55,15 +56,15 @@ function body(catalog) {const fams = catalog.families.filter(f => f.url);const count = (f) => f.count || catalog.tools.filter(t => t.family === f.id).length;return `<style>${CSS}</style>--<section class="home-sec" id="network" aria-labelledby="h-sites"><h2 id="h-sites">Five sites, one front door</h2><p class="lede">Stream, build, share, play and store. Everything is open to visitors; signing in once carries your name, theme and notifications to all of it.</p>--<div class="home-grid">${SITES.map(([ic, n, u, tag, d]) => `<a class="home-card" href="${u}">${icon(ic, 48)}<span><b>${n}</b><em>${tag}</em><small>${d}</small></span></a>`).join('')}</div></section>++<section class="home-sec" id="network" aria-labelledby="h-sites"><h2 id="h-sites">${inWords(OPEN.length)} sites, one front door</h2><p class="lede">Stream, chat, build, share, play, write and store. Everything is open to visitors; signing in once carries your name, theme and notifications to all of it.</p>++<div class="home-grid">${OPEN.map((site) => `<a class="home-card" href="https://${esc(site.host)}/">${icon(site.icon, 48)}<span><b>${esc(siteName(site))}</b><em>${esc(site.tagline)}</em><small>${esc(site.what)}</small></span></a>`).join('')}</div></section><section class="home-sec" id="tools" aria-labelledby="h-tools"><h2 id="h-tools">${catalog.tools.length} tools that just open</h2><p class="lede">No installs and no sign-up wall. Every tool has its own short address, so <a href="https://yt.openvibe.tools/">yt.openvibe.tools</a> or <a href="https://dns.openvibe.tools/">dns.openvibe.tools</a> takes you straight there. Browse them all at <a href="https://openvibe.tools/">openvibe.tools</a>.</p><div class="home-fams">${fams.map(f => `<a class="home-fam" href="${esc(f.path ? 'https://openvibe.tools' + f.path : f.url)}">${icon(f.icon, 38)}<span><b>${esc(f.name)}</b><small>${count(f)} tools · ${esc(f.tagline || '')}</small></span></a>`).join('')}</div><ul class="home-chips">${popular.map(t => `<li><a href="${esc(t.url)}" title="${esc(t.tagline || '')}">${icon(t.icon, 22)}${esc(t.name)}</a></li>`).join('')}</ul></section><section class="home-sec" aria-labelledby="h-acct"><h2 id="h-acct">What signing in adds</h2><p class="lede">You can use almost everything as a guest. An account is for the things that need to remember you, and it works on every OpenVibe site the moment you arrive.</p><div class="home-grid">${ACCOUNT.map(([ic, n, d, u]) => `<a class="home-card" href="${u}">${icon(ic, 44)}<span><b>${n}</b><small>${d}</small></span></a>`).join('')}</div></section>--<section class="home-sec" aria-labelledby="h-soon"><h2 id="h-soon">Opening next</h2><p class="lede">Thirteen more addresses are staked out. Each one opens when it is good enough to use daily, and your account will already work there.</p>--<ul class="home-chips">${SOON.map(([id, n]) => `<li><a href="https://openvibe.${id}/">${icon(id, 22)}OpenVibe.${n}</a></li>`).join('')}<li><a href="https://openre.stream/">${icon('stream', 22)}OpenRe.Stream</a></li></ul></section>++${SOON.length ? `<section class="home-sec" aria-labelledby="h-soon"><h2 id="h-soon">Opening next</h2><p class="lede">${inWords(SOON.length)} more addresses are staked out. Each one opens when it is good enough to use daily, and your account will already work there.</p>++<ul class="home-chips">${SOON.map((site) => `<li><a href="https://${esc(site.host)}/" title="${esc(site.tagline)}">${icon(site.icon, 22)}${esc(siteName(site))}</a></li>`).join('')}</ul></section>` : ''}<section class="home-sec" aria-labelledby="h-dev"><h2 id="h-dev">For developers and crawlers</h2><p class="lede">OpenVibe is open source and community-run. Everything public is meant to be read by machines too.</p><div class="home-dev"><div><b>Tool catalog</b><p>Every tool with its description, keywords and addresses as JSON: <a href="https://openvibe.tools/api/catalog.json"><code>openvibe.tools/api/catalog.json</code></a></p></div><div><b>llms.txt</b><p>A plain-text map of the network for AI assistants: <a href="/llms.txt"><code>/llms.txt</code></a> here and on <a href="https://openvibe.tools/llms.txt">openvibe.tools</a>.</p></div>@@@ -76,7 +77,7 @@ function render() {const stat = fs.statSync(SHELL);const key = `${stat.mtimeMs}:${catalog.updated}:${catalog.tools.length}`;if (cache.key === key) return cache;-- const ld = seo.jsonLdTag(seo.jsonLd.itemList('OpenVibe sites', SITES.map(([, n, u, , d]) => ({ name: n, url: u, description: d }))));++ const ld = seo.jsonLdTag(seo.jsonLd.itemList('OpenVibe sites', OPEN.map((site) => ({ name: siteName(site), url: `https://${site.host}/`, description: site.what }))));const html = fs.readFileSync(SHELL, 'utf8').replace('<div id="navbar-mount"></div>', '<div id="navbar-mount"></div>' + require('openvibe-shared/frame').noscriptNav({ name: 'OpenVibe.Network', links: [{ label: 'Sign in', href: '/login' }, { label: 'Themes', href: '/themes' }] })).replace('<!--OV:HOME-->', body(catalog)).replace('<!--OV:COUNT-->', String(catalog.tools.length)).replace('<div id="ov-footer"></div>', require('openvibe-shared/footer').ssr({ service: 'network', variant: 'full' })).replace('</head>', `${ld}\n</head>`);cache = { key, html, etag: '"' + crypto.createHash('sha1').update(html).digest('base64url').slice(0, 20) + '"' };return cache;@@@ -94,8 +95,8 @@ function sendHome(req, res) {function llmsTxt() {const { catalog } = toolsCatalog.peek();-- return seo.llmsTxt({ name: 'OpenVibe Network', summary: 'OpenVibe is an open source, community-run network of sites that share one account: live streaming, online tools, community pastes, games and media.',-- sections: [{ title: 'Sites', links: SITES.map(([, n, u, , d]) => ({ title: n, url: u, note: d })) },++ return seo.llmsTxt({ name: 'OpenVibe Network', summary: `OpenVibe is an open source, community-run network of sites that share one account: ${OPEN.map((site) => site.name.toLowerCase()).join(', ')}.`,++ sections: [{ title: 'Sites', links: OPEN.map((site) => ({ title: siteName(site), url: `https://${site.host}/`, note: site.what })) },{ title: 'Tool families', links: catalog.families.filter(f => f.url).map(f => ({ title: f.name, url: f.path ? 'https://openvibe.tools' + f.path : f.url, note: f.tagline })) },{ title: 'Machine-readable', links: [{ title: 'Tool catalog (JSON)', url: 'https://openvibe.tools/api/catalog.json' }, { title: 'Tools llms.txt', url: 'https://openvibe.tools/llms.txt' }, { title: 'Tool domains (JSON)', url: 'https://openvibe.network/api/domains' }] }] });}@@@ -731,6 +731,8 @@ app.use(require('./not-found').notFound);// ── Start ────────────────────────────────────────────────────app.listen(config.port, config.host, () => {++ // The home page renders the Tools catalog it already holds: fetch it now, not on the first visit.++ require('./domains/catalog').refresh().catch(() => {});console.log(`\n╔═══════════════════════════════════════╗`);console.log(`║ OpenVibe.Network — Identity Service ║`);console.log(`╠═══════════════════════════════════════╣`);
@@ -158,7 +158,7 @@ footer{border-top:1px solid var(--border);background:var(--bg-secondary)}
@@ -158,7 +158,7 @@ footer{border-top:1px solid var(--border);background:var(--bg-secondary)}
<section class="hero">
<section class="hero">
<div class="hero-badge"><span class="ov-mark" data-size="20" data-variant="network"></span> The OpenVibe Network — open source, community run</div>
<div class="hero-badge"><span class="ov-mark" data-size="20" data-variant="network"></span> The OpenVibe Network — open source, community run</div>
<h1>The open network<br><span class="grad">for what you do online.</span></h1>
<h1>The open network<br><span class="grad">for what you do online.</span></h1>
- <p class="sub">Live streaming, <!--OV:COUNT--> online tools, games, community pastes and a shared media library. Open source, run by the people who use it, and open to everyone without an account. Sign in once and every site already knows you.</p>
+ <p class="sub">Live streaming, <!--OV:COUNT--> online tools, chat, games, community pastes, a wiki, a blog and a shared media library. Open source, run by the people who use it, and open to everyone without an account. Sign in once and every site already knows you.</p>
<div class="hero-ctas" id="hero-guest-ctas">
<div class="hero-ctas" id="hero-guest-ctas">
<a class="btn btn-primary" href="/login"><i class="fa-solid fa-right-to-bracket"></i> Sign In</a>
<a class="btn btn-primary" href="/login"><i class="fa-solid fa-right-to-bracket"></i> Sign In</a>
<a class="btn btn-ghost" href="/login?tab=register"><i class="fa-solid fa-user-plus"></i> Create Account</a>
<a class="btn btn-ghost" href="/login?tab=register"><i class="fa-solid fa-user-plus"></i> Create Account</a>
@@ -20,7 +20,7 @@ const RAW_SITES = [
@@ -20,7 +20,7 @@ const RAW_SITES = [
{ id: 'games', name: 'Games', icon: 'games', service: 'games', profile: 'games', tagline: 'Browser games', what: 'Scraplandia, a shared pixel canvas and other games that run in a tab and know your OpenVibe account.' },
{ id: 'games', name: 'Games', icon: 'games', service: 'games', profile: 'games', tagline: 'Browser games', what: 'Scraplandia, a shared pixel canvas and other games that run in a tab and know your OpenVibe account.' },
{ id: 'media', name: 'Media', icon: 'media', service: 'media', profile: 'hosting', tagline: 'VODs, clips, files', what: 'The public media library behind every site: recorded streams, clips, images and uploads.' },
{ id: 'media', name: 'Media', icon: 'media', service: 'media', profile: 'hosting', tagline: 'VODs, clips, files', what: 'The public media library behind every site: recorded streams, clips, images and uploads.' },
{ id: 'network', name: 'Network', icon: 'network', service: 'network', profile: 'account', tagline: 'Account and themes', what: 'The account behind every site: one sign-in, themes, notifications, history and linked services.' },
{ id: 'network', name: 'Network', icon: 'network', service: 'network', profile: 'account', tagline: 'Account and themes', what: 'The account behind every site: one sign-in, themes, notifications, history and linked services.' },
- { id: 'chat', name: 'Chat', icon: 'chat', service: 'chat', profile: 'ugc', tagline: 'One chat for the network', what: 'Rooms, stream chat, direct messages, calls and text-to-speech under one identity.' },
+ { id: 'chat', name: 'Chat', icon: 'chat', service: 'chat', profile: 'ugc', tagline: 'One chat for the network', what: 'Global chat, rooms, stream chat and direct messages under one account, with or without JavaScript.' },
{ id: 'codes', name: 'Codes', icon: 'codes', service: 'codes', profile: 'ugc', tagline: 'Build on OpenVibe', what: 'The developer portal: API and SDK docs, the service and capability registry, event schemas, credentials, playgrounds and mod publishing.' },
{ id: 'codes', name: 'Codes', icon: 'codes', service: 'codes', profile: 'ugc', tagline: 'Build on OpenVibe', what: 'The developer portal: API and SDK docs, the service and capability registry, event schemas, credentials, playgrounds and mod publishing.' },
{ id: 'blog', name: 'Blog', icon: 'blog', service: 'blog', profile: 'info', tagline: 'Long-form, yours to keep', what: 'The official OpenVibe blog and a blog for every member: drafts, scheduling, media, comments and feeds.' },
{ id: 'blog', name: 'Blog', icon: 'blog', service: 'blog', profile: 'info', tagline: 'Long-form, yours to keep', what: 'The official OpenVibe blog and a blog for every member: drafts, scheduling, media, comments and feeds.' },
{ id: 'wiki', name: 'Wiki', icon: 'wiki', service: 'wiki', profile: 'ugc', tagline: 'Knowledge, with sources', what: 'Wiki spaces with page trees, revision history, citations and discussion, editable together.' },
{ id: 'wiki', name: 'Wiki', icon: 'wiki', service: 'wiki', profile: 'ugc', tagline: 'Knowledge, with sources', what: 'Wiki spaces with page trees, revision history, citations and discussion, editable together.' },
@@ -13,14 +13,15 @@ const SHELL = path.join(__dirname, '..', '..', 'public', 'index.html');
@@ -13,14 +13,15 @@ const SHELL = path.join(__dirname, '..', '..', 'public', 'index.html');
const esc = seo.esc;
const esc = seo.esc;
const icon = (name, size) => `<span class="ov-icon" data-icon="${esc(name)}" data-ovi="${esc(icons.resolve(name))}" data-fx="none" style="--ovi-size:${size}px" aria-hidden="true">${icons.svg(name)}</span>`;
const icon = (name, size) => `<span class="ov-icon" data-icon="${esc(name)}" data-ovi="${esc(icons.resolve(name))}" data-fx="none" style="--ovi-size:${size}px" aria-hidden="true">${icons.svg(name)}</span>`;
-const SITES = [
- ['live', 'OpenVibe.Live', 'https://openvibe.live/', 'Live streaming', 'Go live from a browser, OBS or a phone. Restream to other platforms, clip moments, run chat games and keep your VODs.'],
- ['tools', 'OpenVibe.Tools', 'https://openvibe.tools/', 'Online tools', 'Converters, downloaders, PDF and image tools, developer utilities and network diagnostics, each on its own easy address.'],
- ['community', 'OpenVibe.Community', 'https://openvibe.community/', 'Pastes and posts', 'Share text and code with a link, comment, and follow what the people of OpenVibe are making.'],
- ['games', 'OpenVibe.Games', 'https://openvibe.games/', 'Browser games', 'Scraplandia and other games that run in a tab and know your OpenVibe account.'],
- ['media', 'OpenVibe.Media', 'https://openvibe.media/', 'VODs, clips and files', 'The media library behind every site: recorded streams, clips and uploads.'],
-];
-const SOON = [['chat', 'Chat'], ['codes', 'Codes'], ['blog', 'Blog'], ['wiki', 'Wiki'], ['news', 'News'], ['reviews', 'Reviews'], ['tips', 'Tips'], ['vip', 'VIP'], ['trade', 'Trade'], ['host', 'Host'], ['deals', 'Deals'], ['coupons', 'Coupons']];
+// The sites come from the frame's list (server/frame/sites.js): a site is open only when its service's
+// public domain serves the service itself (Network's exposure overlay), so the home page, the navbar and
+// "Opening next" always agree with what is actually running.
+const frameSites = require('../frame/sites').SITES;
+const siteName = (site) => (site.name.includes('.') ? site.name : `OpenVibe.${site.name}`);
+const OPEN = frameSites.filter((site) => site.status === 'open' && site.id !== 'network');
+const SOON = frameSites.filter((site) => site.status !== 'open');
+const WORDS = ['No', 'One', 'Two', 'Three', 'Four', 'Five', 'Six', 'Seven', 'Eight', 'Nine', 'Ten', 'Eleven', 'Twelve', 'Thirteen', 'Fourteen', 'Fifteen', 'Sixteen', 'Seventeen', 'Eighteen', 'Nineteen', 'Twenty'];
+const inWords = (n) => WORDS[n] || String(n);
const ACCOUNT = [
const ACCOUNT = [
['live', 'Go live and keep your VODs', 'Stream from a browser tab, clip the good parts, earn channel points and restream to other platforms.', 'https://openvibe.live/'],
['live', 'Go live and keep your VODs', 'Stream from a browser tab, clip the good parts, earn channel points and restream to other platforms.', 'https://openvibe.live/'],
['tools', 'Tool results that stick around', 'Guests keep results for an hour. Signed in, they stay for a day and your recent tools follow you.', 'https://openvibe.tools/'],
['tools', 'Tool results that stick around', 'Guests keep results for an hour. Signed in, they stay for a day and your recent tools follow you.', 'https://openvibe.tools/'],
@@ -55,15 +56,15 @@ function body(catalog) {
@@ -55,15 +56,15 @@ function body(catalog) {
const fams = catalog.families.filter(f => f.url);
const fams = catalog.families.filter(f => f.url);
const count = (f) => f.count || catalog.tools.filter(t => t.family === f.id).length;
const count = (f) => f.count || catalog.tools.filter(t => t.family === f.id).length;
return `<style>${CSS}</style>
return `<style>${CSS}</style>
-<section class="home-sec" id="network" aria-labelledby="h-sites"><h2 id="h-sites">Five sites, one front door</h2><p class="lede">Stream, build, share, play and store. Everything is open to visitors; signing in once carries your name, theme and notifications to all of it.</p>
-<div class="home-grid">${SITES.map(([ic, n, u, tag, d]) => `<a class="home-card" href="${u}">${icon(ic, 48)}<span><b>${n}</b><em>${tag}</em><small>${d}</small></span></a>`).join('')}</div></section>
+<section class="home-sec" id="network" aria-labelledby="h-sites"><h2 id="h-sites">${inWords(OPEN.length)} sites, one front door</h2><p class="lede">Stream, chat, build, share, play, write and store. Everything is open to visitors; signing in once carries your name, theme and notifications to all of it.</p>
+<div class="home-grid">${OPEN.map((site) => `<a class="home-card" href="https://${esc(site.host)}/">${icon(site.icon, 48)}<span><b>${esc(siteName(site))}</b><em>${esc(site.tagline)}</em><small>${esc(site.what)}</small></span></a>`).join('')}</div></section>
<section class="home-sec" id="tools" aria-labelledby="h-tools"><h2 id="h-tools">${catalog.tools.length} tools that just open</h2><p class="lede">No installs and no sign-up wall. Every tool has its own short address, so <a href="https://yt.openvibe.tools/">yt.openvibe.tools</a> or <a href="https://dns.openvibe.tools/">dns.openvibe.tools</a> takes you straight there. Browse them all at <a href="https://openvibe.tools/">openvibe.tools</a>.</p>
<section class="home-sec" id="tools" aria-labelledby="h-tools"><h2 id="h-tools">${catalog.tools.length} tools that just open</h2><p class="lede">No installs and no sign-up wall. Every tool has its own short address, so <a href="https://yt.openvibe.tools/">yt.openvibe.tools</a> or <a href="https://dns.openvibe.tools/">dns.openvibe.tools</a> takes you straight there. Browse them all at <a href="https://openvibe.tools/">openvibe.tools</a>.</p>
<div class="home-fams">${fams.map(f => `<a class="home-fam" href="${esc(f.path ? 'https://openvibe.tools' + f.path : f.url)}">${icon(f.icon, 38)}<span><b>${esc(f.name)}</b><small>${count(f)} tools · ${esc(f.tagline || '')}</small></span></a>`).join('')}</div>
<div class="home-fams">${fams.map(f => `<a class="home-fam" href="${esc(f.path ? 'https://openvibe.tools' + f.path : f.url)}">${icon(f.icon, 38)}<span><b>${esc(f.name)}</b><small>${count(f)} tools · ${esc(f.tagline || '')}</small></span></a>`).join('')}</div>
<ul class="home-chips">${popular.map(t => `<li><a href="${esc(t.url)}" title="${esc(t.tagline || '')}">${icon(t.icon, 22)}${esc(t.name)}</a></li>`).join('')}</ul></section>
<ul class="home-chips">${popular.map(t => `<li><a href="${esc(t.url)}" title="${esc(t.tagline || '')}">${icon(t.icon, 22)}${esc(t.name)}</a></li>`).join('')}</ul></section>
<section class="home-sec" aria-labelledby="h-acct"><h2 id="h-acct">What signing in adds</h2><p class="lede">You can use almost everything as a guest. An account is for the things that need to remember you, and it works on every OpenVibe site the moment you arrive.</p>
<section class="home-sec" aria-labelledby="h-acct"><h2 id="h-acct">What signing in adds</h2><p class="lede">You can use almost everything as a guest. An account is for the things that need to remember you, and it works on every OpenVibe site the moment you arrive.</p>
<div class="home-grid">${ACCOUNT.map(([ic, n, d, u]) => `<a class="home-card" href="${u}">${icon(ic, 44)}<span><b>${n}</b><small>${d}</small></span></a>`).join('')}</div></section>
<div class="home-grid">${ACCOUNT.map(([ic, n, d, u]) => `<a class="home-card" href="${u}">${icon(ic, 44)}<span><b>${n}</b><small>${d}</small></span></a>`).join('')}</div></section>
-<section class="home-sec" aria-labelledby="h-soon"><h2 id="h-soon">Opening next</h2><p class="lede">Thirteen more addresses are staked out. Each one opens when it is good enough to use daily, and your account will already work there.</p>
-<ul class="home-chips">${SOON.map(([id, n]) => `<li><a href="https://openvibe.${id}/">${icon(id, 22)}OpenVibe.${n}</a></li>`).join('')}<li><a href="https://openre.stream/">${icon('stream', 22)}OpenRe.Stream</a></li></ul></section>
+${SOON.length ? `<section class="home-sec" aria-labelledby="h-soon"><h2 id="h-soon">Opening next</h2><p class="lede">${inWords(SOON.length)} more addresses are staked out. Each one opens when it is good enough to use daily, and your account will already work there.</p>
+<ul class="home-chips">${SOON.map((site) => `<li><a href="https://${esc(site.host)}/" title="${esc(site.tagline)}">${icon(site.icon, 22)}${esc(siteName(site))}</a></li>`).join('')}</ul></section>` : ''}
<section class="home-sec" aria-labelledby="h-dev"><h2 id="h-dev">For developers and crawlers</h2><p class="lede">OpenVibe is open source and community-run. Everything public is meant to be read by machines too.</p>
<section class="home-sec" aria-labelledby="h-dev"><h2 id="h-dev">For developers and crawlers</h2><p class="lede">OpenVibe is open source and community-run. Everything public is meant to be read by machines too.</p>
<div class="home-dev"><div><b>Tool catalog</b><p>Every tool with its description, keywords and addresses as JSON: <a href="https://openvibe.tools/api/catalog.json"><code>openvibe.tools/api/catalog.json</code></a></p></div>
<div class="home-dev"><div><b>Tool catalog</b><p>Every tool with its description, keywords and addresses as JSON: <a href="https://openvibe.tools/api/catalog.json"><code>openvibe.tools/api/catalog.json</code></a></p></div>
<div><b>llms.txt</b><p>A plain-text map of the network for AI assistants: <a href="/llms.txt"><code>/llms.txt</code></a> here and on <a href="https://openvibe.tools/llms.txt">openvibe.tools</a>.</p></div>
<div><b>llms.txt</b><p>A plain-text map of the network for AI assistants: <a href="/llms.txt"><code>/llms.txt</code></a> here and on <a href="https://openvibe.tools/llms.txt">openvibe.tools</a>.</p></div>
@@ -76,7 +77,7 @@ function render() {
@@ -76,7 +77,7 @@ function render() {
const stat = fs.statSync(SHELL);
const stat = fs.statSync(SHELL);
const key = `${stat.mtimeMs}:${catalog.updated}:${catalog.tools.length}`;
const key = `${stat.mtimeMs}:${catalog.updated}:${catalog.tools.length}`;
if (cache.key === key) return cache;
if (cache.key === key) return cache;
- const ld = seo.jsonLdTag(seo.jsonLd.itemList('OpenVibe sites', SITES.map(([, n, u, , d]) => ({ name: n, url: u, description: d }))));
+ const ld = seo.jsonLdTag(seo.jsonLd.itemList('OpenVibe sites', OPEN.map((site) => ({ name: siteName(site), url: `https://${site.host}/`, description: site.what }))));
const html = fs.readFileSync(SHELL, 'utf8').replace('<div id="navbar-mount"></div>', '<div id="navbar-mount"></div>' + require('openvibe-shared/frame').noscriptNav({ name: 'OpenVibe.Network', links: [{ label: 'Sign in', href: '/login' }, { label: 'Themes', href: '/themes' }] })).replace('<!--OV:HOME-->', body(catalog)).replace('<!--OV:COUNT-->', String(catalog.tools.length)).replace('<div id="ov-footer"></div>', require('openvibe-shared/footer').ssr({ service: 'network', variant: 'full' })).replace('</head>', `${ld}\n</head>`);
const html = fs.readFileSync(SHELL, 'utf8').replace('<div id="navbar-mount"></div>', '<div id="navbar-mount"></div>' + require('openvibe-shared/frame').noscriptNav({ name: 'OpenVibe.Network', links: [{ label: 'Sign in', href: '/login' }, { label: 'Themes', href: '/themes' }] })).replace('<!--OV:HOME-->', body(catalog)).replace('<!--OV:COUNT-->', String(catalog.tools.length)).replace('<div id="ov-footer"></div>', require('openvibe-shared/footer').ssr({ service: 'network', variant: 'full' })).replace('</head>', `${ld}\n</head>`);
cache = { key, html, etag: '"' + crypto.createHash('sha1').update(html).digest('base64url').slice(0, 20) + '"' };
cache = { key, html, etag: '"' + crypto.createHash('sha1').update(html).digest('base64url').slice(0, 20) + '"' };
return cache;
return cache;
@@ -94,8 +95,8 @@ function sendHome(req, res) {
@@ -94,8 +95,8 @@ function sendHome(req, res) {
function llmsTxt() {
function llmsTxt() {
const { catalog } = toolsCatalog.peek();
const { catalog } = toolsCatalog.peek();
- return seo.llmsTxt({ name: 'OpenVibe Network', summary: 'OpenVibe is an open source, community-run network of sites that share one account: live streaming, online tools, community pastes, games and media.',
- sections: [{ title: 'Sites', links: SITES.map(([, n, u, , d]) => ({ title: n, url: u, note: d })) },
+ return seo.llmsTxt({ name: 'OpenVibe Network', summary: `OpenVibe is an open source, community-run network of sites that share one account: ${OPEN.map((site) => site.name.toLowerCase()).join(', ')}.`,
+ sections: [{ title: 'Sites', links: OPEN.map((site) => ({ title: siteName(site), url: `https://${site.host}/`, note: site.what })) },
{ title: 'Tool families', links: catalog.families.filter(f => f.url).map(f => ({ title: f.name, url: f.path ? 'https://openvibe.tools' + f.path : f.url, note: f.tagline })) },
{ title: 'Tool families', links: catalog.families.filter(f => f.url).map(f => ({ title: f.name, url: f.path ? 'https://openvibe.tools' + f.path : f.url, note: f.tagline })) },
{ title: 'Machine-readable', links: [{ title: 'Tool catalog (JSON)', url: 'https://openvibe.tools/api/catalog.json' }, { title: 'Tools llms.txt', url: 'https://openvibe.tools/llms.txt' }, { title: 'Tool domains (JSON)', url: 'https://openvibe.network/api/domains' }] }] });
{ title: 'Machine-readable', links: [{ title: 'Tool catalog (JSON)', url: 'https://openvibe.tools/api/catalog.json' }, { title: 'Tools llms.txt', url: 'https://openvibe.tools/llms.txt' }, { title: 'Tool domains (JSON)', url: 'https://openvibe.network/api/domains' }] }] });
}
}
@@ -731,6 +731,8 @@ app.use(require('./not-found').notFound);
@@ -731,6 +731,8 @@ app.use(require('./not-found').notFound);
// ── Start ────────────────────────────────────────────────────
// ── Start ────────────────────────────────────────────────────
app.listen(config.port, config.host, () => {
app.listen(config.port, config.host, () => {
+ // The home page renders the Tools catalog it already holds: fetch it now, not on the first visit.
+ require('./domains/catalog').refresh().catch(() => {});
console.log(`\n╔═══════════════════════════════════════╗`);
console.log(`\n╔═══════════════════════════════════════╗`);
console.log(`║ OpenVibe.Network — Identity Service ║`);
console.log(`║ OpenVibe.Network — Identity Service ║`);
console.log(`╠═══════════════════════════════════════╣`);
console.log(`╠═══════════════════════════════════════╣`);