openvibers/openvibe.network Commits

Home: one count of the sites, the same as /sites (#572)

OP OpenVibers committed 7 days ago · b9a7c9e021 · 3 files, +37 −10

A phone showed "51 sites, one front door" above "All 52 sites, grouped by what
you came to do". Both were computed from the frame's list, but the heading left
the front door out and the link, the orbit, the numbers and the hero counted it.
SITE_COUNT is every open site, Network included, the count /sites prints, and
every number of sites on the home uses it. A new test reads every "N sites" on
the rendered page and checks they are all that count.

Changes

ddiff --git a/server/home/render.js b/server/home/render.js
iindex 0d87d2cf..4fb3ef01 100644
---- a/server/home/render.js
++++ b/server/home/render.js
@@@ -29,6 +29,11 @@ const social = require('openvibe-shared/social');
// OpenRestream is its own name, never "OpenVibe.OpenRestream" (the owner, 2026-10-28).
const siteName = (site) => (site.name.includes('.') ? site.name : fullName(site));
const OPEN = frameSites.filter((site) => site.status === 'open' && site.id !== 'network');
++// How many sites there are: every open one, this front door included, the same count /sites shows (server/sites/page.js).
++// One number for the whole page: until 2026-11-06 the sites heading counted without the front door and the link under it
++// with it, so a phone showed "51 sites, one front door" above "All 52 sites" (the owner: "why isn't this count just
++// dynamic"). It always was computed; it now has one definition.
++const SITE_COUNT = frameSites.filter((site) => site.status === 'open').length;
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);
@@@ -102,11 +107,11 @@ const SHORT = (site) => site.name.replace(/^OpenVibe\./, '');
function orbit() {
const inner = OPEN.slice(0, 6), outer = OPEN.slice(6, 16);
const node = (site, i, n, big) => `<a class="orb-node${big ? ' is-big' : ''}" href="https://${esc(site.host)}/" style="--i:${i};--n:${n}" title="${esc(siteName(site))}: ${esc(site.tagline || '')}" aria-label="${esc(siteName(site))}"><span class="orb-face">${icon(site.icon, big ? 26 : 20)}${big ? `<span class="orb-name">${esc(SHORT(site))}</span>` : ''}</span></a>`;
-- return `<div class="orbit" role="group" aria-label="The OpenVibe network: ${OPEN.length + 1} sites around one account">
++ return `<div class="orbit" role="group" aria-label="The OpenVibe network: ${SITE_COUNT} sites around one account">
<div class="orb-glow" aria-hidden="true"></div>
<div class="orb-ring r2">${outer.map((s, i) => node(s, i, outer.length, false)).join('')}</div>
<div class="orb-ring r1">${inner.map((s, i) => node(s, i, inner.length, true)).join('')}</div>
--<a class="orb-core" href="#network"><span class="ov-mark" data-size="64" data-variant="network" style="width:64px;height:64px"></span><b>${OPEN.length + 1} sites</b><small>one account</small></a>
++<a class="orb-core" href="#network"><span class="ov-mark" data-size="64" data-variant="network" style="width:64px;height:64px"></span><b>${SITE_COUNT} sites</b><small>one account</small></a>
</div>`;
}
@@@ -195,7 +200,7 @@ function numbers(catalog, a) {
const c = a.counts || {};
const items = [
c.services ? [fmtCount(c.services.up), 'services up', `of ${c.services.live} checked`, '/status'] : null,
-- [String(OPEN.length + 1), 'sites, one account', '', '#network'],
++ [String(SITE_COUNT), 'sites, one account', '', '#network'],
catalog.tools.length ? [fmtCount(catalog.tools.length), 'online tools', '', 'https://openvibe.tools/'] : null,
c.accounts ? [fmtCount(c.accounts), 'accounts', '', '/login?tab=register'] : null,
a.liveCount ? [fmtCount(a.liveCount), a.liveCount === 1 ? 'stream live now' : 'streams live now', '', 'https://openvibe.live/'] : null,
@@@ -321,7 +326,7 @@ ${icons.CSS}
// The sites section's heading; the JSON-LD ItemList of the same sites carries it as its name, so the
// structured data names something a visitor can see (browser check, WS-Q task 3).
--const sitesHeading = () => `${inWords(OPEN.length)} sites, one front door`;
++const sitesHeading = () => `${inWords(SITE_COUNT)} sites, one front door`;
/** A tagline as the start of a sentence: a period only when it does not already end one ("What should we eat?"). */
const asSentence = (t) => (/[.!?…]$/.test(String(t).trim()) ? String(t).trim() : `${String(t).trim()}.`);
@@@ -366,7 +371,7 @@ function body(catalog, act) {
const sites = showcase.apps({
id: 'network', title: sitesHeading(),
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.',
-- groups, more: { href: '/sites', label: `All ${OPEN.length + 1} sites, grouped by what you came to do` },
++ groups, more: { href: '/sites', label: `All ${SITE_COUNT} sites, grouped by what you came to do` },
});
// The first two neighbourhoods show; the rest fold (a phone saw 48 tiles in eight groups, two screens of the front door).
const foldSites = (html) => {
@@@ -413,9 +418,9 @@ ${moreParts([['tools', 'Tools', tools], ['account', 'Your account', account], ['
function summaryOf(catalog) {
return seo.pageSummary({
title: 'OpenVibe.Network',
-- summary: `One account for ${OPEN.length} open sites (${OPEN.map((site) => SHORT(site).toLowerCase()).join(', ')}), free, open source and community run.`,
++ summary: `One account for ${SITE_COUNT} open sites (this front door, ${OPEN.map((site) => SHORT(site).toLowerCase()).join(', ')}), free, open source and community run.`,
url: 'https://openvibe.network/',
-- facts: [['Open sites', OPEN.length], ['Opening next', SOON.length], ...(catalog.tools.length ? [['Online tools', catalog.tools.length]] : [])],
++ facts: [['Open sites', SITE_COUNT], ['Opening next', SOON.length], ...(catalog.tools.length ? [['Online tools', catalog.tools.length]] : [])],
updated: catalog.tools.length ? String(catalog.updated).slice(0, 10) : undefined,
});
}
@@@ -442,12 +447,12 @@ function render({ releaseId = '' } = {}) {
const ld = seo.jsonLdTag(seo.jsonLd.website({
name: 'OpenVibe.Network',
url: 'https://openvibe.network/',
-- description: `The front door to the OpenVibe network: one free account across ${OPEN.length} open sites — live streaming, tools, chat, games, a wiki, news, deals and an open developer platform.`,
++ description: `The front door to the OpenVibe network: one free account across ${SITE_COUNT} open sites — live streaming, tools, chat, games, a wiki, news, deals and an open developer platform.`,
searchUrl: 'https://search.openvibe.network/?q={q}',
})) + seo.jsonLdTag(seo.jsonLd.itemList(sitesHeading(), OPEN.map((site) => ({ name: siteName(site), url: `https://${site.host}/`, description: site.what })))) + seo.jsonLdTag(seo.jsonLd.faq(faqPairs()));
const card = social.url('network', 'https://openvibe.network/shared') || 'https://openvibe.network/assets/logo-512.png';
const summary = summaryOf(catalog);
-- const html = withSameAs(lookBoot.apply(fs.readFileSync(SHELL, 'utf8'))).replace('<div id="navbar-mount"></div>', '<div id="navbar-mount"></div>' + summary.body + require('openvibe-shared/frame').noscriptNav({ name: 'OpenVibe.Network', links: [{ label: 'Sign in', href: '/login' }, { label: 'Themes', href: '/themes' }] })).replace('<!--OV:HOME-->', body(catalog, act)).replace('<!--OV:INTENTS-->', intents(catalog, act.popularity)).replace('<!--OV:CONSTELLATION-->', () => orbit() + numbers(catalog, act)).replaceAll('__OV_CARD__', card).replace('<!--OV:SITES-->', String(OPEN.length + 1)).replace('<!--OV:COUNT-->', catalog.tools.length ? String(catalog.tools.length) : 'free').replace('<div id="ov-footer"></div>', require('openvibe-shared/footer').ssr({ service: 'network', variant: 'full' })).replace('</head>', () => `${ld}\n${summary.head}\n${boost.meta(releaseId)}\n</head>`);
++ const html = withSameAs(lookBoot.apply(fs.readFileSync(SHELL, 'utf8'))).replace('<div id="navbar-mount"></div>', '<div id="navbar-mount"></div>' + summary.body + require('openvibe-shared/frame').noscriptNav({ name: 'OpenVibe.Network', links: [{ label: 'Sign in', href: '/login' }, { label: 'Themes', href: '/themes' }] })).replace('<!--OV:HOME-->', body(catalog, act)).replace('<!--OV:INTENTS-->', intents(catalog, act.popularity)).replace('<!--OV:CONSTELLATION-->', () => orbit() + numbers(catalog, act)).replaceAll('__OV_CARD__', card).replace('<!--OV:SITES-->', String(SITE_COUNT)).replace('<!--OV:COUNT-->', catalog.tools.length ? String(catalog.tools.length) : 'free').replace('<div id="ov-footer"></div>', require('openvibe-shared/footer').ssr({ service: 'network', variant: 'full' })).replace('</head>', () => `${ld}\n${summary.head}\n${boost.meta(releaseId)}\n</head>`);
pageCache = { key, html, etag: '"' + crypto.createHash('sha1').update(html).digest('base64url').slice(0, 20) + '"' };
return pageCache;
}
ddiff --git a/test/home-front-door.test.js b/test/home-front-door.test.js
iindex 3bc03f5e..7c540fe8 100644
---- a/test/home-front-door.test.js
++++ b/test/home-front-door.test.js
@@@ -96,7 +96,7 @@ for (const id of ['network', 'tools', 'account', 'developers']) assert.ok(new Re
assert.strictEqual(html.includes('<section class="sc-sec" id="soon"'), sites.some((s) => s.status === 'soon'), 'an "Opening next" section only while a site is still to open (none since Tips and VIP, contracts 0.200.0)');
assert.ok(!/class="home-(sec|card|grid|fams|fam|dev)\b/.test(html), 'no hand-built section markup is left');
assert.strictEqual((html.match(/<h1[\s>]/g) || []).length, 1, 'one h1: the hero');
--assert.match(html, /<meta name="ai-summary" content="One account for \d+ open sites \(live, /);
++assert.match(html, /<meta name="ai-summary" content="One account for \d+ open sites \(this front door, live, /);
assert.ok(/"@type":"WebPage"/.test(html), 'the summary has its WebPage JSON-LD');
// SEO-31: the network is one WebSite entity, and its sitelinks search is the network's own search service.
const siteLd = JSON.parse(/<script type="application\/ld\+json">(\{"@context":"https:\/\/schema.org","@type":"WebSite"[\s\S]*?)<\/script>/.exec(html)[1]);
ddiff --git a/test/home-site-count.test.js b/test/home-site-count.test.js
nnew file mode 100644
iindex 00000000..9dffd2ab
---- /dev/null
++++ b/test/home-site-count.test.js
@@@ -0,0 +1,22 @@
++'use strict';
++// One count of the sites on the front door (the owner, 2026-11-06: a phone showed "51 sites, one front door" above
++// "All 52 sites, grouped by what you came to do"). Every number of sites the home page prints is the count /sites
++// prints: every open site, this front door included, from the frame's list, never a number typed in.
++const assert = require('assert');
++const activity = require('../server/home/activity');
++const home = require('../server/home/render');
++const sitesPage = require('../server/sites/page');
++const { SITES } = require('../server/frame/sites');
++
++activity.setEnabled(false);
++const expected = SITES.filter((s) => s.status === 'open').length;
++const { html } = home.render();
++const text = html.replace(/<script[\s\S]*?<\/script>/g, ' ').replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ');
++const WORDS = ['no', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten', 'eleven', 'twelve', 'thirteen', 'fourteen', 'fifteen', 'sixteen', 'seventeen', 'eighteen', 'nineteen', 'twenty'];
++const counts = [...text.matchAll(/\b(\d+|[A-Za-z]+) (?:open )?sites\b/g)].map((m) => m[1]).map((n) => (/^\d+$/.test(n) ? Number(n) : WORDS.indexOf(n.toLowerCase())))
++ .filter((n) => n > 1); // "all sites", "other sites" and the like are words, not counts
++assert.ok(counts.length >= 4, `the page states its count in several places (found ${counts.length})`);
++assert.deepStrictEqual([...new Set(counts)], [expected], `every count of sites on the home is ${expected}: ${counts.join(', ')}`);
++const sites = sitesPage.render();
++assert.ok(sites.includes(`All ${expected} OpenVibe sites`), '/sites says the same number');
++console.log(`home site count: ${counts.length} mentions, all ${expected}, the same as /sites`);