openvibers/openvibe.live Commits

Live: channel pages, /updates, /settings, /themes, /admin and /documentation answer the page again, not {}. (#48)

OP OpenVibers committed 8 Oct 2026 · cc6997fa55 · 3 files, +33 −19

Since the PostgreSQL switch (Live#46) seo.shellHtml is async, but the SPA fallback's sendShell in server/index.js still
called it synchronously and sent the Promise, which Express serialised as {}: every page without its own SEO renderer
(channels, /updates, /settings, /themes, /admin, /documentation) was a blank {} in the browser. sendShell now lives in
server/web/shell.js and awaits it; the page-status test runs that exact function and checks that every page answer is
the HTML shell, not only its status (it fails without the fix).


Claude-Session: https://claude.ai/code/session_01K8AYn3FF3HE7CPfM262qdh

Co-authored-by: Claude Opus 5.5 <[email protected]>

Changes

ddiff --git a/server/index.js b/server/index.js
iindex eab4afd5..b369d05b 100644
---- a/server/index.js
++++ b/server/index.js
@@@ -1102,15 +1102,7 @@ app.get('/banned', async (req, res) => {
// own head describes the home page), and no other page claims the home page as its canonical.
let _shellHtml = null;
try { _shellHtml = require('./seo').shellHtml; } catch { _shellHtml = null; }
--function sendShell(res, urlPath) {
-- let html = null;
-- try { html = _shellHtml ? _shellHtml(urlPath, res.statusCode) : null; } catch { html = null; }
-- if (!html) return sendDocument(res, 'index.html', urlPath);
-- if (!res.getHeader('Cache-Control')) assets.setNoCache(res);
-- try { res.setHeader('Content-Security-Policy-Report-Only', assets.cspReportOnly(html)); } catch { /* */ }
-- res.type('html').send(html);
-- return true;
--}
++const sendShell = require('./web/shell').createSendShell({ shellHtml: _shellHtml, sendDocument, assets });
app.get('*', require('./web/page-status').spaFallback((res, urlPath) => sendShell(res, urlPath)));
// ── Global Error Handler ─────────────────────────────────────
ddiff --git a/server/web/shell.js b/server/web/shell.js
nnew file mode 100644
iindex 00000000..0b213a9f
---- /dev/null
++++ b/server/web/shell.js
@@@ -0,0 +1,24 @@
++'use strict';
++
++/**
++ * sendShell for the SPA fallback (server/web/page-status.js spaFallback): the page shell from seo.shellHtml for a URL,
++ * with the status the fallback set (a 404 shell is noindex with no canonical), or the plain index.html when the SEO
++ * shell is unavailable. Kept out of server/index.js so the page-status test exercises this exact function.
++ *
++ * seo.shellHtml is async (server/seo/pages.js): its result must be awaited. Sending the unawaited Promise answered every
++ * page without its own SEO renderer (channels, /updates, /settings, /documentation, …) with `{}` after the PostgreSQL
++ * switch (2026-10-08).
++ */
++function createSendShell({ shellHtml, sendDocument, assets }) {
++ return async function sendShell(res, urlPath) {
++ let html = null;
++ try { html = shellHtml ? await shellHtml(urlPath, res.statusCode) : null; } catch { html = null; }
++ if (typeof html !== 'string' || !html) return sendDocument(res, 'index.html', urlPath);
++ if (!res.getHeader('Cache-Control')) assets.setNoCache(res);
++ try { res.setHeader('Content-Security-Policy-Report-Only', assets.cspReportOnly(html)); } catch { /* the header is optional */ }
++ res.type('html').send(html);
++ return true;
++ };
++}
++
++module.exports = { createSendShell };
ddiff --git a/test/page-status.test.js b/test/page-status.test.js
iindex 12d55c98..2261cac6 100644
---- a/test/page-status.test.js
++++ b/test/page-status.test.js
@@@ -68,8 +68,10 @@ async function expectAll(paths, want, opts) {
const wrong = [];
for (const p of paths) {
for (const html of [false, true]) {
-- const s = await statusOf(p, { ...opts, html });
-- if (s !== want) wrong.push(`${p}${html ? ' (html)' : ''} → ${s}`);
++ const r = await get(p, { ...opts, html });
++ if (r.status !== want) wrong.push(`${p}${html ? ' (html)' : ''} → ${r.status}`);
++ // Every page answer is the HTML shell (a 404 too): never JSON, never a serialized Promise (`{}`).
++ else if (!/^<!doctype html/i.test(r.body.trimStart()) || !/text\/html/.test(r.type)) wrong.push(`${p}${html ? ' (html)' : ''} → ${r.status} but not the HTML shell: ${JSON.stringify(r.body.slice(0, 40))}`);
}
}
assert.deepStrictEqual(wrong, [], `expected ${want}: ${wrong.join(', ')}`);
@@@ -121,14 +123,10 @@ async function main() {
// ── The app: the SEO middleware, then the real SPA fallback (as server/index.js mounts them) ──
const app = express();
seo.register(app);
-- // As server/index.js sendShell: the shell from seo.shellHtml (a 404 is noindex, no canonical).
-- app.get('*', pageStatus.spaFallback(async (res, urlPath) => {
-- const html = await seo.shellHtml(urlPath, res.statusCode);
-- if (!html) return false;
-- res.type('html').send(html);
-- return true;
-- }));
-- void assets;
++ // server/index.js's own sendShell (server/web/shell.js) over the real seo.shellHtml: a page without an SEO renderer
++ // (a channel, /updates, /documentation) must get the HTML shell, never a serialized Promise (`{}`, 2026-10-08).
++ const sendShell = require('../server/web/shell').createSendShell({ shellHtml: seo.shellHtml, sendDocument: () => false, assets });
++ app.get('*', pageStatus.spaFallback(sendShell));
const server = http.createServer(app);
await new Promise((r) => server.listen(0, '127.0.0.1', r));