openvibers/openvibe.shared Commits

1.10.0: chrome-ssr shipped() (a home page's pill and recent changes), updatesBody() (a site's /updates page) and shippedScript(): the server-side half of the shared update system, so every site renders the same markup

OP OpenVibers committed 24 Sept 2026 · 16c66ef95a · 6 files, +73 −4


Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex fdee9b1..5f30eaf 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -4,6 +4,16 @@ 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).
++## 1.10.0 — 2026-09-24
++
++`openvibe-shared/chrome-ssr` gains the server-side pieces of the shared update system, so every site renders the same markup instead of its own copy:
++
++- `shipped({ service, updates, title, limit })`: the home page block (the pill and the recent changes), with a plain link without JavaScript.
++- `updatesBody({ service, siteName })`: the body of a site's `/updates` page. Without JavaScript it falls back to openvibe.network's server-rendered log.
++- `shippedScript()`: the script tag.
++
++`shipped.js` styles both. Additive.
++
## 1.9.0 — 2026-09-24
- **One "shipped" and update-log system for every site** (`shipped.js`). There are three views: `latest` (the "🚀 shipped 5m ago: …" pill), `list` (the recent changes plus the latest Patch notes link and an "All updates" link) and `log` (a full `/updates` page: changes grouped by day, "Load more" by cursor, this site or the whole network with a filter per site, and the Patch notes posts). Views mount from markup, `data-ov-shipped="latest|list|log"` with `data-service`, `data-limit`, `data-more` and `data-href`, when the script loads, or from code with `latest()`, `list()` (`mount()`), `log()` and `scan()`. The feed is now `https://openvibe.network/api/v1/changelog`, a cached proxy of OpenVibe.Blog's changelog that every site's CSP already allows. One request is shared per page; the "ago" text stays current; `serviceFor(host)` maps hostnames to registry ids.
ddiff --git a/chrome-ssr.js b/chrome-ssr.js
iindex d06798b..831ef62 100644
---- a/chrome-ssr.js
++++ b/chrome-ssr.js
@@@ -3,6 +3,12 @@
// footer(cfg) the full shared footer (HTML + CSS); the browser's footer.js renders into the same element
// noscriptNav(site) a plain navigation bar inside <noscript>: the JavaScript navbar cannot exist without
// scripts, so visitors who block them (and text browsers) still get the brand and the network
++// shipped(opts) a home page's "shipped" block: the "🚀 shipped X ago" pill and the recent changes,
++// filled by shipped.js; without JavaScript, a link to the update log
++// updatesBody(opts) the body of a site's /updates page (the full log; openvibe.network's server-rendered
++// log is the no-JavaScript fallback)
++// shippedScript() the <script> tag for shipped.js (the footer loads it lazily too; a page that shows the
++// log should load it itself)
const footerModule = require('./footer');
const SITES = [['Live', 'https://openvibe.live/'], ['Tools', 'https://openvibe.tools/'], ['Community', 'https://openvibe.community/'], ['Games', 'https://openvibe.games/'], ['Media', 'https://openvibe.media/'], ['Network', 'https://openvibe.network/']];
@@@ -14,4 +20,33 @@ function noscriptNav({ name, home = '/', links = [] } = {}) {
return `<noscript><nav aria-label="Site" style="display:flex;flex-wrap:wrap;align-items:center;gap:6px 16px;padding:12px 20px;border-bottom:1px solid rgba(255,255,255,.1);font:500 14px/1.4 system-ui,sans-serif"><a href="${esc(home)}" style="font-weight:700;color:inherit;text-decoration:none">${esc(name || 'OpenVibe')}</a>${own}<span style="flex:1"></span>${net}</nav></noscript>`;
}
--module.exports = { footer: (cfg) => footerModule.ssr(cfg), noscriptNav };
++const NETWORK = 'https://openvibe.network';
++const SERVICE_RE = /^[a-z][a-z0-9-]{1,39}$/;
++const svc = (s) => (SERVICE_RE.test(String(s || '')) ? String(s) : '');
++
++function shipped({ service, updates = '/updates', title = null, limit = 5 } = {}) {
++ const id = svc(service);
++ const n = Math.min(Math.max(parseInt(limit, 10) || 5, 1), 20);
++ return `<section class="ov-shipped-home" aria-label="What shipped">`
++ + `<a data-ov-shipped="latest" data-service="${esc(id)}" href="${esc(updates)}" hidden></a>`
++ + `<div data-ov-shipped="list" data-service="${esc(id)}" data-limit="${n}" data-more="${esc(updates)}"${title ? ` data-title="${esc(title)}"` : ''} hidden></div>`
++ + `<noscript><p><a href="${esc(updates)}">What shipped recently</a></p></noscript>`
++ + `</section>`;
++}
++
++function updatesBody({ service, siteName = null, base = NETWORK } = {}) {
++ const id = svc(service);
++ const name = siteName || (id ? `OpenVibe.${id.charAt(0).toUpperCase()}${id.slice(1)}` : 'OpenVibe');
++ const network = `${String(base).replace(/\/+$/, '')}/updates${id ? `?site=${encodeURIComponent(id)}` : ''}`;
++ return `<section class="ov-updates">`
++ + `<h1>What shipped on ${esc(name)}</h1>`
++ + `<p class="ov-updates-lede">Every change deployed to ${esc(name)}, newest first. Each line is a commit from the OpenVibers repositories, linked to the change itself; batches become <a href="https://openvibe.blog/@openvibe">Patch notes on openvibe.blog</a>. Switch to <a href="${esc(String(base).replace(/\/+$/, ''))}/updates">the whole network</a>.</p>`
++ + `<div data-ov-shipped="log" data-service="${esc(id)}" data-limit="50"><noscript><p>The update log needs JavaScript here; <a href="${esc(network)}">the same log on openvibe.network</a> works without it.</p></noscript></div>`
++ + `</section>`;
++}
++
++function shippedScript({ base = NETWORK } = {}) {
++ return `<script src="${esc(String(base).replace(/\/+$/, ''))}/shared/shipped.js" defer></script>`;
++}
++
++module.exports = { footer: (cfg) => footerModule.ssr(cfg), noscriptNav, shipped, updatesBody, shippedScript };
ddiff --git a/package-lock.json b/package-lock.json
iindex 2070ac8..7aeea9d 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -1,12 +1,12 @@
{
"name": "openvibe-shared",
-- "version": "1.9.0",
++ "version": "1.10.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openvibe-shared",
-- "version": "1.9.0",
++ "version": "1.10.0",
"license": "MIT",
"dependencies": {
"jsonwebtoken": "^9.0.0"
ddiff --git a/package.json b/package.json
iindex f6e6c38..31a82e4 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-shared",
-- "version": "1.9.0",
++ "version": "1.10.0",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"exports": {
ddiff --git a/shipped.js b/shipped.js
iindex 6069fc4..b187cc9 100644
---- a/shipped.js
++++ b/shipped.js
@@@ -45,6 +45,8 @@
`.ov-shipped-latest:hover{border-color:var(--accent,#3b82f6);color:${TEXT}}.ov-shipped-latest svg{flex:none;color:var(--accent,#3b82f6)}`,
`.ov-shipped-latest b{color:${TEXT};font-weight:600;overflow:hidden;text-overflow:ellipsis;min-width:0}`,
'[data-ov-shipped][hidden],.ov-shipped[hidden],.ov-shipped-latest[hidden]{display:none!important}',
++ '.ov-shipped-home{display:grid;gap:12px;justify-items:start;margin:16px 0;min-width:0}.ov-shipped-home>.ov-shipped{justify-self:stretch}',
++ `.ov-updates{min-width:0}.ov-updates h1{margin:.2em 0 .3em}.ov-updates-lede{color:${MUTED};line-height:1.55;margin:0 0 18px}`,
'.ov-shipped{font-size:.92rem;min-width:0}.ov-shipped h3{font-size:1rem;margin:0 0 .5rem;display:flex;gap:.4rem;align-items:center}',
'.ov-shipped ul{list-style:none;margin:0;padding:0;display:grid;gap:.35rem}.ov-shipped li{display:flex;gap:.5rem;align-items:baseline;min-width:0}',
`.ov-shipped-site{flex:none;font-size:.72rem;font-weight:700;padding:.05rem .45rem;border-radius:999px;background:color-mix(in srgb,var(--accent,#3b82f6) 16%,transparent);color:${ACCENT};text-decoration:none}`,
ddiff --git a/test/chrome-ssr.test.js b/test/chrome-ssr.test.js
nnew file mode 100644
iindex 0000000..25eb553
---- /dev/null
++++ b/test/chrome-ssr.test.js
@@@ -0,0 +1,22 @@
++'use strict';
++// openvibe-shared/chrome-ssr: the server-side pieces of the shared chrome, including the "shipped"
++// block for a home page and the body of a site's /updates page (shipped.js fills both).
++const assert = require('assert');
++const chrome = require('../chrome-ssr');
++
++const home = chrome.shipped({ service: 'wiki', title: 'Recently shipped on OpenVibe.Wiki <x>' });
++assert.ok(home.includes('data-ov-shipped="latest" data-service="wiki" href="/updates"'));
++assert.ok(home.includes('data-ov-shipped="list" data-service="wiki" data-limit="5" data-more="/updates"'));
++assert.ok(home.includes('data-title="Recently shipped on OpenVibe.Wiki &lt;x&gt;"'), 'title escaped');
++assert.ok(home.includes('<noscript><p><a href="/updates">'), 'a plain link without JavaScript');
++assert.ok(chrome.shipped({ service: '"><script>' }).includes('data-service=""'), 'a bad id never reaches the markup');
++
++const body = chrome.updatesBody({ service: 'wiki' });
++assert.ok(body.includes('<h1>What shipped on OpenVibe.Wiki</h1>'));
++assert.ok(body.includes('data-ov-shipped="log" data-service="wiki"'));
++assert.ok(body.includes('https://openvibe.network/updates?site=wiki'), 'the no-JavaScript fallback is the network log');
++assert.strictEqual(chrome.shippedScript(), '<script src="https://openvibe.network/shared/shipped.js" defer></script>');
++
++const foot = chrome.footer({ service: 'wiki', variant: 'full', updates: '/updates' });
++assert.ok(foot.includes('data-ov-shipped="latest"') && foot.includes('href="/updates"'));
++console.log('chrome-ssr: all checks passed');