openvibers/openvibe.shared Commits

Shared 2.2.0 (plan T11): boost.js starts itself from its script tag (data-main, data-prefix, data-exclude; OVBoost.controller) and carries the new page's body class; navbar.js follows a boost swap (re-renders for active links and a sign-in that returns to the new page, records the new page in the person's history). boost-chrome.test.js runs the auto-start path and checks the body class. 46/46.

OP OpenVibers committed 29 Sept 2026 · c34086d267 · 5 files, +43 −10


Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex eed2b21..6a0daec 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -4,6 +4,15 @@ All notable changes to `openvibe-shared`. Versions follow [semver](https://semve
breaking change to any exported module, browser global or served file name is a new major.
A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
++## 2.2.0 — 2026-09-29
++
++Boost on real sites (plan T11):
++- `boost.js` starts itself from its script tag: `<script src="/shared/boost.js" data-main="#main" defer>` (optional
++ `data-prefix`, `data-exclude="/a/,/b/"`); the controller is `OVBoost.controller`. `OVBoost.start()` still works.
++- A swap also carries the new page's `<body>` class (page-type classes).
++- `navbar.js` follows boost: on `ov:boost:load` the bar re-renders (active links, a sign-in that returns to the new
++ page) and records the new page in the signed-in person's history.
++
## 2.1.0 — 2026-09-29
Three new modules for plan T11, plus `serve.js` now taking its cache headers from the shared policy (its own headers are
ddiff --git a/boost.js b/boost.js
iindex e07941e..f989759 100644
---- a/boost.js
++++ b/boost.js
@@@ -4,8 +4,7 @@
*
* <meta name="ov-boost" content="<site>@<release>"> on every page that may be swapped (the server's release id)
* <main id="main">…</main> the part that changes
-- * <script src="/shared/boost.js" defer></script>
-- * <script>addEventListener('DOMContentLoaded', () => OVBoost.start({ main: '#main' }))</script>
++ * <script src="/shared/boost.js" data-main="#main" defer></script> starts itself (or call OVBoost.start below)
*
* A click on a same-site link (no modifier key, no target, no download, not data-no-boost, not /auth/ or /api/) fetches
* the next page's HTML (already prefetched on hover, focus or touch), loads its new stylesheets first, swaps <main> with
@@@ -22,6 +21,7 @@
'use strict';
if (typeof document === 'undefined' || root.OVBoost) return;
const selfSrc = document.currentScript && document.currentScript.src;
++ const selfData = (document.currentScript && document.currentScript.dataset) || {};
const HEAD_TAGS = 'meta[name="description"],link[rel="canonical"],meta[property^="og:"],meta[name^="twitter:"],script[type="application/ld+json"],link[rel="alternate"][hreflang],meta[name="robots"]';
const FILE_EXT = /\.(?:pdf|zip|gz|tgz|png|jpe?g|gif|webp|avif|svg|mp4|webm|mp3|wav|ogg|json|xml|txt|csv|ics)$/i;
@@@ -97,6 +97,7 @@
doc.head.querySelectorAll(HEAD_TAGS).forEach((el) => document.head.appendChild(document.importNode(el, true)));
const lang = doc.documentElement.getAttribute('lang');
if (lang) document.documentElement.setAttribute('lang', lang);
++ if (doc.body) document.body.className = doc.body.className; // page-type classes
}
function runInlineScripts(scope) {
@@@ -209,5 +210,14 @@
};
}
-- root.OVBoost = { start };
++ root.OVBoost = { start, controller: null };
++ // <script src=".../boost.js" data-main="#main" [data-prefix] [data-exclude="/a/,/b/"] defer>: start once the page is parsed.
++ if (selfData.main) {
++ const boot = () => {
++ if (root.OVBoost.controller) return;
++ root.OVBoost.controller = start({ main: selfData.main, prefix: selfData.prefix || 'ov', exclude: (selfData.exclude || '').split(',').map((x) => x.trim()).filter(Boolean) });
++ };
++ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot, { once: true });
++ else boot();
++ }
})(typeof window !== 'undefined' ? window : globalThis);
ddiff --git a/navbar.js b/navbar.js
iindex 73ccb97..0cc30bf 100644
---- a/navbar.js
++++ b/navbar.js
@@@ -1496,10 +1496,24 @@
return nav;
}
++ // openvibe-shared/boost swapped the page in place: this is a new page for the history and the
++ // active-link state, and a login from here returns here.
++ let _boostHooked = false;
++ function hookBoost() {
++ if (_boostHooked || typeof document === 'undefined') return;
++ _boostHooked = true;
++ document.addEventListener('ov:boost:load', () => {
++ if (_config.history) _config.history = Object.assign({}, _config.history, { title: document.title, url: location.href });
++ try { render(); } catch { /* keep the old bar */ }
++ if (_config.user) recordHistory();
++ });
++ }
++
const OpenVibeNavbar = {
init(opts = {}) {
Object.assign(_config, opts);
injectStyles();
++ hookBoost();
const el = render();
upgradeIconsWhenFontsReady();
if (typeof document !== 'undefined') setTimeout(loadReleaseWatch, 4000);
ddiff --git a/package.json b/package.json
iindex 7dccbd1..121e179 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-shared",
-- "version": "2.1.0",
++ "version": "2.2.0",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"exports": {
ddiff --git a/test/boost-chrome.test.js b/test/boost-chrome.test.js
iindex 2aba52d..3cf52b7 100644
---- a/test/boost-chrome.test.js
++++ b/test/boost-chrome.test.js
@@@ -19,9 +19,8 @@ const hits = {};
const page = ({ title, marker = 'site@r1', css = [], scripts = ['/js/site.js'], body, canonical }) => `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>${title}</title>
<meta name="ov-boost" content="${marker}"><meta name="description" content="about ${title}"><link rel="canonical" href="https://site.example${canonical}">
<link rel="icon" href="data:,">${css.map((c) => `<link rel="stylesheet" href="${c}">`).join('')}
--${scripts.map((s) => `<script src="${s}"></script>`).join('')}<script src="/boost.js" defer></script>
--<script>addEventListener('DOMContentLoaded', () => { window.boost = OVBoost.start({ main: '#main' }); });</script></head>
--<body><header>header</header><main id="main" data-page="${title}">${body}</main><footer>footer</footer></body></html>`;
++${scripts.map((s) => `<script src="${s}"></script>`).join('')}<script src="/boost.js" data-main="#main" defer></script></head>
++<body class="page-${title.replace(/\W+/g, '-').toLowerCase()}"><header>header</header><main id="main" data-page="${title}">${body}</main><footer>footer</footer></body></html>`;
const LINKS = '<a id="to-a" href="/a">A</a> <a id="to-b" href="/b">B</a> <a id="to-x" href="/x">X</a> <a id="to-y" href="/y">Y</a> <a id="to-api" href="/api/thing">api</a> <a id="to-new" href="/a" target="_blank">new</a>';
const PAGES = {
'/': page({ title: 'Home', canonical: '/', body: `<h1>Home</h1><div style="height:3000px">${LINKS}</div><p id="bottom">bottom</p>` }),
@@@ -52,7 +51,7 @@ const server = http.createServer((req, res) => {
const context = await browser.newContext();
const p = await h.openPage(browser, context, { width: 390 });
await p.goto(`${base}/`, { maxMs: 5000 });
-- await p.evaluate('new Promise((r) => (window.boost ? r() : addEventListener("DOMContentLoaded", () => setTimeout(r, 0))))');
++ await p.evaluate('new Promise((r) => (window.OVBoost && OVBoost.controller ? r() : addEventListener("DOMContentLoaded", () => setTimeout(r, 0))))');
const startY = await p.evaluate('window.alive = 1; window.scrollTo(0, 1200); scrollY');
assert.ok(startY > 300, `the home page scrolled (${startY})`);
@@@ -64,7 +63,7 @@ const server = http.createServer((req, res) => {
let v = await p.evaluate(`new Promise((done) => {
document.addEventListener('ov:boost:load', () => done({ alive: window.alive, page: document.getElementById('main').dataset.page, h1color: getComputedStyle(document.querySelector('main h1')).color,
title: document.title, canonical: document.querySelector('link[rel=canonical]').href, desc: document.querySelector('meta[name=description]').content,
-- ld: document.querySelectorAll('script[type="application/ld+json"]').length, inline: window.inlineRuns, loads: window.loads, path: location.pathname, y: scrollY,
++ ld: document.querySelectorAll('script[type="application/ld+json"]').length, bodyClass: document.body.className, inline: window.inlineRuns, loads: window.loads, path: location.pathname, y: scrollY,
focused: document.activeElement && document.activeElement.id }), { once: true });
document.getElementById('to-a').click();
})`);
@@@ -72,6 +71,7 @@ const server = http.createServer((req, res) => {
assert.deepStrictEqual([v.alive, v.page, v.path, v.title], [1, 'Page A', '/a', 'Page A'], 'swapped in place, no page load');
assert.strictEqual(v.h1color, 'rgb(1, 2, 3)', 'the new stylesheet applied before the content showed');
assert.deepStrictEqual([v.canonical, v.desc], ['https://site.example/a', 'about Page A']);
++ assert.strictEqual(v.bodyClass, 'page-page-a', 'the body class follows the page');
assert.strictEqual(v.inline, 1, 'the inline script in the new main ran once');
assert.strictEqual(v.loads, 1, 'the site script was not loaded again');
assert.strictEqual(v.y, 0, 'a new page starts at the top'); assert.strictEqual(v.focused, 'main', 'focus moves to the new content');
@@@ -93,7 +93,7 @@ const server = http.createServer((req, res) => {
await wait(1500);
v = await p.evaluate('({ alive: window.alive, path: location.pathname, page: document.getElementById("main").dataset.page })');
assert.deepStrictEqual([v.alive, v.path, v.page], [undefined, '/x', 'Page X'], 'another release: a normal page load');
-- await p.evaluate('new Promise((r) => (window.boost ? r() : addEventListener("DOMContentLoaded", () => setTimeout(r, 0))))');
++ await p.evaluate('new Promise((r) => (window.OVBoost && OVBoost.controller ? r() : addEventListener("DOMContentLoaded", () => setTimeout(r, 0))))');
await p.evaluate('window.alive = 2; document.getElementById("to-y").click(); 1');
await wait(1500);
v = await p.evaluate('({ alive: window.alive, path: location.pathname })');