openvibers/openvibe.network Commits
Home: one count of the sites, the same as /sites (#572)
Changes
@@@ -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;}@@@ -96,7 +96,7 @@ for (const id of ['network', 'tools', 'account', 'developers']) assert.ok(new Reassert.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]);nnew file mode 100644@@@ -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`);
@@ -29,6 +29,11 @@ const social = require('openvibe-shared/social');
@@ -29,6 +29,11 @@ const social = require('openvibe-shared/social');
// OpenRestream is its own name, never "OpenVibe.OpenRestream" (the owner, 2026-10-28).
// OpenRestream is its own name, never "OpenVibe.OpenRestream" (the owner, 2026-10-28).
const siteName = (site) => (site.name.includes('.') ? site.name : fullName(site));
const siteName = (site) => (site.name.includes('.') ? site.name : fullName(site));
const OPEN = frameSites.filter((site) => site.status === 'open' && site.id !== 'network');
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 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 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 inWords = (n) => WORDS[n] || String(n);
@@ -102,11 +107,11 @@ const SHORT = (site) => site.name.replace(/^OpenVibe\./, '');
@@ -102,11 +107,11 @@ const SHORT = (site) => site.name.replace(/^OpenVibe\./, '');
function orbit() {
function orbit() {
const inner = OPEN.slice(0, 6), outer = OPEN.slice(6, 16);
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>`;
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-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 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>
<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>`;
</div>`;
}
}
@@ -195,7 +200,7 @@ function numbers(catalog, a) {
@@ -195,7 +200,7 @@ function numbers(catalog, a) {
const c = a.counts || {};
const c = a.counts || {};
const items = [
const items = [
c.services ? [fmtCount(c.services.up), 'services up', `of ${c.services.live} checked`, '/status'] : null,
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,
catalog.tools.length ? [fmtCount(catalog.tools.length), 'online tools', '', 'https://openvibe.tools/'] : null,
c.accounts ? [fmtCount(c.accounts), 'accounts', '', '/login?tab=register'] : 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,
a.liveCount ? [fmtCount(a.liveCount), a.liveCount === 1 ? 'stream live now' : 'streams live now', '', 'https://openvibe.live/'] : null,
@@ -321,7 +326,7 @@ ${icons.CSS}
@@ -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
// 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).
// 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?"). */
/** 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()}.`);
const asSentence = (t) => (/[.!?…]$/.test(String(t).trim()) ? String(t).trim() : `${String(t).trim()}.`);
@@ -366,7 +371,7 @@ function body(catalog, act) {
@@ -366,7 +371,7 @@ function body(catalog, act) {
const sites = showcase.apps({
const sites = showcase.apps({
id: 'network', title: sitesHeading(),
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.',
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).
// 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) => {
const foldSites = (html) => {
@@ -413,9 +418,9 @@ ${moreParts([['tools', 'Tools', tools], ['account', 'Your account', account], ['
@@ -413,9 +418,9 @@ ${moreParts([['tools', 'Tools', tools], ['account', 'Your account', account], ['
function summaryOf(catalog) {
function summaryOf(catalog) {
return seo.pageSummary({
return seo.pageSummary({
title: 'OpenVibe.Network',
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/',
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,
updated: catalog.tools.length ? String(catalog.updated).slice(0, 10) : undefined,
});
});
}
}
@@ -442,12 +447,12 @@ function render({ releaseId = '' } = {}) {
@@ -442,12 +447,12 @@ function render({ releaseId = '' } = {}) {
const ld = seo.jsonLdTag(seo.jsonLd.website({
const ld = seo.jsonLdTag(seo.jsonLd.website({
name: 'OpenVibe.Network',
name: 'OpenVibe.Network',
url: 'https://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}',
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()));
})) + 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 card = social.url('network', 'https://openvibe.network/shared') || 'https://openvibe.network/assets/logo-512.png';
const summary = summaryOf(catalog);
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) + '"' };
pageCache = { key, html, etag: '"' + crypto.createHash('sha1').update(html).digest('base64url').slice(0, 20) + '"' };
return pageCache;
return pageCache;
}
}
@@ -96,7 +96,7 @@ for (const id of ['network', 'tools', 'account', 'developers']) assert.ok(new Re
@@ -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.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.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.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');
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.
// 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]);
const siteLd = JSON.parse(/<script type="application\/ld\+json">(\{"@context":"https:\/\/schema.org","@type":"WebSite"[\s\S]*?)<\/script>/.exec(html)[1]);
new file mode 100644
new file mode 100644
@@ -0,0 +1,22 @@
@@ -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`);