openvibers/openvibe.live Commits
Live: channel pages, /updates, /settings, /themes, /admin and /documentation answer the page again, not {}. (#48)
Changes
@@@ -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 ─────────────────────────────────────nnew file mode 100644@@@ -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 };@@@ -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));
@@ -1102,15 +1102,7 @@ app.get('/banned', async (req, res) => {
@@ -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.
// own head describes the home page), and no other page claims the home page as its canonical.
let _shellHtml = null;
let _shellHtml = null;
try { _shellHtml = require('./seo').shellHtml; } catch { _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)));
app.get('*', require('./web/page-status').spaFallback((res, urlPath) => sendShell(res, urlPath)));
// ── Global Error Handler ─────────────────────────────────────
// ── Global Error Handler ─────────────────────────────────────
new file mode 100644
new file mode 100644
@@ -0,0 +1,24 @@
@@ -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 };
@@ -68,8 +68,10 @@ async function expectAll(paths, want, opts) {
@@ -68,8 +68,10 @@ async function expectAll(paths, want, opts) {
const wrong = [];
const wrong = [];
for (const p of paths) {
for (const p of paths) {
for (const html of [false, true]) {
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(', ')}`);
assert.deepStrictEqual(wrong, [], `expected ${want}: ${wrong.join(', ')}`);
@@ -121,14 +123,10 @@ async function main() {
@@ -121,14 +123,10 @@ async function main() {
// ── The app: the SEO middleware, then the real SPA fallback (as server/index.js mounts them) ──
// ── The app: the SEO middleware, then the real SPA fallback (as server/index.js mounts them) ──
const app = express();
const app = express();
seo.register(app);
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);
const server = http.createServer(app);
await new Promise((r) => server.listen(0, '127.0.0.1', r));
await new Promise((r) => server.listen(0, '127.0.0.1', r));