openvibers/openvibe.search Commits

The front page and /updates carry a canonical URL on the configured origin (config.baseUrl, passed to pageRouter); result pages stay noindex without one. The browser check (Host scripts/browser-check.js, WS-Q task 3) found the indexable front page with no canonical. test/page.test.js

OP OpenVibers committed 26 Sept 2026 · 99f1e7627a · 3 files, +14 −6


Changes

ddiff --git a/server/app.js b/server/app.js
iindex 5a00cad..0907045 100644
---- a/server/app.js
++++ b/server/app.js
@@@ -54,7 +54,7 @@ function createApp({ config, db, store, engine, auth, keys, outbox, relay, purge
app.use(queryRouter({ config, store, engine, auth, searcher }));
app.use(savedRouter({ config, saved, searcher, auth }));
// GET / (HTML search page for browsers, the text route index otherwise) and /robots.txt.
-- app.use(pageRouter({ searcher, auth }));
++ app.use(pageRouter({ searcher, auth, baseUrl: config.baseUrl }));
app.use((req, res) => http.sendProblem(res, 404, 'search.not_found', { detail: `no route ${req.method} ${req.path}`, ctx: req.ov }));
ddiff --git a/server/web/page.js b/server/web/page.js
iindex 536cfc6..7ff32e7 100644
---- a/server/web/page.js
++++ b/server/web/page.js
@@@ -71,14 +71,14 @@ const TEXT_INDEX = [
'',
].join('\n');
--function layout({ title, q, owner, type, body, noindex }) {
++function layout({ title, q, owner, type, body, noindex, canonical }) {
return `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${esc(title)}</title>
--${noindex ? '<meta name="robots" content="noindex, nofollow">\n' : ''}<meta name="description" content="Search what the OpenVibe network's services have published.">
++${noindex ? '<meta name="robots" content="noindex, nofollow">\n' : ''}${canonical && !noindex ? `<link rel="canonical" href="${esc(canonical)}">\n` : ''}<meta name="description" content="Search what the OpenVibe network's services have published.">
<meta name="color-scheme" content="light dark">
${require('openvibe-shared/app-icon').headTags({ site: 'network', iconBase: `${NETWORK}/assets` }).split('\n').filter((l) => l.startsWith('<link') && !/rel="manifest"/.test(l)).join('\n')}
<script src="${ovServe.url('theme-loader.js')}" defer></script>
@@@ -144,7 +144,9 @@ ${r.snippet_html ? `<p class="snip">${r.snippet_html}</p>` : r.summary ? `<p cla
}).join('\n')}</ol>`;
}
--function pageRouter({ searcher, auth }) {
++/** baseUrl (config.baseUrl) names the canonical URL of the pages that may be indexed: the front page and /updates. */
++function pageRouter({ searcher, auth, baseUrl = 'https://search.openvibe.network' }) {
++ const origin = String(baseUrl).replace(/\/+$/, '');
const router = express.Router();
router.get('/frame-init.js', (_req, res) => {
@@@ -156,7 +158,7 @@ function pageRouter({ searcher, auth }) {
res.setHeader('Content-Security-Policy', CSP);
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('Cache-Control', 'public, max-age=60');
-- res.type('html').send(layout({ title: 'What shipped on OpenVibe.Search', q: '', owner: '', type: '', body: frame.updatesBody({ service: 'search', siteName: 'OpenVibe.Search' }) + `<script src="${ovServe.url('shipped.js')}" defer></script>` }));
++ res.type('html').send(layout({ title: 'What shipped on OpenVibe.Search', q: '', owner: '', type: '', body: frame.updatesBody({ service: 'search', siteName: 'OpenVibe.Search' }) + `<script src="${ovServe.url('shipped.js')}" defer></script>`, canonical: `${origin}/updates` }));
});
router.get('/robots.txt', (_req, res) => {
@@@ -209,7 +211,7 @@ function pageRouter({ searcher, auth }) {
body = `<p class="empty">${esc(err.message)}.</p>`;
}
}
-- res.status(status).type('html').send(layout({ title: q ? `${q} · OpenVibe.Search` : 'OpenVibe.Search', q, owner, type, body, noindex: searching }));
++ res.status(status).type('html').send(layout({ title: q ? `${q} · OpenVibe.Search` : 'OpenVibe.Search', q, owner, type, body, noindex: searching, canonical: `${origin}/` }));
});
return router;
ddiff --git a/test/page.test.js b/test/page.test.js
iindex 0faa83e..5e951db 100644
---- a/test/page.test.js
++++ b/test/page.test.js
@@@ -28,6 +28,10 @@ t('the front page is a search form, indexable, no-store, with a strict CSP', asy
assert.match(r.text, /<form method="get" action="\/" role="search">/);
assert.ok(!/noindex/.test(r.text));
assert.strictEqual(r.headers.get('x-robots-tag'), null);
++ // One canonical URL (the browser check found none): the configured origin's front page.
++ const canon = r.text.match(/<link rel="canonical" href="([^"]+)">/g) || [];
++ assert.strictEqual(canon.length, 1);
++ assert.match(canon[0], /href="https?:\/\/[^"/]+\/">$/);
assert.strictEqual(r.headers.get('cache-control'), 'no-store');
assert.match(r.headers.get('content-security-policy'), /default-src 'none'/);
// The OpenVibe Frame (navbar, footer, shipped views) is the only script: from this site or
@@@ -54,6 +58,7 @@ t('the Frame init is a same-origin script and /updates is the shared log', async
const r = await html('/updates');
assert.strictEqual(r.status, 200);
assert.ok(r.text.includes('What shipped on OpenVibe.Search') && r.text.includes('data-ov-shipped="log" data-service="search"'));
++ assert.match(r.text, /<link rel="canonical" href="https?:\/\/[^"/]+\/updates">/);
assert.match(r.headers.get('content-security-policy'), /default-src 'none'/);
});
@@@ -62,6 +67,7 @@ t('results show public documents only, escaped, linked to their canonical URL, n
assert.strictEqual(r.status, 200);
assert.strictEqual(r.headers.get('x-robots-tag'), 'noindex, nofollow');
assert.match(r.text, /<meta name="robots" content="noindex, nofollow">/);
++ assert.ok(!/rel="canonical"/.test(r.text), 'a result page is noindex: no canonical');
assert.ok(r.text.includes('Kestrel &lt;script&gt;alert(1)&lt;/script&gt; nesting'));
assert.ok(!r.text.includes('<script>alert(1)'));
assert.ok(r.text.includes('href="https://openvibe.wiki/p/kestrel?a=1&amp;b=2"'));