openvibers/openrestream Commits
A missing page answers the site's 404 page in the Frame (#174)
Changes
@@@ -186,7 +186,13 @@ function createApp({ rt, auth, keys, log = console, fetchImpl }) {app.use('/', uiRoutes);}-- app.use((req, res) => contracts.http.sendProblem(res, 404, 'openre.not_found', { detail: `no route ${req.method} ${req.path}`, ctx: req.ov }));++ app.use((req, res) => {++ if (req.path.startsWith('/api/') || req.path.startsWith('/internal/') || req.path.startsWith('/oauth/') || req.accepts(['application/json', 'text/html']) !== 'text/html') {++ return contracts.http.sendProblem(res, 404, 'openre.not_found', { detail: `no route ${req.method} ${req.path}`, ctx: req.ov });++ }++ return res.status(404).type('html').set('Cache-Control', 'no-cache').set('X-Robots-Tag', 'noindex')++ .send(require('./ui/not-found').renderNotFound({ config }));++ });// eslint-disable-next-line no-unused-varsapp.use((err, req, res, _next) => {if (err.type === 'entity.parse.failed') return contracts.http.sendProblem(res, 400, 'openre.bad_json', { detail: 'request body is not valid JSON', ctx: req.ov });@@@ -55,6 +55,9 @@ main.ore .sc-hero,main.ore .sc-sec,main.ore .sc-cta{padding-left:0;padding-right.ore .ore-option{display:flex;gap:10px;align-items:flex-start;margin:0;padding:10px 12px;border:1px solid var(--ore-line);border-radius:8px;background:var(--ore-field);color:var(--ore-text);font-size:.95rem;cursor:pointer}.ore .ore-option:has(input:checked){border-color:var(--ore-accent);box-shadow:inset 0 0 0 1px var(--ore-accent)}.ore .ore-option:has(input:focus-visible){outline:2px solid var(--ore-accent);outline-offset:2px}.ore .ore-option input{margin:.3em 0 0;accent-color:var(--ore-accent);flex:none}.ore .ore-option strong{display:block}.ore .ore-option .muted{display:block;font-size:.86rem;margin-top:2px}++.ore .nf-links{list-style:none;margin:22px 0 0;padding:0;display:flex;flex-wrap:wrap;gap:10px}++.ore .nf-links a{display:inline-block;padding:9px 16px;border-radius:999px;border:1px solid var(--ore-line);background:var(--ore-panel);color:var(--ore-text);text-decoration:none;font-weight:600}++.ore .nf-links a:hover,.ore .nf-links a:focus-visible{border-color:var(--ore-accent);outline:0}.ore form:has(input[name=record_to][value=videos]:checked) .ore-live-only{display:none}@media (max-width:640px){.ore th:nth-child(n+4),.ore td:nth-child(n+4){display:none}}/* Touch targets on phones (fleet audit, 2026-10-29): touch pointers only. */nnew file mode 100644@@@ -0,0 +1,24 @@++'use strict';++/**++ * The page a browser gets for a path nothing serves: OpenRestream's own Frame, a way back, and no clue++ * about what was asked for. An API client keeps the problem document (server/app.js).++ */++const { renderPage, esc } = require('./layout');++++function renderNotFound({ config }) {++ const host = String(config.baseUrl).replace(/^https?:\/\//, '').replace(/\/.*$/, '');++ return renderPage({++ title: 'Page not found',++ robots: 'noindex,nofollow',++ config,++ body: `<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 href="/">OpenRestream home</a></li>++<li><a href="/streams">Streams</a></li>++<li><a href="/sessions">Sessions</a></li>++</ul>`,++ });++}++++module.exports = { renderNotFound };nnew file mode 100644@@@ -0,0 +1,36 @@++'use strict';++// A path nothing serves: a browser gets OpenRestream's own 404 page in the Frame, an API client (or a++// client that does not accept HTML) keeps the problem document.++const assert = require('assert');++const { bootApi, request, suite } = require('./helpers');++++const t = suite('not-found');++++t('a browser that asks for a missing page gets the site 404 page; an API client the problem document', async () => {++ const api = await bootApi({});++ try {++ const host = api.config.baseUrl.replace(/^https?:\/\//, '').replace(/\/.*$/, '');++ const page = await request(api.base, 'GET', '/no-such-page-xyz', { headers: { Accept: 'text/html,application/xhtml+xml' } });++ assert.strictEqual(page.status, 404);++ assert.match(page.headers.get('content-type'), /^text\/html/);++ assert.ok(page.text.includes('<h1>Page not found</h1>'), page.text.slice(0, 300));++ assert.ok(page.text.includes(`There is no page at this address on ${host}. 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,nofollow">'));++ assert.strictEqual(page.headers.get('x-robots-tag'), 'noindex');++ assert.ok(page.text.includes('class="nf-links"') && page.text.includes('href="/"'), 'home is the first way back');++++ const apiPath = await request(api.base, 'GET', '/api/no-such', { headers: { Accept: 'text/html' } });++ assert.strictEqual(apiPath.status, 404);++ assert.match(apiPath.headers.get('content-type'), /problem\+json/);++ assert.strictEqual(apiPath.body.code, 'openre.not_found');++++ const notHtml = await request(api.base, 'GET', '/no-such-page-xyz', { headers: { Accept: 'application/json' } });++ assert.strictEqual(notHtml.status, 404);++ assert.match(notHtml.headers.get('content-type'), /problem\+json/);++ } finally {++ await api.close();++ }++});++++t.run();
@@ -186,7 +186,13 @@ function createApp({ rt, auth, keys, log = console, fetchImpl }) {
@@ -186,7 +186,13 @@ function createApp({ rt, auth, keys, log = console, fetchImpl }) {
app.use('/', uiRoutes);
app.use('/', uiRoutes);
}
}
- app.use((req, res) => contracts.http.sendProblem(res, 404, 'openre.not_found', { detail: `no route ${req.method} ${req.path}`, ctx: req.ov }));
+ app.use((req, res) => {
+ if (req.path.startsWith('/api/') || req.path.startsWith('/internal/') || req.path.startsWith('/oauth/') || req.accepts(['application/json', 'text/html']) !== 'text/html') {
+ return contracts.http.sendProblem(res, 404, 'openre.not_found', { detail: `no route ${req.method} ${req.path}`, ctx: req.ov });
+ }
+ return res.status(404).type('html').set('Cache-Control', 'no-cache').set('X-Robots-Tag', 'noindex')
+ .send(require('./ui/not-found').renderNotFound({ config }));
+ });
// eslint-disable-next-line no-unused-vars
// eslint-disable-next-line no-unused-vars
app.use((err, req, res, _next) => {
app.use((err, req, res, _next) => {
if (err.type === 'entity.parse.failed') return contracts.http.sendProblem(res, 400, 'openre.bad_json', { detail: 'request body is not valid JSON', ctx: req.ov });
if (err.type === 'entity.parse.failed') return contracts.http.sendProblem(res, 400, 'openre.bad_json', { detail: 'request body is not valid JSON', ctx: req.ov });
@@ -55,6 +55,9 @@ main.ore .sc-hero,main.ore .sc-sec,main.ore .sc-cta{padding-left:0;padding-right
@@ -55,6 +55,9 @@ main.ore .sc-hero,main.ore .sc-sec,main.ore .sc-cta{padding-left:0;padding-right
.ore .ore-option{display:flex;gap:10px;align-items:flex-start;margin:0;padding:10px 12px;border:1px solid var(--ore-line);border-radius:8px;background:var(--ore-field);color:var(--ore-text);font-size:.95rem;cursor:pointer}
.ore .ore-option{display:flex;gap:10px;align-items:flex-start;margin:0;padding:10px 12px;border:1px solid var(--ore-line);border-radius:8px;background:var(--ore-field);color:var(--ore-text);font-size:.95rem;cursor:pointer}
.ore .ore-option:has(input:checked){border-color:var(--ore-accent);box-shadow:inset 0 0 0 1px var(--ore-accent)}.ore .ore-option:has(input:focus-visible){outline:2px solid var(--ore-accent);outline-offset:2px}
.ore .ore-option:has(input:checked){border-color:var(--ore-accent);box-shadow:inset 0 0 0 1px var(--ore-accent)}.ore .ore-option:has(input:focus-visible){outline:2px solid var(--ore-accent);outline-offset:2px}
.ore .ore-option input{margin:.3em 0 0;accent-color:var(--ore-accent);flex:none}.ore .ore-option strong{display:block}.ore .ore-option .muted{display:block;font-size:.86rem;margin-top:2px}
.ore .ore-option input{margin:.3em 0 0;accent-color:var(--ore-accent);flex:none}.ore .ore-option strong{display:block}.ore .ore-option .muted{display:block;font-size:.86rem;margin-top:2px}
+.ore .nf-links{list-style:none;margin:22px 0 0;padding:0;display:flex;flex-wrap:wrap;gap:10px}
+.ore .nf-links a{display:inline-block;padding:9px 16px;border-radius:999px;border:1px solid var(--ore-line);background:var(--ore-panel);color:var(--ore-text);text-decoration:none;font-weight:600}
+.ore .nf-links a:hover,.ore .nf-links a:focus-visible{border-color:var(--ore-accent);outline:0}
.ore form:has(input[name=record_to][value=videos]:checked) .ore-live-only{display:none}
.ore form:has(input[name=record_to][value=videos]:checked) .ore-live-only{display:none}
@media (max-width:640px){.ore th:nth-child(n+4),.ore td:nth-child(n+4){display:none}}
@media (max-width:640px){.ore th:nth-child(n+4),.ore td:nth-child(n+4){display:none}}
/* Touch targets on phones (fleet audit, 2026-10-29): touch pointers only. */
/* Touch targets on phones (fleet audit, 2026-10-29): touch pointers only. */
new file mode 100644
new file mode 100644
@@ -0,0 +1,24 @@
@@ -0,0 +1,24 @@
+'use strict';
+/**
+ * The page a browser gets for a path nothing serves: OpenRestream's own Frame, a way back, and no clue
+ * about what was asked for. An API client keeps the problem document (server/app.js).
+ */
+const { renderPage, esc } = require('./layout');
+
+function renderNotFound({ config }) {
+ const host = String(config.baseUrl).replace(/^https?:\/\//, '').replace(/\/.*$/, '');
+ return renderPage({
+ title: 'Page not found',
+ robots: 'noindex,nofollow',
+ config,
+ body: `<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 href="/">OpenRestream home</a></li>
+<li><a href="/streams">Streams</a></li>
+<li><a href="/sessions">Sessions</a></li>
+</ul>`,
+ });
+}
+
+module.exports = { renderNotFound };
new file mode 100644
new file mode 100644
@@ -0,0 +1,36 @@
@@ -0,0 +1,36 @@
+'use strict';
+// A path nothing serves: a browser gets OpenRestream's own 404 page in the Frame, an API client (or a
+// client that does not accept HTML) keeps the problem document.
+const assert = require('assert');
+const { bootApi, request, suite } = require('./helpers');
+
+const t = suite('not-found');
+
+t('a browser that asks for a missing page gets the site 404 page; an API client the problem document', async () => {
+ const api = await bootApi({});
+ try {
+ const host = api.config.baseUrl.replace(/^https?:\/\//, '').replace(/\/.*$/, '');
+ const page = await request(api.base, 'GET', '/no-such-page-xyz', { headers: { Accept: 'text/html,application/xhtml+xml' } });
+ assert.strictEqual(page.status, 404);
+ assert.match(page.headers.get('content-type'), /^text\/html/);
+ assert.ok(page.text.includes('<h1>Page not found</h1>'), page.text.slice(0, 300));
+ assert.ok(page.text.includes(`There is no page at this address on ${host}. 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,nofollow">'));
+ assert.strictEqual(page.headers.get('x-robots-tag'), 'noindex');
+ assert.ok(page.text.includes('class="nf-links"') && page.text.includes('href="/"'), 'home is the first way back');
+
+ const apiPath = await request(api.base, 'GET', '/api/no-such', { headers: { Accept: 'text/html' } });
+ assert.strictEqual(apiPath.status, 404);
+ assert.match(apiPath.headers.get('content-type'), /problem\+json/);
+ assert.strictEqual(apiPath.body.code, 'openre.not_found');
+
+ const notHtml = await request(api.base, 'GET', '/no-such-page-xyz', { headers: { Accept: 'application/json' } });
+ assert.strictEqual(notHtml.status, 404);
+ assert.match(notHtml.headers.get('content-type'), /problem\+json/);
+ } finally {
+ await api.close();
+ }
+});
+
+t.run();