openvibers/openvibe.blog Commits

The shared update system and one account UI: the home shows what shipped, /updates is the shared log, the footer (now initialised in the browser too) links it; the shared navbar handles sign in/out with logoutUrl ending this site's session, and the page's own 'Write · Sign out' bar is only for no-JS visitors. openvibe-shared 1.10.0, openvibe-publishing 0.3.1

OP OpenVibers committed 24 Sept 2026 · 528f9170bd · 5 files, +64 −17


Changes

ddiff --git a/package-lock.json b/package-lock.json
iindex a42c149..43db477 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -16,9 +16,9 @@
"express-rate-limit": "^7.5.0",
"helmet": "^8.1.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.0",
-- "openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v0.3.0",
++ "openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v0.3.1",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.7.0",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1"
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.10.0"
},
"devDependencies": {
"jsonwebtoken": "^9.0.2"
@@@ -1031,12 +1031,12 @@
}
},
"node_modules/openvibe-publishing": {
-- "version": "0.3.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v0.3.0",
-- "integrity": "sha512-zekpQwHyxkhVW5CZf3ccDPkOniV1hjyw9VlVajA+4bLRCsLUo+jcxamtFIVM2IJ0i8pDlF/MpUfj4wSSv9H/iw==",
++ "version": "0.3.1",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v0.3.1",
++ "integrity": "sha512-fK7BqaLKLmTwpWTPL+1cQt13tbG6Tp407hIooVWJ8Hi5QvZIzJxQPWb3lnzdCDjAOUCCgxmNNQ0o+fQbohbUJg==",
"license": "MIT",
"dependencies": {
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1"
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.10.0"
},
"engines": {
"node": ">=20"
@@@ -1060,9 +1060,9 @@
}
},
"node_modules/openvibe-shared": {
-- "version": "1.5.1",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1",
-- "integrity": "sha512-1XWRFwN8br3iiy2Z8fmnXQMgR76VlfMpHjP1xr5uKLCJmtvtNSWO1BgA1kQmVtyBY7g+1n6YFIjahaD4qWbD/Q==",
++ "version": "1.10.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.10.0",
++ "integrity": "sha512-d2GkNuJkx6as4NKiryIBAGuff+7kY+eDMmGxK3AR91yLcgW8UYugML0foas/vdhqRNowE2I8JRVEnD01ZLhnrQ==",
"license": "MIT",
"dependencies": {
"jsonwebtoken": "^9.0.0"
ddiff --git a/package.json b/package.json
iindex e7d9464..6bc9596 100644
---- a/package.json
++++ b/package.json
@@@ -22,9 +22,9 @@
"express-rate-limit": "^7.5.0",
"helmet": "^8.1.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.0",
-- "openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v0.3.0",
++ "openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v0.3.1",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.7.0",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1"
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.10.0"
},
"devDependencies": {
"jsonwebtoken": "^9.0.2"
ddiff --git a/server/http/public.js b/server/http/public.js
iindex 14d7ff0..46fad11 100644
---- a/server/http/public.js
++++ b/server/http/public.js
@@@ -17,6 +17,7 @@
* members/VIP and private posts, refusals, 404/410 — is `private, no-store` + X-Robots-Tag.
*/
const express = require('express');
++const chromeSsr = require('openvibe-shared/chrome-ssr');
const seo = require('openvibe-publishing/seo');
const ssr = require('openvibe-publishing/ssr');
const authorship = require('openvibe-publishing/authorship');
@@@ -77,7 +78,7 @@ function createPublicRoutes(ctx) {
// ── Blog front pages ────────────────────────────────────
-- async function blogFront(req, res, blog) {
++ async function blogFront(req, res, blog, { home = false } = {}) {
const page = pageNumber(req);
const restricted = isMember(blog, req.viewer);
const path = publication.blogPath(blog);
@@@ -109,11 +110,21 @@ function createPublicRoutes(ctx) {
blog, blogUrl: `/@${blog.handle}`, items, pager, feeds,
series: blogs.series(blog).filter((sr) => shows({ seriesId: sr.id })), categories: prune(reading.categoriesTree(blog)),
canWrite: access.canWrite(store, req.viewer, blog, 'create'),
-- }),
++ }) + (home && pager.page === 1 ? chromeSsr.shipped({ service: 'blog', title: 'Recently shipped on OpenVibe.Blog' }) : ''),
}, { cacheable: !restricted });
}
-- router.get('/', wrap(async (req, res) => blogFront(req, res, blogs.official())));
++ router.get('/', wrap(async (req, res) => blogFront(req, res, blogs.official(), { home: true })));
++
++ // What shipped on OpenVibe.Blog: the shared update log (openvibe-shared shipped.js, fed by the
++ // network changelog this service keeps), like every OpenVibe site's /updates.
++ router.get('/updates', wrap(async (req, res) => send(req, res, 200, {
++ title: 'What shipped on OpenVibe.Blog',
++ description: 'Every change deployed to OpenVibe.Blog, newest first, with the Patch notes that gather them.',
++ decision: pageDecision('/updates'),
++ canonical: seo.canonicalUrl(config.baseUrl, '/updates'),
++ body: chromeSsr.updatesBody({ service: 'blog', siteName: 'OpenVibe.Blog' }) + chromeSsr.shippedScript(),
++ }, { cacheable: true })));
router.get('/@:handle', wrap(async (req, res) => {
const blog = activeBlog(req);
ddiff --git a/server/render/layout.js b/server/render/layout.js
iindex bff88e5..c4543d2 100644
---- a/server/render/layout.js
++++ b/server/render/layout.js
@@@ -72,7 +72,9 @@ function renderPage(o) {
silentLogin: `${o.config.baseUrl}/auth/login?silent=1&next={url}`,
sessionUrl: '/auth/me',
loginUrl: `/auth/login?next=${loginNext}`,
++ logoutUrl: '/auth/logout?next={path}', // Sign out in the shared navbar ends this site's session too
};
++ const footer = { service: 'blog', variant: 'full', mount: '#ov-footer', brandName: SITE_NAME, updates: '/updates' };
const style = o.theme ? ` style="${esc(themeStyle(o.theme.vars))}" data-blog-theme="${esc(o.theme.slug)}"` : '';
const account = signedIn
? `<a href="/write">Write</a> · <a href="/auth/logout?next=${loginNext}">Sign out</a>`
@@@ -90,20 +92,22 @@ ${appIcon.headTags({ site: 'blog' })}
<link rel="stylesheet" href="${asset('css/blog.css')}">
<script src="${NETWORK_URL}/shared/theme-loader.js" defer></script>
<script src="${NETWORK_URL}/shared/navbar.js" defer></script>
++<script src="${NETWORK_URL}/shared/footer.js" defer></script>
</head>
<body class="${esc(o.bodyClass || '')}">
<a class="skip" href="#main">Skip to content</a>
<div id="navbar-mount"></div>
${chrome.noscriptNav({ name: SITE_NAME, home: '/', links: [{ label: 'Blog', href: '/' }, { label: 'Write', href: '/write' }] })}
--<div class="account-bar" role="navigation" aria-label="Account">${account}</div>
++<noscript><div class="account-bar" role="navigation" aria-label="Account">${account}</div></noscript>
<main id="main" class="page blog-surface"${style}>
${o.body || ''}
</main>
--${chrome.footer({ service: 'blog', variant: 'full' })}
++${chrome.footer(footer)}
<script>
--window.__OV_PAGE = ${JSON.stringify({ navbar: nav }).replace(/</g, '\\u003c')};
++window.__OV_PAGE = ${JSON.stringify({ navbar: nav, footer }).replace(/</g, '\\u003c')};
document.addEventListener('DOMContentLoaded', function () {
try { if (window.OpenVibeNavbar) OpenVibeNavbar.init(window.__OV_PAGE.navbar); } catch (e) { /* the chrome is optional */ }
++ try { if (window.OpenVibeFooter) OpenVibeFooter.init(window.__OV_PAGE.footer); } catch (e) { /* */ }
});
</script>
</body>
ddiff --git a/test/shipped-pages.test.js b/test/shipped-pages.test.js
nnew file mode 100644
iindex 0000000..db19ffc
---- /dev/null
++++ b/test/shipped-pages.test.js
@@@ -0,0 +1,32 @@
++'use strict';
++/**
++ * The shared update system on openvibe.blog: the home shows what shipped, /updates is the log, the
++ * footer carries the "shipped" line and an Updates link, and the shared navbar (not a second account
++ * bar) handles sign in and out, with Sign out ending this site's session.
++ */
++const assert = require('assert');
++const { boot, check, done } = require('./helpers/boot');
++
++(async () => {
++ const t = await boot();
++ await check('the home shows the shipped block; /updates is the shared log', async () => {
++ const home = await t.get('/');
++ assert.strictEqual(home.status, 200);
++ assert.ok(home.text.includes('data-ov-shipped="latest" data-service="blog" href="/updates"'));
++ assert.ok(home.text.includes('data-ov-shipped="list" data-service="blog"'));
++ const up = await t.get('/updates');
++ assert.strictEqual(up.status, 200);
++ assert.ok(up.text.includes('What shipped on OpenVibe.Blog') && up.text.includes('data-ov-shipped="log" data-service="blog"'));
++ assert.ok(up.text.includes('https://openvibe.network/shared/shipped.js'));
++ assert.ok(up.text.includes('https://openvibe.network/updates?site=blog'), 'no-JavaScript fallback');
++ });
++ await check('one account UI: the shared navbar signs out through this site; the old bar is only for no-JS', async () => {
++ const home = await t.get('/');
++ assert.ok(home.text.includes('"logoutUrl":"/auth/logout?next={path}"'));
++ assert.ok(/<noscript><div class="account-bar"/.test(home.text), 'the site account bar only without JavaScript');
++ assert.ok(home.text.includes('https://openvibe.network/shared/footer.js') && home.text.includes('"updates":"/updates"'));
++ assert.ok(home.text.includes('href="/updates"'), 'the footer links the update log');
++ });
++ await t.close();
++ done();
++})();