@@ -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+ '<' : '<' , '>' : '>' , '&' : '&' , '"' : '"' , "'" : ''' ,
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