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
Changes
@@@ -54,7 +54,7 @@ function createApp({ config, db, store, engine, auth, keys, outbox, relay, purgeapp.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 }));@@@ -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;@@@ -28,6 +28,10 @@ t('the front page is a search form, indexable, no-store, with a strict CSP', asyassert.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', asyncconst 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, nassert.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 <script>alert(1)</script> nesting'));assert.ok(!r.text.includes('<script>alert(1)'));assert.ok(r.text.includes('href="https://openvibe.wiki/p/kestrel?a=1&b=2"'));
@@ -54,7 +54,7 @@ function createApp({ config, db, store, engine, auth, keys, outbox, relay, purge
@@ -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(queryRouter({ config, store, engine, auth, searcher }));
app.use(savedRouter({ config, saved, searcher, auth }));
app.use(savedRouter({ config, saved, searcher, auth }));
// GET / (HTML search page for browsers, the text route index otherwise) and /robots.txt.
// 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 }));
app.use((req, res) => http.sendProblem(res, 404, 'search.not_found', { detail: `no route ${req.method} ${req.path}`, ctx: req.ov }));
@@ -71,14 +71,14 @@ const TEXT_INDEX = [
@@ -71,14 +71,14 @@ const TEXT_INDEX = [
'',
'',
].join('\n');
].join('\n');
-function layout({ title, q, owner, type, body, noindex }) {
+function layout({ title, q, owner, type, body, noindex, canonical }) {
return `<!doctype html>
return `<!doctype html>
<html lang="en">
<html lang="en">
<head>
<head>
<meta charset="utf-8">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${esc(title)}</title>
<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">
<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')}
${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>
<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
@@ -144,7 +144,9 @@ ${r.snippet_html ? `<p class="snip">${r.snippet_html}</p>` : r.summary ? `<p cla
}).join('\n')}</ol>`;
}).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();
const router = express.Router();
router.get('/frame-init.js', (_req, res) => {
router.get('/frame-init.js', (_req, res) => {
@@ -156,7 +158,7 @@ function pageRouter({ searcher, auth }) {
@@ -156,7 +158,7 @@ function pageRouter({ searcher, auth }) {
res.setHeader('Content-Security-Policy', CSP);
res.setHeader('Content-Security-Policy', CSP);
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('Cache-Control', 'public, max-age=60');
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) => {
router.get('/robots.txt', (_req, res) => {
@@ -209,7 +211,7 @@ function pageRouter({ searcher, auth }) {
@@ -209,7 +211,7 @@ function pageRouter({ searcher, auth }) {
body = `<p class="empty">${esc(err.message)}.</p>`;
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;
return router;
@@ -28,6 +28,10 @@ t('the front page is a search form, indexable, no-store, with a strict CSP', asy
@@ -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.match(r.text, /<form method="get" action="\/" role="search">/);
assert.ok(!/noindex/.test(r.text));
assert.ok(!/noindex/.test(r.text));
assert.strictEqual(r.headers.get('x-robots-tag'), null);
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.strictEqual(r.headers.get('cache-control'), 'no-store');
assert.match(r.headers.get('content-security-policy'), /default-src 'none'/);
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
// 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
@@ -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');
const r = await html('/updates');
assert.strictEqual(r.status, 200);
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.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'/);
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
@@ -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.status, 200);
assert.strictEqual(r.headers.get('x-robots-tag'), 'noindex, nofollow');
assert.strictEqual(r.headers.get('x-robots-tag'), 'noindex, nofollow');
assert.match(r.text, /<meta name="robots" content="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 <script>alert(1)</script> nesting'));
assert.ok(r.text.includes('Kestrel <script>alert(1)</script> nesting'));
assert.ok(!r.text.includes('<script>alert(1)'));
assert.ok(!r.text.includes('<script>alert(1)'));
assert.ok(r.text.includes('href="https://openvibe.wiki/p/kestrel?a=1&b=2"'));
assert.ok(r.text.includes('href="https://openvibe.wiki/p/kestrel?a=1&b=2"'));