openvibers/openvibe.ai Commits
A missing page answers the site's 404 page in the Frame (#248)
Changes
@@@ -237,7 +237,8 @@ function createApp({ config, db, registry, pool, quotas, cache, runs, auth, keys// A browser asking for a page that is not here gets a page; an API client the problem document.app.use((req, res) => {if (req.method === 'GET' && !req.path.startsWith('/api/') && !req.path.startsWith('/v1/') && req.accepts(['application/json', 'text/html']) === 'text/html') {-- return res.status(404).type('html').set('Content-Security-Policy', home.HOME_CSP).set('Cache-Control', 'no-store').send(home.renderNotFound());++ return res.status(404).type('html').set('Content-Security-Policy', home.HOME_CSP).set('Cache-Control', 'no-store')++ .set('X-Robots-Tag', 'noindex').send(home.renderNotFound({ siteUrl: config.baseUrl }));}return http.sendProblem(res, 404, 'ai.not_found', { detail: `no route ${req.method} ${req.path}`, ctx: req.ov });});@@@ -414,12 +414,35 @@ function llmsTxt({ siteUrl }) {].join('\n');}--/** The page a browser gets for a path nothing serves: a real document (language, title, the app icon), not JSON. */--function renderNotFound() {-- return ['<!doctype html>', '<html lang="en">', '<head>', '<meta charset="utf-8">', '<meta name="viewport" content="width=device-width, initial-scale=1">',-- `<title>Not found · ${SITE_NAME}</title>`, '<meta name="robots" content="noindex">', appIcon.headTags({ site: 'ai' }), '</head>',-- '<body style="font-family:system-ui,sans-serif;margin:3rem auto;max-width:36rem;padding:0 1rem;line-height:1.5">',-- '<h1>Not found</h1>', `<p>Nothing is here. <a href="/">${SITE_NAME}</a> · <a href="/stats">Prices and latency</a> · <a href="${NETWORK_URL}">OpenVibe</a></p>`, '</body>', '</html>'].join('\n');++/**++ * The page a browser gets for a path nothing serves: the site's own Frame, one way back, and no clue++ * about what was asked for. An API client keeps the problem document (server/app.js).++ */++function renderNotFound({ siteUrl = 'https://ai.openvibe.services' } = {}) {++ const host = String(siteUrl).replace(/^https?:\/\//, '').replace(/\/.*$/, '');++ return shell.page({++ name: SITE_NAME, service: 'ai', lang: 'en',++ title: `Page not found · ${SITE_NAME}`,++ siteName: SITE_NAME,++ description: `There is no page at this address on ${host}.`,++ robots: 'noindex, follow',++ navbar: { service: 'ai', apiBase: NETWORK_URL, links: NAV_LINKS, history: { type: 'page', title: SITE_NAME } },++ footer: FOOTER, home: '/', navLinks: NAV_LINKS,++ head: [++ appIcon.headTags({ site: 'ai' }),++ `<link rel="stylesheet" href="${ovServe.url(showcase.STYLESHEET)}">`,++ ].join('\n'),++ body: `<div id="navbar-mount"></div>++<main id="main" class="page">++<h1>Page not found</h1>++<p>There is no page at this address on ${esc(host)}. The link may be old or mistyped.</p>++<div class="sc-actions">++<a class="sc-btn sc-primary" href="/">${SITE_NAME} home</a>++<a class="sc-btn" href="/stats">Prices and latency</a>++<a class="sc-btn" href="${GUIDE}">Developer guide</a>++</div>++</main>`,++ });}module.exports = { renderHome, renderUpdates, renderStats, llmsTxt, llmsFullTxt, PUBLIC_PAGES, HOME_CSP, SITE_NAME, renderNotFound, FAQ, DESCRIPTION };@@@ -183,13 +183,22 @@ t.test('the CSPs let Cloudflare Web Analytics load and report (it injects its beassert.strictEqual(stats.headers.get('content-security-policy'), HOME_CSP);});--t.test('a browser gets a page for a path nothing serves; an API client the problem document; /favicon.ico is the icon', async () => {++t.test('a browser that asks for a missing page gets the site 404 page; an API client the problem document', async () => {const page = await request(h.base, 'GET', '/no-such-page', { headers: { Accept: BROWSER } });assert.strictEqual(page.status, 404);-- assert.ok(page.text.includes('<html lang="en">') && page.text.includes('<title>Not found') && page.text.includes('rel="icon"'));++ assert.match(page.headers.get('content-type'), /^text\/html/);++ assert.ok(page.text.includes('<h1>Page not found</h1>'));++ assert.ok(page.text.includes('There is no page at this address on ai.openvibe.services. The link may be old or mistyped.'));++ assert.ok(page.text.includes('id="navbar-mount"'), 'the page is in the site Frame');++ assert.ok(page.text.includes('<meta name="robots" content="noindex, follow">'));++ assert.strictEqual(page.headers.get('x-robots-tag'), 'noindex');++ assert.ok(page.text.includes('class="sc-btn sc-primary" href="/"'), 'home is the first way back');const api = await request(h.base, 'GET', '/api/v1/no-such-thing', { headers: { Accept: BROWSER } });assert.strictEqual(api.status, 404);assert.match(api.headers.get('content-type'), /json/);++ const notHtml = await request(h.base, 'GET', '/no-such-page', { headers: { Accept: 'application/json' } });++ assert.strictEqual(notHtml.status, 404);++ assert.match(notHtml.headers.get('content-type'), /json/);const icon = await request(h.base, 'GET', '/favicon.ico');assert.strictEqual(icon.status, 200);assert.match(icon.headers.get('content-type'), /^image\/svg\+xml/);
@@ -237,7 +237,8 @@ function createApp({ config, db, registry, pool, quotas, cache, runs, auth, keys
@@ -237,7 +237,8 @@ function createApp({ config, db, registry, pool, quotas, cache, runs, auth, keys
// A browser asking for a page that is not here gets a page; an API client the problem document.
// A browser asking for a page that is not here gets a page; an API client the problem document.
app.use((req, res) => {
app.use((req, res) => {
if (req.method === 'GET' && !req.path.startsWith('/api/') && !req.path.startsWith('/v1/') && req.accepts(['application/json', 'text/html']) === 'text/html') {
if (req.method === 'GET' && !req.path.startsWith('/api/') && !req.path.startsWith('/v1/') && req.accepts(['application/json', 'text/html']) === 'text/html') {
- return res.status(404).type('html').set('Content-Security-Policy', home.HOME_CSP).set('Cache-Control', 'no-store').send(home.renderNotFound());
+ return res.status(404).type('html').set('Content-Security-Policy', home.HOME_CSP).set('Cache-Control', 'no-store')
+ .set('X-Robots-Tag', 'noindex').send(home.renderNotFound({ siteUrl: config.baseUrl }));
}
}
return http.sendProblem(res, 404, 'ai.not_found', { detail: `no route ${req.method} ${req.path}`, ctx: req.ov });
return http.sendProblem(res, 404, 'ai.not_found', { detail: `no route ${req.method} ${req.path}`, ctx: req.ov });
});
});
@@ -414,12 +414,35 @@ function llmsTxt({ siteUrl }) {
@@ -414,12 +414,35 @@ function llmsTxt({ siteUrl }) {
].join('\n');
].join('\n');
}
}
-/** The page a browser gets for a path nothing serves: a real document (language, title, the app icon), not JSON. */
-function renderNotFound() {
- return ['<!doctype html>', '<html lang="en">', '<head>', '<meta charset="utf-8">', '<meta name="viewport" content="width=device-width, initial-scale=1">',
- `<title>Not found · ${SITE_NAME}</title>`, '<meta name="robots" content="noindex">', appIcon.headTags({ site: 'ai' }), '</head>',
- '<body style="font-family:system-ui,sans-serif;margin:3rem auto;max-width:36rem;padding:0 1rem;line-height:1.5">',
- '<h1>Not found</h1>', `<p>Nothing is here. <a href="/">${SITE_NAME}</a> · <a href="/stats">Prices and latency</a> · <a href="${NETWORK_URL}">OpenVibe</a></p>`, '</body>', '</html>'].join('\n');
+/**
+ * The page a browser gets for a path nothing serves: the site's own Frame, one way back, and no clue
+ * about what was asked for. An API client keeps the problem document (server/app.js).
+ */
+function renderNotFound({ siteUrl = 'https://ai.openvibe.services' } = {}) {
+ const host = String(siteUrl).replace(/^https?:\/\//, '').replace(/\/.*$/, '');
+ return shell.page({
+ name: SITE_NAME, service: 'ai', lang: 'en',
+ title: `Page not found · ${SITE_NAME}`,
+ siteName: SITE_NAME,
+ description: `There is no page at this address on ${host}.`,
+ robots: 'noindex, follow',
+ navbar: { service: 'ai', apiBase: NETWORK_URL, links: NAV_LINKS, history: { type: 'page', title: SITE_NAME } },
+ footer: FOOTER, home: '/', navLinks: NAV_LINKS,
+ head: [
+ appIcon.headTags({ site: 'ai' }),
+ `<link rel="stylesheet" href="${ovServe.url(showcase.STYLESHEET)}">`,
+ ].join('\n'),
+ body: `<div id="navbar-mount"></div>
+<main id="main" class="page">
+<h1>Page not found</h1>
+<p>There is no page at this address on ${esc(host)}. The link may be old or mistyped.</p>
+<div class="sc-actions">
+<a class="sc-btn sc-primary" href="/">${SITE_NAME} home</a>
+<a class="sc-btn" href="/stats">Prices and latency</a>
+<a class="sc-btn" href="${GUIDE}">Developer guide</a>
+</div>
+</main>`,
+ });
}
}
module.exports = { renderHome, renderUpdates, renderStats, llmsTxt, llmsFullTxt, PUBLIC_PAGES, HOME_CSP, SITE_NAME, renderNotFound, FAQ, DESCRIPTION };
module.exports = { renderHome, renderUpdates, renderStats, llmsTxt, llmsFullTxt, PUBLIC_PAGES, HOME_CSP, SITE_NAME, renderNotFound, FAQ, DESCRIPTION };
@@ -183,13 +183,22 @@ t.test('the CSPs let Cloudflare Web Analytics load and report (it injects its be
@@ -183,13 +183,22 @@ t.test('the CSPs let Cloudflare Web Analytics load and report (it injects its be
assert.strictEqual(stats.headers.get('content-security-policy'), HOME_CSP);
assert.strictEqual(stats.headers.get('content-security-policy'), HOME_CSP);
});
});
-t.test('a browser gets a page for a path nothing serves; an API client the problem document; /favicon.ico is the icon', async () => {
+t.test('a browser that asks for a missing page gets the site 404 page; an API client the problem document', async () => {
const page = await request(h.base, 'GET', '/no-such-page', { headers: { Accept: BROWSER } });
const page = await request(h.base, 'GET', '/no-such-page', { headers: { Accept: BROWSER } });
assert.strictEqual(page.status, 404);
assert.strictEqual(page.status, 404);
- assert.ok(page.text.includes('<html lang="en">') && page.text.includes('<title>Not found') && page.text.includes('rel="icon"'));
+ assert.match(page.headers.get('content-type'), /^text\/html/);
+ assert.ok(page.text.includes('<h1>Page not found</h1>'));
+ assert.ok(page.text.includes('There is no page at this address on ai.openvibe.services. The link may be old or mistyped.'));
+ assert.ok(page.text.includes('id="navbar-mount"'), 'the page is in the site Frame');
+ assert.ok(page.text.includes('<meta name="robots" content="noindex, follow">'));
+ assert.strictEqual(page.headers.get('x-robots-tag'), 'noindex');
+ assert.ok(page.text.includes('class="sc-btn sc-primary" href="/"'), 'home is the first way back');
const api = await request(h.base, 'GET', '/api/v1/no-such-thing', { headers: { Accept: BROWSER } });
const api = await request(h.base, 'GET', '/api/v1/no-such-thing', { headers: { Accept: BROWSER } });
assert.strictEqual(api.status, 404);
assert.strictEqual(api.status, 404);
assert.match(api.headers.get('content-type'), /json/);
assert.match(api.headers.get('content-type'), /json/);
+ const notHtml = await request(h.base, 'GET', '/no-such-page', { headers: { Accept: 'application/json' } });
+ assert.strictEqual(notHtml.status, 404);
+ assert.match(notHtml.headers.get('content-type'), /json/);
const icon = await request(h.base, 'GET', '/favicon.ico');
const icon = await request(h.base, 'GET', '/favicon.ico');
assert.strictEqual(icon.status, 200);
assert.strictEqual(icon.status, 200);
assert.match(icon.headers.get('content-type'), /^image\/svg\+xml/);
assert.match(icon.headers.get('content-type'), /^image\/svg\+xml/);