openvibers/openvibe.shared Commits

4.157.0: every shell page has a tab icon (#349)

OP OpenVibers committed 4 days ago · 7ad625094a · 7 files, +42 −5

shell.page() adds the service's OpenVibe mark (app-icon favicon, the tint of its app icons) as an inline SVG
<link rel="icon"> when the page's head has none. Without one the browser asked /favicon.ico, which most sites
answer 404, and the tab showed a blank page icon (11-09: Computer, Email and Studio). A page with its own icon
keeps it; favicon: false opts out.

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex 921936d..c20346d 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -1,5 +1,13 @@
# Changelog
++## 4.157.0 — 2026-11-09
++
++- **Every shell page has a tab icon.** `shell.page()` adds the service's OpenVibe mark (`openvibe-shared/app-icon`
++ `favicon`, the same tint as its app icons) as an inline SVG `<link rel="icon">` when the page's `head` has none.
++ Without one the browser asked `/favicon.ico`, which most sites answer 404, and the tab showed a blank page icon
++ (11-09: openvibe.computer, openvibe.email and openvibe.studio). A page with its own icon keeps it; `favicon: false`
++ opts out.
++
## 4.156.0 — 2026-11-08
- **Locker unlocks with a passkey on this device.** Setting up a locker can use a passkey instead of a passphrase:
ddiff --git a/README.md b/README.md
iindex 46462ac..a1756b7 100644
---- a/README.md
++++ b/README.md
@@@ -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.156.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
++**Status:** alpha, v4.157.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
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
the tag it pins.
ddiff --git a/STATUS.json b/STATUS.json
iindex 1b2e8b5..73288da 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -4,7 +4,7 @@
"code": true,
"wave": "W2",
"package": "openvibe-shared",
-- "version": "4.156.0",
++ "version": "4.157.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)",
ddiff --git a/package-lock.json b/package-lock.json
iindex 0322726..80e347b 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -1,12 +1,12 @@
{
"name": "openvibe-shared",
-- "version": "4.156.0",
++ "version": "4.157.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openvibe-shared",
-- "version": "4.156.0",
++ "version": "4.157.0",
"license": "MIT",
"dependencies": {
"jsonwebtoken": "^9.0.0",
ddiff --git a/package.json b/package.json
iindex 5ce8c7e..d7e1661 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-shared",
-- "version": "4.156.0",
++ "version": "4.157.0",
"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 a96b64d..8f201dc 100644
---- a/shell.js
++++ b/shell.js
@@@ -84,6 +84,8 @@ function scripts(o = {}) {
* head extra <head> markup appended verbatim (already trusted/escaped)
* manifest the installable web app links (manifest + apple-touch-icon) are added when the
* page's service has an icon set; `false` opts out
++ * favicon the tab icon: the service's OpenVibe mark (openvibe-shared/app-icon favicon) as an inline
++ * SVG, added unless `head` already has a rel=icon link; `false` opts out (4.157.0)
* body the page's main content (trusted markup — the caller escaped it)
* bodyAttributes attributes for <body>, escaped; bodyClass is shorthand for `class`
* navLinks, home the `frame.noscriptNav` no-JavaScript navigation
@@@ -124,6 +126,19 @@ function manifestLinks(o) {
return `<link rel="manifest" href="${esc(manifest)}">\n<link rel="apple-touch-icon" href="${esc(touch)}">`;
}
++// The tab icon (4.157.0): without one the browser asks /favicon.ico, which most sites answer 404, and the tab shows a
++// blank page icon (11-09: Computer, Email and Studio, all on this shell). The service's mark, the same tint as its app
++// icons; a page whose `head` already has an icon keeps its own.
++function faviconLink(o) {
++ if (o.favicon === false) return '';
++ if (typeof o.head === 'string' && /rel=["']?(?:shortcut )?icon\b/i.test(o.head)) return '';
++ const service = siteId(o) || 'network';
++ const known = appIcons.site(service);
++ const variant = String((known && known.icon) || service).replace(/[^a-z0-9-]/g, '');
++ const uri = 'data:image/svg+xml,' + encodeURIComponent(require('./app-icon').favicon({ site: variant })).replace(/'/g, '%27');
++ return `<link rel="icon" type="image/svg+xml" data-ov-icon="${esc(variant)}" href="${uri}">`;
++}
++
// 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. The body
// has no browser margin, so the Frame's navbar and footer meet the window's edges on a page with no stylesheet of its own
@@@ -156,6 +171,7 @@ function page(o = {}) {
o.agentLayer && o.canonical ? require('./agent-layer').linkHeaderTags(o.canonical, o.llms).head : '',
summary ? summary.head : '',
o.head || '',
++ faviconLink(o),
manifestLinks(o),
NAVBAR_RESERVE,
scripts(o),
ddiff --git a/test/shell.test.js b/test/shell.test.js
iindex d345b11..69f6b32 100644
---- a/test/shell.test.js
++++ b/test/shell.test.js
@@@ -93,4 +93,17 @@ assert.ok(withSummary.indexOf('</head>') < withSummary.indexOf('<noscript><secti
assert.match(shellLib.page({ name: 'X', navbar: { service: 'games' }, body: '' }), /data-ov-service="games"/);
assert.doesNotMatch(shellLib.page({ name: 'OpenVibe.Design', body: '' }), /data-ov-service/, 'a display name is not an id');
}
++// ── The tab icon (4.157.0): the service's mark unless the page brings its own; `favicon: false` opts out ──
++{
++ const withIcon = shell.page({ name: 'OpenVibe.Email', service: 'mail', body: '' });
++ const icons = withIcon.match(/<link rel="icon"[^>]*>/g) || [];
++ assert.equal(icons.length, 1, 'one tab icon');
++ assert.match(icons[0], /type="image\/svg\+xml" data-ov-icon="[a-z0-9-]+" href="data:image\/svg\+xml,%3Csvg/, 'an inline SVG of the mark');
++ assert.ok(!/['"<>]/.test(icons[0].slice(icons[0].indexOf('href="') + 6, -2)), 'the data URI is fully encoded');
++ assert.ok(withIcon.indexOf(icons[0]) < withIcon.indexOf('</head>'), 'in the head');
++ const own = shell.page({ name: 'X', service: 'mail', head: '<link rel="icon" href="/icon.svg">', body: '' });
++ assert.equal((own.match(/rel="icon"/g) || []).length, 1, 'a page with its own icon keeps only that one');
++ assert.doesNotMatch(shell.page({ name: 'X', service: 'mail', favicon: false, body: '' }), /rel="icon"/, 'favicon: false opts out');
++ assert.match(shell.page({ name: 'Plain', body: '' }), /data-ov-icon="network"/, 'no service: the network mark');
++}
console.log('shell: all checks passed');