/**
* Error + status pages. Every page here is a self-contained HTML document —
* inline CSS only, no external stylesheet, no JavaScript (so it renders under the
* strict CSP, even when the app's assets are what failed). Theme-aware via
* `prefers-color-scheme`, styled in the WrNexus design language (ink-navy,
* azure, a faint blueprint grid + glow). Development shows the stack trace;
* production never leaks internal paths.
*/
import { escapeHtml } from "./security.ts";
export type Mode = "development" | "production";
interface ErrorPageOptions {
status: number;
/** Big display code, e.g. "404" / "500". */
code: string;
title: string;
message: string;
/** Monospace eyebrow, e.g. "ERROR 404". */
eyebrow?: string;
/** Optional dev-only detail (error name + stack), rendered in a code panel. */
detail?: { heading: string; body: string };
/** Show a "Back home" action (default true). */
home?: boolean;
}
/** Shared, self-contained, theme-aware error document. */
function errorDocument(o: ErrorPageOptions): string {
const eyebrow = escapeHtml(o.eyebrow ?? `ERROR ${o.status}`);
const title = escapeHtml(o.title);
const message = escapeHtml(o.message);
const detail = o.detail
? `
${escapeHtml(o.detail.heading)}
${escapeHtml(o.detail.body)}
`
: "";
const home = o.home === false ? "" : `Back to home`;
return `
${title}
${eyebrow}
${escapeHtml(o.code)}
${title}
${message}
${home}
${detail}
`;
}
/** Common status → friendly copy, for a generic HTML status page. */
const STATUS_COPY: Record = {
400: {
title: "Bad request",
message: "The request couldn't be understood. Check the URL and try again.",
},
401: { title: "Sign in required", message: "You need to be signed in to view this page." },
403: { title: "Access denied", message: "You don't have permission to view this page." },
404: {
title: "Page not found",
message: "The page you're looking for doesn't exist or has moved.",
},
413: { title: "Too large", message: "The request was larger than the server allows." },
429: {
title: "Slow down",
message: "You've made too many requests. Please wait a moment and try again.",
},
500: {
title: "Something went wrong",
message: "The server hit an unexpected error. Please try again in a moment.",
},
502: {
title: "Bad gateway",
message: "We couldn't reach an upstream service. Please try again shortly.",
},
503: {
title: "Temporarily unavailable",
message: "The service is down for a moment. Please try again shortly.",
},
};
/** A beautiful, self-contained HTML page for any 4xx/5xx status. */
export function renderStatusPage(status: number): Response {
const copy = STATUS_COPY[status] ?? {
title: status >= 500 ? "Something went wrong" : "Something's not right",
message: "An unexpected response was returned. Please try again.",
};
return new Response(
errorDocument({ status, code: String(status), title: copy.title, message: copy.message }),
{ status, headers: { "content-type": "text/html; charset=utf-8" } },
);
}
/** Readable, styled development error page — includes the stack trace. */
export function renderDevError(err: unknown, status = 500): Response {
const error = err instanceof Error ? err : new Error(String(err));
const name = error.name || "Error";
const message = error.message || "Unknown error";
return new Response(
errorDocument({
status,
code: String(status),
eyebrow: "DEVELOPMENT ERROR",
title: name,
message,
detail: { heading: `${name}: ${message}`, body: error.stack || "(no stack available)" },
}),
{ status, headers: { "content-type": "text/html; charset=utf-8" } },
);
}
/** Generic production error page — no stack, no file paths. */
export function renderProdError(status = 500): Response {
return renderStatusPage(status);
}
/** Pick the right error page for the current mode. */
export function renderError(err: unknown, mode: Mode): Response {
return mode === "development" ? renderDevError(err) : renderProdError();
}
/** Beautiful 404 page. */
export function renderNotFound(): Response {
return renderStatusPage(404);
}