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.
Changes
@@@ -4,6 +4,15 @@ All notable changes to `openvibe-shared`. Versions follow [semver](https://semvebreaking 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-29Three new modules for plan T11, plus `serve.js` now taking its cache headers from the shared policy (its own headers are@@@ -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);@@@ -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);@@@ -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": {@@@ -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 })');
@@ -4,6 +4,15 @@ All notable changes to `openvibe-shared`. Versions follow [semver](https://semve
@@ -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.
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).
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
## 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
Three new modules for plan T11, plus `serve.js` now taking its cache headers from the shared policy (its own headers are
@@ -4,8 +4,7 @@
@@ -4,8 +4,7 @@
*
*
* <meta name="ov-boost" content="<site>@<release>"> on every page that may be swapped (the server's release id)
* <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
* <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
* 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
* the next page's HTML (already prefetched on hover, focus or touch), loads its new stylesheets first, swaps <main> with
@@ -22,6 +21,7 @@
@@ -22,6 +21,7 @@
'use strict';
'use strict';
if (typeof document === 'undefined' || root.OVBoost) return;
if (typeof document === 'undefined' || root.OVBoost) return;
const selfSrc = document.currentScript && document.currentScript.src;
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 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;
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 @@
@@ -97,6 +97,7 @@
doc.head.querySelectorAll(HEAD_TAGS).forEach((el) => document.head.appendChild(document.importNode(el, true)));
doc.head.querySelectorAll(HEAD_TAGS).forEach((el) => document.head.appendChild(document.importNode(el, true)));
const lang = doc.documentElement.getAttribute('lang');
const lang = doc.documentElement.getAttribute('lang');
if (lang) document.documentElement.setAttribute('lang', lang);
if (lang) document.documentElement.setAttribute('lang', lang);
+ if (doc.body) document.body.className = doc.body.className; // page-type classes
}
}
function runInlineScripts(scope) {
function runInlineScripts(scope) {
@@ -209,5 +210,14 @@
@@ -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);
})(typeof window !== 'undefined' ? window : globalThis);
@@ -1496,10 +1496,24 @@
@@ -1496,10 +1496,24 @@
return nav;
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 = {
const OpenVibeNavbar = {
init(opts = {}) {
init(opts = {}) {
Object.assign(_config, opts);
Object.assign(_config, opts);
injectStyles();
injectStyles();
+ hookBoost();
const el = render();
const el = render();
upgradeIconsWhenFontsReady();
upgradeIconsWhenFontsReady();
if (typeof document !== 'undefined') setTimeout(loadReleaseWatch, 4000);
if (typeof document !== 'undefined') setTimeout(loadReleaseWatch, 4000);
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-shared",
"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",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"main": "index.js",
"exports": {
"exports": {
@@ -19,9 +19,8 @@ const hits = {};
@@ -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>
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}">
<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('')}
<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 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 = {
const PAGES = {
'/': page({ title: 'Home', canonical: '/', body: `<h1>Home</h1><div style="height:3000px">${LINKS}</div><p id="bottom">bottom</p>` }),
'/': 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) => {
@@ -52,7 +51,7 @@ const server = http.createServer((req, res) => {
const context = await browser.newContext();
const context = await browser.newContext();
const p = await h.openPage(browser, context, { width: 390 });
const p = await h.openPage(browser, context, { width: 390 });
await p.goto(`${base}/`, { maxMs: 5000 });
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');
const startY = await p.evaluate('window.alive = 1; window.scrollTo(0, 1200); scrollY');
assert.ok(startY > 300, `the home page scrolled (${startY})`);
assert.ok(startY > 300, `the home page scrolled (${startY})`);
@@ -64,7 +63,7 @@ const server = http.createServer((req, res) => {
@@ -64,7 +63,7 @@ const server = http.createServer((req, res) => {
let v = await p.evaluate(`new Promise((done) => {
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,
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,
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 });
focused: document.activeElement && document.activeElement.id }), { once: true });
document.getElementById('to-a').click();
document.getElementById('to-a').click();
})`);
})`);
@@ -72,6 +71,7 @@ const server = http.createServer((req, res) => {
@@ -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.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.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.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.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.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');
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) => {
@@ -93,7 +93,7 @@ const server = http.createServer((req, res) => {
await wait(1500);
await wait(1500);
v = await p.evaluate('({ alive: window.alive, path: location.pathname, page: document.getElementById("main").dataset.page })');
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');
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 p.evaluate('window.alive = 2; document.getElementById("to-y").click(); 1');
await wait(1500);
await wait(1500);
v = await p.evaluate('({ alive: window.alive, path: location.pathname })');
v = await p.evaluate('({ alive: window.alive, path: location.pathname })');