openvibers/openvibe.shared Commits

Shared 4.1.3: shell.page() holds the navbar's place; the updates log no longer jumps while it loads (#70)

OP OpenVibers committed 12 Oct 2026 · a81169a53f · 4 files, +14 −2

Release 4.1.3: two layout-shift fixes that every site gets through the pin round.

**`shell.page()` holds the navbar's place.** The navbar script is deferred and mounts into the page's `#navbar-mount`. When the first paint came before it (likely on a phone), the 52 px bar landed afterwards and pushed the whole page down. An earlier phone sweep measured CLS 0.058 on about 30 homes, including Network, Tools, Community, News, Reviews, Trade, Coupons, Deals, Blog and Wiki. The head now carries `<style>#navbar-mount{min-height:52px}</style>`, and a `<noscript>` style removes the empty mount without JavaScript. The no-JS nav markup is unchanged (`<noscript><nav aria-label="Site"`), because several sites' tests match it.

**The updates log no longer jumps while it loads** (#69, already merged under "Unreleased"). The first load shows one standalone "Loading…" line, the site's own chips are drawn before the data, and the chips, patch notes and days land together. This fixes CLS 0.17 on openvibe.live/updates.

Version 4.1.3 in package.json and the CHANGELOG; I'll tag after merge.

**Tests:**
- `test/shell.test.js` checks the reservation sits in the head.
- `npm test`: 58/59 files pass. `floating-promises` is skipped locally because acorn isn't installed; CI installs it.

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex 2291b14..a976dd2 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -4,8 +4,12 @@ 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).
--## Unreleased
++## 4.1.3 — 2026-10-12
++- **`shell.page()` holds the navbar's place.** The navbar script is deferred and mounts into the page's `#navbar-mount`;
++ on a slow first paint the 52 px bar arrived after it and pushed the page down (CLS 0.058 on many homes in a phone
++ sweep). The head now reserves `#navbar-mount{min-height:52px}`, and a `<noscript>` style removes the empty mount
++ without JavaScript.
- **The updates log no longer jumps while it loads.** `shipped.js` log showed "Loading…" inside the days, then the site
chips and the patch notes arrived above it and pushed it down (CLS 0.17 on openvibe.live/updates on a phone). The
first load now shows one standalone "Loading…" line, a site's own chips are drawn before the data, and the chips,
ddiff --git a/package.json b/package.json
iindex ebfadb2..18d529e 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-shared",
-- "version": "4.1.2",
++ "version": "4.1.3",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"bin": {
ddiff --git a/shell.js b/shell.js
iindex 29aa969..98af5a8 100644
---- a/shell.js
++++ b/shell.js
@@@ -113,6 +113,10 @@ function manifestLinks(o) {
return `<link rel="manifest" href="${esc(manifest)}">\n<link rel="apple-touch-icon" href="${esc(touch)}">`;
}
++// The shared navbar's script is deferred and mounts into the page's #navbar-mount: the mount holds the bar's 52px from
++// the first paint so the page does not drop when it lands (layout shift), and without JavaScript it goes away.
++const NAVBAR_RESERVE = '<style>#navbar-mount{min-height:52px}</style><noscript><style>#navbar-mount{display:none}</style></noscript>';
++
function page(o = {}) {
const name = o.name || o.service || 'OpenVibe';
const title = o.title || name;
@@@ -136,6 +140,7 @@ function page(o = {}) {
summary ? summary.head : '',
o.head || '',
manifestLinks(o),
++ NAVBAR_RESERVE,
scripts(o),
'</head>',
`<body${attrs(bodyAttributes)}>`,
ddiff --git a/test/shell.test.js b/test/shell.test.js
iindex ac53f30..c4c9186 100644
---- a/test/shell.test.js
++++ b/test/shell.test.js
@@@ -26,6 +26,9 @@ assert.match(html, /<noscript><nav aria-label="Site"/, 'no-JS nav present');
assert.match(html, /<footer id="ov-footer"/, 'shared footer present');
assert.ok(html.includes('<main id="hero">Hello</main>'), 'body kept verbatim');
assert.ok(html.includes('<script src="/shared/theme-loader.js?v='), 'theme-loader via serve.url');
++// The deferred navbar lands in a #navbar-mount that already holds its height; without JavaScript the mount goes away.
++assert.ok(html.includes('<style>#navbar-mount{min-height:52px}</style><noscript><style>#navbar-mount{display:none}</style></noscript>'), 'navbar mount reserved');
++assert.ok(html.indexOf('#navbar-mount{min-height:52px}') < html.indexOf('</head>'), 'in the head, before the first paint');
// ── Script order: theme-loader first (eager); the runtime after it (deferred) ─────────────────────
const scripts = shell.scripts({ name: 'example' });