openvibers/openvibe.shared Commits
4.22.0: a tidier navbar (no repeated sites, More for the rest) (#146)
Changes
@@@ -6,6 +6,28 @@ A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).## Unreleased++## 4.22.0 — 2026-10-28++++- **A tidier navbar** (the owner, 2026-10-28: "reanalyze all the nav bars across all the sites… redundant feeling++ options… awkward… not front-facing of what the important stuff is"). A sweep of the 38 sites' bars found three++ problems:++ - **Other sites repeated** after every site's own links ("Live · Tools · Space · Community"), twice on Codes and++ News.++ - The bar no longer repeats them: the launcher lists every site, and the phone drawer keeps "Across OpenVibe".++ - `networkLinks: <n>` (or `true` for four) brings the strip back for a site that wants it.++ - **Long bars**, up to twelve entries. Links marked `more: true` now fold into one "More" dropdown, and the drawer++ lists them like the others. Clicking More opens it; it never follows a link.++ - **"What shipped" and "Search" as main places.** docs/kit.md ("The navbar's links") sets the rules: four or five++ places with icons, `more` for the rest, Search as the magnifier, Updates in the footer, the account menu for the++ person's things.++- **Who OpenVibe is, in every site's structured data** (`seo.jsonLd.organization()`, the publisher every page names).++ - The Organization gains a description, `alternateName` (OpenVibe Network, OpenVibers), a++ `disambiguatingDescription` ("…not the Openvibe mobile social-media aggregator app"), and fifteen sites in `sameAs`.++ - Why: Google's AI Overview for "whois openvibe" described an unrelated "Openvibe" app (the owner, 2026-10-28).++ - Network's home page and llms.txt say the same (Network #368).++- `test/navbar-more.test.js`; `test/seo-org.test.js`; `perf-budget-chrome` gives a loaded runner three tries to report++ the late block's shift (it failed macOS CI twice on 2026-10-28).++## 4.21.0 — 2026-10-28- **Search on every site** (`search.js`; navbar `search`, `openSearch()`).@@@ -2,7 +2,7 @@> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.--**Status:** alpha, v4.21.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed++**Status:** alpha, v4.22.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployedconsumer installs a tagged release and none keeps a vendored copy([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` namesthe tag it pins.@@@ -4,7 +4,7 @@"code": true,"wave": "W2","package": "openvibe-shared",-- "version": "4.21.0",++ "version": "4.22.0","plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)","deployed": false,"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",@@@ -196,6 +196,36 @@ kit.badge(text, 'accent' | 'ok' | 'warn' | 'bad' | 'live') · kit.dot(n) · kit.`search`, `gear`, `members`, `invite`, `chat`, `bell`, `home`, `spark`, `live`, `video`, `image`, `tag`, `coin`,`trophy`, `map`, `code`, `robot` and `game`.++## The navbar's links++++The 2026-10-28 sweep of all 38 sites' bars found three habits that made them feel crowded and repetitive:++- other sites repeated after every site's own links ("Live · Tools · Space · Community", twice on some);++- "What shipped" and "Search" as main places;++- up to twelve entries.++++The rules since 4.22.0:++++1. **Four or five main places, with icons**, in the order people use them: the site's home or feed, finding (browse,++ categories), what is theirs (watches, tickets, saved), and the site's main verb (post, go live, write). The same++ icons make the phone app bar (`appBar: true`).++2. **The rest is `more: true`.** Those links fold into one "More" on computers and are listed in the phone drawer:++ guides, sources, "how it works", the API.++3. **Search is the magnifier**, not a link: `search: { owner, label, page }` points it at the site's own results and++ search page.++4. **"What shipped" lives in the footer** (Updates), which every site has.++5. **The account menu holds the person's things** (dashboard, settings, admin), and the launcher holds the other sites.++ The bar no longer repeats them (`networkLinks` brings the strip back for a site that really wants it).++++```js++OpenVibeNavbar.init({ service: 'food', links: [++ { label: 'Food near you', href: '/near', icon: 'fa-location-dot' },++ { label: 'Food boxes', href: '/boxes', icon: 'fa-utensils' },++ { label: 'Meal plan', href: '/plan', icon: 'fa-list-check' },++ { label: 'Cupboard', href: '/pantry', icon: 'fa-house' },++ { label: 'Food list', href: '/foods', more: true },++], appBar: true, search: { owner: 'food', label: 'OpenVibe.Food' } })++```++## The app bar```js@@@ -852,6 +852,7 @@const item = byId.get(a.getAttribute('data-link-id'));// A parent of a dropdown opens it on touch (there is no hover); a second tap follows the link.const dd = a.parentElement && a.parentElement.classList.contains('ovnav-dd') ? a.parentElement : null;++ if (dd && a.getAttribute('data-link-id') === 'ovnav-more') { e.preventDefault(); const o = !dd.classList.contains('open'); nav.querySelectorAll('.ovnav-dd.open').forEach(x => x.classList.remove('open')); if (o) { dd.classList.add('open'); placeDd(dd); } return; }if (dd && matchMedia('(hover: none)').matches && !dd.classList.contains('open')) { e.preventDefault(); nav.querySelectorAll('.ovnav-dd.open').forEach(x => x.classList.remove('open')); dd.classList.add('open'); placeDd(dd); return; }if (item && typeof item.onClick === 'function') { const r = item.onClick(e); if (r === false) e.preventDefault(); closeDrawer(); if (e.defaultPrevented) return; }if (typeof _config.onNavigate === 'function' && a.target !== '_blank') {@@@ -946,7 +947,8 @@if (!frame) return '';const here = currentHost().toLowerCase();const taken = new Set((pageLinks || []).map(l => { try { return new URL(l.href, location.href).hostname; } catch { return ''; } }));-- const max = inDrawer ? 8 : (typeof _config.networkLinks === 'number' ? _config.networkLinks : 4);++ const max = inDrawer ? 8 : (typeof _config.networkLinks === 'number' ? _config.networkLinks : _config.networkLinks === true ? 4 : 0);++ if (!max) return '';const pick = frame.nav.filter(n => { try { const h = new URL(n.url).hostname; return h !== here && !here.endsWith('.' + h) && !taken.has(h); } catch { return false; } }).slice(0, max);if (!pick.length) return '';if (inDrawer) {@@@ -1645,7 +1647,7 @@nav.innerHTML = `${brandHTML(brand)}<div class="openvibe-navbar-links">-- ${links.map(l => linkHTML(l, false)).join('')}++ ${links.filter(l => !l.more).map(l => linkHTML(l, false)).join('')}${links.some(l => l.more && !l.hidden) ? linkHTML({ id: 'ovnav-more', label: 'More', href: '#', children: links.filter(l => l.more) }, false) : ''}${networkLinksHTML(links)}${u && u.role === 'admin' && _config.adminLink !== false && !flatLinks(links).some(l => l.id === 'admin') ? `<a href="https://openvibe.network/admin">${navIcon('fa-shield-halved')} Admin</a>` : ''}</div>@@@ -1,6 +1,6 @@{"name": "openvibe-shared",-- "version": "4.21.0",++ "version": "4.22.0","description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site","main": "index.js","bin": {@@@ -86,7 +86,18 @@ function headTags(o = {}) {return out.join('\n');}--const ORG = { '@type': 'Organization', '@id': 'https://openvibe.network/#org', name: 'OpenVibe', url: 'https://openvibe.network/', logo: 'https://openvibe.network/assets/logo-512.png', sameAs: ['https://github.com/OpenVibers', 'https://discord.gg/M6MuRUaeJj'] };++// OpenVibe, said so search engines and AI cannot take it for something else (the owner, 2026-10-28: Google's AI Overview++// for "whois openvibe" described an unrelated "Openvibe" mobile app): what it is, its other names, what it is not, and its++// sites. Network's home page carries the same Organization (#org); every site's pages name it as their publisher.++const ORG = {++ '@type': 'Organization', '@id': 'https://openvibe.network/#org', name: 'OpenVibe', alternateName: ['OpenVibe Network', 'OpenVibers'],++ url: 'https://openvibe.network/', logo: 'https://openvibe.network/assets/logo-512.png',++ description: 'OpenVibe is an open-source, community-run network of websites that share one free account: live streaming from the browser (OpenVibe.Live), more than 180 online tools (OpenVibe.Tools), chat and the Vibe assistant (OpenVibe.Chat), games, a wiki, news, reviews, deals and an open developer platform.',++ disambiguatingDescription: 'The open-source network of websites at openvibe.network, made by OpenVibers; not the Openvibe mobile social-media aggregator app.',++ sameAs: ['https://github.com/OpenVibers', 'https://discord.gg/M6MuRUaeJj', 'https://openvibe.live', 'https://openvibe.tools', 'https://openvibe.chat', 'https://openvibe.games',++ 'https://openvibe.media', 'https://openvibe.video', 'https://openvibe.community', 'https://openvibe.space', 'https://openvibe.wiki', 'https://openvibe.blog',++ 'https://openvibe.news', 'https://openvibe.services', 'https://openre.stream'],++};const jsonLd = {organization(extra = {}) { return { '@context': 'https://schema.org', ...ORG, ...extra }; },nnew file mode 100644@@@ -0,0 +1,63 @@++'use strict';++// navbar.js 4.22.0 — a tidier bar (the owner, 2026-10-28: "reanalyze all the nav bars… redundant feeling options…++// not front-facing of what the important stuff is"): other sites are no longer repeated after this site's links (the++// launcher lists them all), and links marked more: true fold into one "More" dropdown (the phone drawer lists them).++const assert = require('assert');++const path = require('path');++const NAVBAR_PATH = path.join(__dirname, '..', 'navbar.js');++++function stubElement(tag) {++ const el = {++ tag, style: { setProperty() {} }, textContent: '', className: '', id: '', innerHTML: '', dataset: {}, attrs: {}, children: [],++ setAttribute(k, v) { this.attrs[k] = String(v); }, getAttribute(k) { return this.attrs[k] ?? null; },++ appendChild(c) { this.children.push(c); return c; }, prepend(c) { this.children.unshift(c); }, remove() {}, addEventListener() {},++ querySelector() { return null; }, querySelectorAll() { return []; },++ };++ el.classList = { add() {}, remove() {}, toggle() {}, contains: () => false };++ return el;++}++function load() {++ global.location = { hostname: 'openvibe.food', href: 'https://openvibe.food/', pathname: '/', protocol: 'https:', origin: 'https://openvibe.food' };++ global.window = { location: global.location };++ try { Object.defineProperty(global, 'navigator', { value: { userAgent: 'test' }, configurable: true }); } catch { /* */ }++ const body = stubElement('body');++ const created = [];++ global.document = { cookie: '', head: stubElement('head'), body, documentElement: stubElement('html'), title: 'T', getElementById: () => null,++ createElement: (tag) => { const e = stubElement(tag); created.push(e); return e; }, addEventListener() {}, removeEventListener() {}, dispatchEvent() {} };++ global.CustomEvent = class { constructor(t, i) { this.type = t; this.detail = i && i.detail; } };++ const store = {};++ global.localStorage = { getItem: (k) => (k in store ? store[k] : null), setItem: (k, v) => { store[k] = String(v); }, removeItem: (k) => { delete store[k]; } };++ global.sessionStorage = { getItem: () => null, setItem() {}, removeItem() {} };++ // The Frame's site list (what the launcher and the old strip read), as Network's /api/chrome would give it.++ global.fetch = async () => ({ ok: true, status: 200, json: async () => ({ nav: [{ id: 'live', name: 'Live', url: 'https://openvibe.live/', icon: 'live' }, { id: 'tools', name: 'Tools', url: 'https://openvibe.tools/', icon: 'tools' }], groups: [] }) });++ delete globalThis.OVFrame;++ delete require.cache[require.resolve(NAVBAR_PATH)];++ return { navbar: require(NAVBAR_PATH), created };++}++const barHtml = (created) => (created.filter((e) => e.tag === 'nav' && /openvibe-navbar-links/.test(e.innerHTML)).pop() || { innerHTML: '' }).innerHTML;++const between = (html, a, b) => { const i = html.indexOf(a); const j = html.indexOf(b, i); return i < 0 ? '' : html.slice(i, j < 0 ? undefined : j); };++++{++ const { navbar, created } = load();++ navbar.init({ service: 'food', user: null, links: [++ { label: 'Food near you', href: '/near', icon: 'fa-location-dot' },++ { label: 'Food boxes', href: '/boxes', icon: 'fa-utensils' },++ { label: 'Food list', href: '/foods', more: true },++ { label: 'What shipped', href: '/updates', more: true },++ ] });++ const bar = between(barHtml(created), 'openvibe-navbar-links', 'openvibe-navbar-spacer');++ assert.ok(bar.includes('>Food near you<') && bar.includes('>Food boxes<'), 'the main places are on the bar');++ assert.ok(/data-link-id="ovnav-more"[^>]*>(<[^>]+>)*More/.test(bar) || bar.includes('data-link-id="ovnav-more"'), 'one More');++ const more = between(bar, 'ovnav-dd-menu', '</div>');++ assert.ok(more.includes('>Food list<') && more.includes('>What shipped<'), 'secondary links are inside More');++ assert.ok(!/class="ovnav-net"/.test(bar), 'no other sites repeated on the bar');++ const drawer = between(barHtml(created), 'id="openvibe-drawer"', 'ovnav-drawer-net');++ assert.ok(drawer.includes('>Food list<') && drawer.includes('>What shipped<'), 'the drawer lists every link');++ assert.ok(!drawer.includes('ovnav-more'), 'and no More in the drawer');++}++{++ const { navbar, created } = load();++ navbar.init({ service: 'food', user: null, links: [{ label: 'Near', href: '/near' }] });++ assert.ok(!barHtml(created).includes('data-link-id="ovnav-more"'), 'no secondary links: no More');++}++console.log('navbar more: secondary links fold into More, other sites leave the bar ok');@@@ -31,7 +31,9 @@ document.addEventListener('click', () => { const t = performance.now(); while (pconst tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-perf-budget-test-'));try {// launch() options: this call starts Chrome and stops it again.-- const m = await measure({ base, browser: { tmpDir } });++ // A loaded CI runner (macOS) sometimes reports the late block's shift a frame too late: up to three tries.++ const shifted = async (run) => { let r; for (let i = 0; i < 3; i++) { r = await run(); if (r.cwv.cls > 0 && r.cwv.lcpMs > 0) break; } return r; };++ const m = await shifted(() => measure({ base, browser: { tmpDir } }));console.log('perf-budget-chrome: cwv', JSON.stringify(m.cwv));assert.ok(m.cwv.cls > 0, `the late block shifts the text: ${JSON.stringify(m.cwv)}`);assert.ok(m.cwv.lcpMs > 0, 'the text paints');@@@ -42,7 +44,7 @@ document.addEventListener('click', () => { const t = performance.now(); while (p// A running browser is reused and left running.const running = await h.launch({ tmpDir });try {-- const again = await measure({ base, browser: { running } });++ const again = await shifted(() => measure({ base, browser: { running } }));assert.ok(again.cwv.cls > 0 && again.cwv.lcpMs > 0, JSON.stringify(again.cwv));assert.strictEqual(running.closed, false, 'a browser the caller started stays open');} finally { await running.close(); }nnew file mode 100644@@@ -0,0 +1,15 @@++'use strict';++// seo.jsonLd.organization(): who OpenVibe is, in every site's structured data (4.22.0), so search engines and AI cannot++// take it for something else: the one @id, its other names, what it is, what it is not, and its sites.++const assert = require('assert');++const seo = require('../seo');++++const org = seo.jsonLd.organization();++assert.strictEqual(org['@id'], 'https://openvibe.network/#org');++assert.strictEqual(org.name, 'OpenVibe');++assert.deepStrictEqual(org.alternateName, ['OpenVibe Network', 'OpenVibers']);++assert.ok(/open-source, community-run network of websites/.test(org.description));++assert.ok(/not the Openvibe mobile social-media aggregator app/.test(org.disambiguatingDescription));++['https://github.com/OpenVibers', 'https://openvibe.live', 'https://openvibe.tools', 'https://openvibe.chat', 'https://openre.stream'].forEach((u) => assert.ok(org.sameAs.includes(u), u));++assert.strictEqual(seo.jsonLd.website({ name: 'X', url: 'https://x.example/', description: 'd' }).publisher['@id'], org['@id'], 'a WebSite names it by @id');++console.log('seo org: who OpenVibe is ok');
@@ -6,6 +6,28 @@ A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
@@ -6,6 +6,28 @@ A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
## Unreleased
## Unreleased
+## 4.22.0 — 2026-10-28
+
+- **A tidier navbar** (the owner, 2026-10-28: "reanalyze all the nav bars across all the sites… redundant feeling
+ options… awkward… not front-facing of what the important stuff is"). A sweep of the 38 sites' bars found three
+ problems:
+ - **Other sites repeated** after every site's own links ("Live · Tools · Space · Community"), twice on Codes and
+ News.
+ - The bar no longer repeats them: the launcher lists every site, and the phone drawer keeps "Across OpenVibe".
+ - `networkLinks: <n>` (or `true` for four) brings the strip back for a site that wants it.
+ - **Long bars**, up to twelve entries. Links marked `more: true` now fold into one "More" dropdown, and the drawer
+ lists them like the others. Clicking More opens it; it never follows a link.
+ - **"What shipped" and "Search" as main places.** docs/kit.md ("The navbar's links") sets the rules: four or five
+ places with icons, `more` for the rest, Search as the magnifier, Updates in the footer, the account menu for the
+ person's things.
+- **Who OpenVibe is, in every site's structured data** (`seo.jsonLd.organization()`, the publisher every page names).
+ - The Organization gains a description, `alternateName` (OpenVibe Network, OpenVibers), a
+ `disambiguatingDescription` ("…not the Openvibe mobile social-media aggregator app"), and fifteen sites in `sameAs`.
+ - Why: Google's AI Overview for "whois openvibe" described an unrelated "Openvibe" app (the owner, 2026-10-28).
+ - Network's home page and llms.txt say the same (Network #368).
+- `test/navbar-more.test.js`; `test/seo-org.test.js`; `perf-budget-chrome` gives a loaded runner three tries to report
+ the late block's shift (it failed macOS CI twice on 2026-10-28).
+
## 4.21.0 — 2026-10-28
## 4.21.0 — 2026-10-28
- **Search on every site** (`search.js`; navbar `search`, `openSearch()`).
- **Search on every site** (`search.js`; navbar `search`, `openSearch()`).
@@ -2,7 +2,7 @@
@@ -2,7 +2,7 @@
> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.
> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.
-**Status:** alpha, v4.21.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
+**Status:** alpha, v4.22.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
consumer installs a tagged release and none keeps a vendored copy
consumer installs a tagged release and none keeps a vendored copy
([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` names
([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` names
the tag it pins.
the tag it pins.
@@ -4,7 +4,7 @@
@@ -4,7 +4,7 @@
"code": true,
"code": true,
"wave": "W2",
"wave": "W2",
"package": "openvibe-shared",
"package": "openvibe-shared",
- "version": "4.21.0",
+ "version": "4.22.0",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"deployed": false,
"deployed": false,
"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",
"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",
@@ -196,6 +196,36 @@ kit.badge(text, 'accent' | 'ok' | 'warn' | 'bad' | 'live') · kit.dot(n) · kit.
@@ -196,6 +196,36 @@ kit.badge(text, 'accent' | 'ok' | 'warn' | 'bad' | 'live') · kit.dot(n) · kit.
`search`, `gear`, `members`, `invite`, `chat`, `bell`, `home`, `spark`, `live`, `video`, `image`, `tag`, `coin`,
`search`, `gear`, `members`, `invite`, `chat`, `bell`, `home`, `spark`, `live`, `video`, `image`, `tag`, `coin`,
`trophy`, `map`, `code`, `robot` and `game`.
`trophy`, `map`, `code`, `robot` and `game`.
+## The navbar's links
+
+The 2026-10-28 sweep of all 38 sites' bars found three habits that made them feel crowded and repetitive:
+- other sites repeated after every site's own links ("Live · Tools · Space · Community", twice on some);
+- "What shipped" and "Search" as main places;
+- up to twelve entries.
+
+The rules since 4.22.0:
+
+1. **Four or five main places, with icons**, in the order people use them: the site's home or feed, finding (browse,
+ categories), what is theirs (watches, tickets, saved), and the site's main verb (post, go live, write). The same
+ icons make the phone app bar (`appBar: true`).
+2. **The rest is `more: true`.** Those links fold into one "More" on computers and are listed in the phone drawer:
+ guides, sources, "how it works", the API.
+3. **Search is the magnifier**, not a link: `search: { owner, label, page }` points it at the site's own results and
+ search page.
+4. **"What shipped" lives in the footer** (Updates), which every site has.
+5. **The account menu holds the person's things** (dashboard, settings, admin), and the launcher holds the other sites.
+ The bar no longer repeats them (`networkLinks` brings the strip back for a site that really wants it).
+
+```js
+OpenVibeNavbar.init({ service: 'food', links: [
+ { label: 'Food near you', href: '/near', icon: 'fa-location-dot' },
+ { label: 'Food boxes', href: '/boxes', icon: 'fa-utensils' },
+ { label: 'Meal plan', href: '/plan', icon: 'fa-list-check' },
+ { label: 'Cupboard', href: '/pantry', icon: 'fa-house' },
+ { label: 'Food list', href: '/foods', more: true },
+], appBar: true, search: { owner: 'food', label: 'OpenVibe.Food' } })
+```
+
## The app bar
## The app bar
```js
```js
@@ -852,6 +852,7 @@
@@ -852,6 +852,7 @@
const item = byId.get(a.getAttribute('data-link-id'));
const item = byId.get(a.getAttribute('data-link-id'));
// A parent of a dropdown opens it on touch (there is no hover); a second tap follows the link.
// A parent of a dropdown opens it on touch (there is no hover); a second tap follows the link.
const dd = a.parentElement && a.parentElement.classList.contains('ovnav-dd') ? a.parentElement : null;
const dd = a.parentElement && a.parentElement.classList.contains('ovnav-dd') ? a.parentElement : null;
+ if (dd && a.getAttribute('data-link-id') === 'ovnav-more') { e.preventDefault(); const o = !dd.classList.contains('open'); nav.querySelectorAll('.ovnav-dd.open').forEach(x => x.classList.remove('open')); if (o) { dd.classList.add('open'); placeDd(dd); } return; }
if (dd && matchMedia('(hover: none)').matches && !dd.classList.contains('open')) { e.preventDefault(); nav.querySelectorAll('.ovnav-dd.open').forEach(x => x.classList.remove('open')); dd.classList.add('open'); placeDd(dd); return; }
if (dd && matchMedia('(hover: none)').matches && !dd.classList.contains('open')) { e.preventDefault(); nav.querySelectorAll('.ovnav-dd.open').forEach(x => x.classList.remove('open')); dd.classList.add('open'); placeDd(dd); return; }
if (item && typeof item.onClick === 'function') { const r = item.onClick(e); if (r === false) e.preventDefault(); closeDrawer(); if (e.defaultPrevented) return; }
if (item && typeof item.onClick === 'function') { const r = item.onClick(e); if (r === false) e.preventDefault(); closeDrawer(); if (e.defaultPrevented) return; }
if (typeof _config.onNavigate === 'function' && a.target !== '_blank') {
if (typeof _config.onNavigate === 'function' && a.target !== '_blank') {
@@ -946,7 +947,8 @@
@@ -946,7 +947,8 @@
if (!frame) return '';
if (!frame) return '';
const here = currentHost().toLowerCase();
const here = currentHost().toLowerCase();
const taken = new Set((pageLinks || []).map(l => { try { return new URL(l.href, location.href).hostname; } catch { return ''; } }));
const taken = new Set((pageLinks || []).map(l => { try { return new URL(l.href, location.href).hostname; } catch { return ''; } }));
- const max = inDrawer ? 8 : (typeof _config.networkLinks === 'number' ? _config.networkLinks : 4);
+ const max = inDrawer ? 8 : (typeof _config.networkLinks === 'number' ? _config.networkLinks : _config.networkLinks === true ? 4 : 0);
+ if (!max) return '';
const pick = frame.nav.filter(n => { try { const h = new URL(n.url).hostname; return h !== here && !here.endsWith('.' + h) && !taken.has(h); } catch { return false; } }).slice(0, max);
const pick = frame.nav.filter(n => { try { const h = new URL(n.url).hostname; return h !== here && !here.endsWith('.' + h) && !taken.has(h); } catch { return false; } }).slice(0, max);
if (!pick.length) return '';
if (!pick.length) return '';
if (inDrawer) {
if (inDrawer) {
@@ -1645,7 +1647,7 @@
@@ -1645,7 +1647,7 @@
nav.innerHTML = `
nav.innerHTML = `
${brandHTML(brand)}
${brandHTML(brand)}
<div class="openvibe-navbar-links">
<div class="openvibe-navbar-links">
- ${links.map(l => linkHTML(l, false)).join('')}
+ ${links.filter(l => !l.more).map(l => linkHTML(l, false)).join('')}${links.some(l => l.more && !l.hidden) ? linkHTML({ id: 'ovnav-more', label: 'More', href: '#', children: links.filter(l => l.more) }, false) : ''}
${networkLinksHTML(links)}
${networkLinksHTML(links)}
${u && u.role === 'admin' && _config.adminLink !== false && !flatLinks(links).some(l => l.id === 'admin') ? `<a href="https://openvibe.network/admin">${navIcon('fa-shield-halved')} Admin</a>` : ''}
${u && u.role === 'admin' && _config.adminLink !== false && !flatLinks(links).some(l => l.id === 'admin') ? `<a href="https://openvibe.network/admin">${navIcon('fa-shield-halved')} Admin</a>` : ''}
</div>
</div>
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "4.21.0",
+ "version": "4.22.0",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"main": "index.js",
"bin": {
"bin": {
@@ -86,7 +86,18 @@ function headTags(o = {}) {
@@ -86,7 +86,18 @@ function headTags(o = {}) {
return out.join('\n');
return out.join('\n');
}
}
-const ORG = { '@type': 'Organization', '@id': 'https://openvibe.network/#org', name: 'OpenVibe', url: 'https://openvibe.network/', logo: 'https://openvibe.network/assets/logo-512.png', sameAs: ['https://github.com/OpenVibers', 'https://discord.gg/M6MuRUaeJj'] };
+// OpenVibe, said so search engines and AI cannot take it for something else (the owner, 2026-10-28: Google's AI Overview
+// for "whois openvibe" described an unrelated "Openvibe" mobile app): what it is, its other names, what it is not, and its
+// sites. Network's home page carries the same Organization (#org); every site's pages name it as their publisher.
+const ORG = {
+ '@type': 'Organization', '@id': 'https://openvibe.network/#org', name: 'OpenVibe', alternateName: ['OpenVibe Network', 'OpenVibers'],
+ url: 'https://openvibe.network/', logo: 'https://openvibe.network/assets/logo-512.png',
+ description: 'OpenVibe is an open-source, community-run network of websites that share one free account: live streaming from the browser (OpenVibe.Live), more than 180 online tools (OpenVibe.Tools), chat and the Vibe assistant (OpenVibe.Chat), games, a wiki, news, reviews, deals and an open developer platform.',
+ disambiguatingDescription: 'The open-source network of websites at openvibe.network, made by OpenVibers; not the Openvibe mobile social-media aggregator app.',
+ sameAs: ['https://github.com/OpenVibers', 'https://discord.gg/M6MuRUaeJj', 'https://openvibe.live', 'https://openvibe.tools', 'https://openvibe.chat', 'https://openvibe.games',
+ 'https://openvibe.media', 'https://openvibe.video', 'https://openvibe.community', 'https://openvibe.space', 'https://openvibe.wiki', 'https://openvibe.blog',
+ 'https://openvibe.news', 'https://openvibe.services', 'https://openre.stream'],
+};
const jsonLd = {
const jsonLd = {
organization(extra = {}) { return { '@context': 'https://schema.org', ...ORG, ...extra }; },
organization(extra = {}) { return { '@context': 'https://schema.org', ...ORG, ...extra }; },
new file mode 100644
new file mode 100644
@@ -0,0 +1,63 @@
@@ -0,0 +1,63 @@
+'use strict';
+// navbar.js 4.22.0 — a tidier bar (the owner, 2026-10-28: "reanalyze all the nav bars… redundant feeling options…
+// not front-facing of what the important stuff is"): other sites are no longer repeated after this site's links (the
+// launcher lists them all), and links marked more: true fold into one "More" dropdown (the phone drawer lists them).
+const assert = require('assert');
+const path = require('path');
+const NAVBAR_PATH = path.join(__dirname, '..', 'navbar.js');
+
+function stubElement(tag) {
+ const el = {
+ tag, style: { setProperty() {} }, textContent: '', className: '', id: '', innerHTML: '', dataset: {}, attrs: {}, children: [],
+ setAttribute(k, v) { this.attrs[k] = String(v); }, getAttribute(k) { return this.attrs[k] ?? null; },
+ appendChild(c) { this.children.push(c); return c; }, prepend(c) { this.children.unshift(c); }, remove() {}, addEventListener() {},
+ querySelector() { return null; }, querySelectorAll() { return []; },
+ };
+ el.classList = { add() {}, remove() {}, toggle() {}, contains: () => false };
+ return el;
+}
+function load() {
+ global.location = { hostname: 'openvibe.food', href: 'https://openvibe.food/', pathname: '/', protocol: 'https:', origin: 'https://openvibe.food' };
+ global.window = { location: global.location };
+ try { Object.defineProperty(global, 'navigator', { value: { userAgent: 'test' }, configurable: true }); } catch { /* */ }
+ const body = stubElement('body');
+ const created = [];
+ global.document = { cookie: '', head: stubElement('head'), body, documentElement: stubElement('html'), title: 'T', getElementById: () => null,
+ createElement: (tag) => { const e = stubElement(tag); created.push(e); return e; }, addEventListener() {}, removeEventListener() {}, dispatchEvent() {} };
+ global.CustomEvent = class { constructor(t, i) { this.type = t; this.detail = i && i.detail; } };
+ const store = {};
+ global.localStorage = { getItem: (k) => (k in store ? store[k] : null), setItem: (k, v) => { store[k] = String(v); }, removeItem: (k) => { delete store[k]; } };
+ global.sessionStorage = { getItem: () => null, setItem() {}, removeItem() {} };
+ // The Frame's site list (what the launcher and the old strip read), as Network's /api/chrome would give it.
+ global.fetch = async () => ({ ok: true, status: 200, json: async () => ({ nav: [{ id: 'live', name: 'Live', url: 'https://openvibe.live/', icon: 'live' }, { id: 'tools', name: 'Tools', url: 'https://openvibe.tools/', icon: 'tools' }], groups: [] }) });
+ delete globalThis.OVFrame;
+ delete require.cache[require.resolve(NAVBAR_PATH)];
+ return { navbar: require(NAVBAR_PATH), created };
+}
+const barHtml = (created) => (created.filter((e) => e.tag === 'nav' && /openvibe-navbar-links/.test(e.innerHTML)).pop() || { innerHTML: '' }).innerHTML;
+const between = (html, a, b) => { const i = html.indexOf(a); const j = html.indexOf(b, i); return i < 0 ? '' : html.slice(i, j < 0 ? undefined : j); };
+
+{
+ const { navbar, created } = load();
+ navbar.init({ service: 'food', user: null, links: [
+ { label: 'Food near you', href: '/near', icon: 'fa-location-dot' },
+ { label: 'Food boxes', href: '/boxes', icon: 'fa-utensils' },
+ { label: 'Food list', href: '/foods', more: true },
+ { label: 'What shipped', href: '/updates', more: true },
+ ] });
+ const bar = between(barHtml(created), 'openvibe-navbar-links', 'openvibe-navbar-spacer');
+ assert.ok(bar.includes('>Food near you<') && bar.includes('>Food boxes<'), 'the main places are on the bar');
+ assert.ok(/data-link-id="ovnav-more"[^>]*>(<[^>]+>)*More/.test(bar) || bar.includes('data-link-id="ovnav-more"'), 'one More');
+ const more = between(bar, 'ovnav-dd-menu', '</div>');
+ assert.ok(more.includes('>Food list<') && more.includes('>What shipped<'), 'secondary links are inside More');
+ assert.ok(!/class="ovnav-net"/.test(bar), 'no other sites repeated on the bar');
+ const drawer = between(barHtml(created), 'id="openvibe-drawer"', 'ovnav-drawer-net');
+ assert.ok(drawer.includes('>Food list<') && drawer.includes('>What shipped<'), 'the drawer lists every link');
+ assert.ok(!drawer.includes('ovnav-more'), 'and no More in the drawer');
+}
+{
+ const { navbar, created } = load();
+ navbar.init({ service: 'food', user: null, links: [{ label: 'Near', href: '/near' }] });
+ assert.ok(!barHtml(created).includes('data-link-id="ovnav-more"'), 'no secondary links: no More');
+}
+console.log('navbar more: secondary links fold into More, other sites leave the bar ok');
@@ -31,7 +31,9 @@ document.addEventListener('click', () => { const t = performance.now(); while (p
@@ -31,7 +31,9 @@ document.addEventListener('click', () => { const t = performance.now(); while (p
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-perf-budget-test-'));
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-perf-budget-test-'));
try {
try {
// launch() options: this call starts Chrome and stops it again.
// launch() options: this call starts Chrome and stops it again.
- const m = await measure({ base, browser: { tmpDir } });
+ // A loaded CI runner (macOS) sometimes reports the late block's shift a frame too late: up to three tries.
+ const shifted = async (run) => { let r; for (let i = 0; i < 3; i++) { r = await run(); if (r.cwv.cls > 0 && r.cwv.lcpMs > 0) break; } return r; };
+ const m = await shifted(() => measure({ base, browser: { tmpDir } }));
console.log('perf-budget-chrome: cwv', JSON.stringify(m.cwv));
console.log('perf-budget-chrome: cwv', JSON.stringify(m.cwv));
assert.ok(m.cwv.cls > 0, `the late block shifts the text: ${JSON.stringify(m.cwv)}`);
assert.ok(m.cwv.cls > 0, `the late block shifts the text: ${JSON.stringify(m.cwv)}`);
assert.ok(m.cwv.lcpMs > 0, 'the text paints');
assert.ok(m.cwv.lcpMs > 0, 'the text paints');
@@ -42,7 +44,7 @@ document.addEventListener('click', () => { const t = performance.now(); while (p
@@ -42,7 +44,7 @@ document.addEventListener('click', () => { const t = performance.now(); while (p
// A running browser is reused and left running.
// A running browser is reused and left running.
const running = await h.launch({ tmpDir });
const running = await h.launch({ tmpDir });
try {
try {
- const again = await measure({ base, browser: { running } });
+ const again = await shifted(() => measure({ base, browser: { running } }));
assert.ok(again.cwv.cls > 0 && again.cwv.lcpMs > 0, JSON.stringify(again.cwv));
assert.ok(again.cwv.cls > 0 && again.cwv.lcpMs > 0, JSON.stringify(again.cwv));
assert.strictEqual(running.closed, false, 'a browser the caller started stays open');
assert.strictEqual(running.closed, false, 'a browser the caller started stays open');
} finally { await running.close(); }
} finally { await running.close(); }
new file mode 100644
new file mode 100644
@@ -0,0 +1,15 @@
@@ -0,0 +1,15 @@
+'use strict';
+// seo.jsonLd.organization(): who OpenVibe is, in every site's structured data (4.22.0), so search engines and AI cannot
+// take it for something else: the one @id, its other names, what it is, what it is not, and its sites.
+const assert = require('assert');
+const seo = require('../seo');
+
+const org = seo.jsonLd.organization();
+assert.strictEqual(org['@id'], 'https://openvibe.network/#org');
+assert.strictEqual(org.name, 'OpenVibe');
+assert.deepStrictEqual(org.alternateName, ['OpenVibe Network', 'OpenVibers']);
+assert.ok(/open-source, community-run network of websites/.test(org.description));
+assert.ok(/not the Openvibe mobile social-media aggregator app/.test(org.disambiguatingDescription));
+['https://github.com/OpenVibers', 'https://openvibe.live', 'https://openvibe.tools', 'https://openvibe.chat', 'https://openre.stream'].forEach((u) => assert.ok(org.sameAs.includes(u), u));
+assert.strictEqual(seo.jsonLd.website({ name: 'X', url: 'https://x.example/', description: 'd' }).publisher['@id'], org['@id'], 'a WebSite names it by @id');
+console.log('seo org: who OpenVibe is ok');