Skip to content

Commit db4f939

Browse files
hotlongCopilot
andcommitted
feat(cli/serve): render proper HTML 404 page for unknown hostnames
Browsers (Accept: text/html) now get a clean styled 404 page with dark-mode support and a link back to cloud.objectos.app. API clients (curl / fetch) still receive the JSON {error, message, hostname} shape via content negotiation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 2928cf3 commit db4f939

1 file changed

Lines changed: 78 additions & 0 deletions

File tree

packages/cli/src/commands/serve.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -654,6 +654,84 @@ export default class Serve extends Command {
654654
} catch {
655655
return next();
656656
}
657+
// Content negotiation: browsers (Accept: text/html) get
658+
// a clean 404 page; API clients (curl/fetch with JSON
659+
// accept) get a structured error body.
660+
const accept = (c.req.header('accept') || '').toLowerCase();
661+
const wantsHtml = accept.includes('text/html');
662+
if (wantsHtml) {
663+
const safeHost = host.replace(/[<>&"']/g, (ch: string) => ((({
664+
'<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;', "'": '&#39;',
665+
} as Record<string, string>)[ch]) ?? ch));
666+
const html = `<!doctype html>
667+
<html lang="en">
668+
<head>
669+
<meta charset="utf-8" />
670+
<meta name="viewport" content="width=device-width,initial-scale=1" />
671+
<title>404 — Environment not found</title>
672+
<style>
673+
:root { color-scheme: light dark; }
674+
* { box-sizing: border-box; }
675+
html, body { height: 100%; margin: 0; }
676+
body {
677+
font: 16px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
678+
background: #fafafa;
679+
color: #111;
680+
display: grid;
681+
place-items: center;
682+
padding: 24px;
683+
}
684+
@media (prefers-color-scheme: dark) {
685+
body { background: #0b0b0c; color: #e8e8e8; }
686+
.card { background: #141417; border-color: #26262b; }
687+
.host { background: #1c1c20; border-color: #2d2d33; color: #d0d0d0; }
688+
.muted { color: #8b8b94; }
689+
a { color: #6ea8fe; }
690+
}
691+
.card {
692+
max-width: 520px;
693+
width: 100%;
694+
background: #fff;
695+
border: 1px solid #e6e6e6;
696+
border-radius: 12px;
697+
padding: 32px;
698+
box-shadow: 0 1px 2px rgba(0,0,0,.04);
699+
text-align: center;
700+
}
701+
.code { font: 600 64px/1 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; margin: 0; letter-spacing: -2px; }
702+
h1 { font-size: 20px; margin: 16px 0 8px; font-weight: 600; }
703+
p { margin: 8px 0; }
704+
.muted { color: #666; font-size: 14px; }
705+
.host {
706+
display: inline-block;
707+
margin-top: 16px;
708+
padding: 6px 12px;
709+
background: #f4f4f5;
710+
border: 1px solid #e4e4e7;
711+
border-radius: 6px;
712+
font: 13px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
713+
color: #444;
714+
word-break: break-all;
715+
}
716+
a { color: #2563eb; text-decoration: none; }
717+
a:hover { text-decoration: underline; }
718+
</style>
719+
</head>
720+
<body>
721+
<main class="card">
722+
<p class="code">404</p>
723+
<h1>Environment not found</h1>
724+
<p class="muted">No ObjectStack environment is bound to this hostname.</p>
725+
<div class="host">${safeHost}</div>
726+
<p class="muted" style="margin-top:24px">
727+
If you own this domain, bind it to an environment in the
728+
<a href="https://cloud.objectos.app/">ObjectStack Cloud console</a>.
729+
</p>
730+
</main>
731+
</body>
732+
</html>`;
733+
return c.html(html, 404);
734+
}
657735
return c.json(
658736
{
659737
error: 'environment_not_found',

0 commit comments

Comments
 (0)