openvibers/openvibe.search Commits

A missing page answers the site's 404 page in the Frame (#173)

OP OpenVibers committed 12 days ago · cabf2437c1 · 2 files, +66 −0

A browser that asks for a path Search does not serve gets the site's own 404 page (the OpenVibe Frame, noindex, one line naming the host, links home and to the update log). An API path, another method or a request whose Accept is not text/html keeps the problem document.

Changes

ddiff --git a/server/web/page.js b/server/web/page.js
iindex ce9d1b1..d5cd043 100644
---- a/server/web/page.js
++++ b/server/web/page.js
@@@ -453,6 +453,40 @@ ${r.snippet_html ? `<p class="snip">${r.snippet_html}</p>` : r.summary ? `<p cla
}).join('\n')}</ol>`;
}
++// The page a browser gets for a path nothing serves: the site's own Frame, a way back, and no clue
++// about what was asked for. An API client keeps the problem document (server/app.js).
++function renderNotFound({ origin }) {
++ const host = String(origin).replace(/^https?:\/\//, '').replace(/\/.*$/, '');
++ return shell.page({
++ ...FRAME,
++ title: `Page not found · ${SITE_NAME}`,
++ description: `There is no page at this address on ${host}.`,
++ robots: 'noindex, nofollow',
++ head: [
++ '<meta name="color-scheme" content="light dark">',
++ ICON_LINKS,
++ `<link rel="stylesheet" href="${ovServe.url(showcase.STYLESHEET)}">`,
++ `<script>${FOOTER_INIT}</script>`,
++ `<style>
++body{margin:0;background:var(--bg-primary,#fff);color:var(--text-primary,#1a1a1a);font:16px/1.5 system-ui,-apple-system,"Segoe UI",sans-serif}
++main{max-width:760px;margin:0 auto;padding:48px 16px 64px}
++h1{font-size:1.5rem;margin:0 0 8px}
++p{color:var(--text-secondary,#5c5c66);margin:0 0 22px}
++.nf-links{list-style:none;margin:0;padding:0;display:flex;flex-wrap:wrap;gap:10px}
++</style>`,
++ ].join('\n'),
++ body: `<div id="navbar-mount"></div>
++<main id="main">
++<h1>Page not found</h1>
++<p>There is no page at this address on ${esc(host)}. The link may be old or mistyped.</p>
++<ul class="nf-links">
++<li><a class="sc-btn sc-primary" href="/">${SITE_NAME} home</a></li>
++<li><a class="sc-btn" href="/updates">What shipped</a></li>
++</ul>
++</main>`,
++ });
++}
++
/** baseUrl (config.baseUrl) names the canonical URL of the pages that may be indexed: the front page and /updates.
* newestPublic() reads the newest content time among the public indexable documents (the sitemap's real
* lastmod); releasedAt is the deployed release's date (/updates' lastmod). */
@@@ -565,6 +599,19 @@ function pageRouter({ searcher, auth, baseUrl = 'https://search.openvibe.network
res.status(status).type('html').send(layout({ title: q ? `${q} · OpenVibe.Search` : isFront ? FRONT_TITLE : 'OpenVibe.Search', q, owner, type, body, noindex: searching, canonical: `${origin}/`, jsonLd, front: isFront, after: isFront ? after : '', heroAside: isFront ? front.statList : '' }));
});
++ // A browser asking for a page nothing serves gets the site's own 404 page; an API path, another
++ // method or a client that does not accept HTML keeps the problem document (server/app.js).
++ router.use((req, res, next) => {
++ if (req.path.startsWith('/api/') || req.path.startsWith('/internal/')) return next();
++ if (req.method !== 'GET' || req.accepts(['application/json', 'text/html']) !== 'text/html') return next();
++ res.status(404);
++ res.setHeader('Content-Security-Policy', CSP);
++ res.setHeader('X-Frame-Options', 'DENY');
++ res.setHeader('Cache-Control', 'no-store');
++ res.setHeader('X-Robots-Tag', 'noindex, nofollow');
++ return res.type('html').send(renderNotFound({ origin }));
++ });
++
return router;
}
ddiff --git a/test/page.test.js b/test/page.test.js
iindex 75eed26..22ce93a 100644
---- a/test/page.test.js
++++ b/test/page.test.js
@@@ -150,6 +150,25 @@ t('paging with More results, filters carried along, and an honest empty state',
assert.match(bad.text, /owner is malformed/);
});
++t('a browser that asks for a missing page gets the site 404 page; an API client the problem document', async () => {
++ const r = await html('/no-such-page-xyz');
++ assert.strictEqual(r.status, 404);
++ assert.match(r.headers.get('content-type'), /^text\/html/);
++ assert.ok(r.text.includes('<h1>Page not found</h1>'), r.text.slice(0, 300));
++ assert.match(r.text, /There is no page at this address on [^<]+\. The link may be old or mistyped\./);
++ assert.ok(r.text.includes('id="navbar-mount"'), 'the page is in the site Frame');
++ assert.ok(r.text.includes('<meta name="robots" content="noindex, nofollow">'));
++ assert.strictEqual(r.headers.get('x-robots-tag'), 'noindex, nofollow');
++ assert.ok(r.text.includes('class="sc-btn sc-primary" href="/"'), 'home is the first way back');
++ assert.match(r.headers.get('content-security-policy'), /default-src 'none'/);
++ const api = await request(svc.base, 'GET', '/api/no-such', { headers: { Accept: 'text/html' } });
++ assert.strictEqual(api.status, 404);
++ assert.match(api.headers.get('content-type'), /problem\+json/);
++ const notHtml = await request(svc.base, 'GET', '/no-such-page-xyz', { headers: { Accept: 'application/json' } });
++ assert.strictEqual(notHtml.status, 404);
++ assert.match(notHtml.headers.get('content-type'), /problem\+json/);
++});
++
t('non-HTML clients get the text route index; robots.txt keeps result pages out', async () => {
const r = await request(svc.base, 'GET', '/');
assert.match(r.headers.get('content-type'), /text\/plain/);