openvibers/openvibe.search Commits
A missing page answers the site's 404 page in the Frame (#173)
Changes
@@@ -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.networkres.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;}@@@ -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/);
@@ -453,6 +453,40 @@ ${r.snippet_html ? `<p class="snip">${r.snippet_html}</p>` : r.summary ? `<p cla
@@ -453,6 +453,40 @@ ${r.snippet_html ? `<p class="snip">${r.snippet_html}</p>` : r.summary ? `<p cla
}).join('\n')}</ol>`;
}).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.
/** 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
* 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). */
* lastmod); releasedAt is the deployed release's date (/updates' lastmod). */
@@ -565,6 +599,19 @@ function pageRouter({ searcher, auth, baseUrl = 'https://search.openvibe.network
@@ -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 : '' }));
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;
return router;
}
}
@@ -150,6 +150,25 @@ t('paging with More results, filters carried along, and an honest empty state',
@@ -150,6 +150,25 @@ t('paging with More results, filters carried along, and an honest empty state',
assert.match(bad.text, /owner is malformed/);
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 () => {
t('non-HTML clients get the text route index; robots.txt keeps result pages out', async () => {
const r = await request(svc.base, 'GET', '/');
const r = await request(svc.base, 'GET', '/');
assert.match(r.headers.get('content-type'), /text\/plain/);
assert.match(r.headers.get('content-type'), /text\/plain/);