diff --git a/packages/void/src/assets.d.ts b/packages/void/src/assets.d.ts
new file mode 100644
index 000000000..a2cb5c3a0
--- /dev/null
+++ b/packages/void/src/assets.d.ts
@@ -0,0 +1,14 @@
+declare module '*.css?raw' {
+ const content: string;
+ export default content;
+}
+
+declare module '*.html?raw' {
+ const content: string;
+ export default content;
+}
+
+declare module '*.js?raw' {
+ const content: string;
+ export default content;
+}
diff --git a/packages/void/src/css.d.ts b/packages/void/src/css.d.ts
deleted file mode 100644
index 4f8dea640..000000000
--- a/packages/void/src/css.d.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-declare module '*.css?raw' {
- const content: string;
- export default content;
-}
diff --git a/packages/void/src/runtime/error-pages/404.html b/packages/void/src/runtime/error-pages/404.html
new file mode 100644
index 000000000..835a02779
--- /dev/null
+++ b/packages/void/src/runtime/error-pages/404.html
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 500Something went wrong
+
+ Please try again in a moment
+ ↻Reload page
+
+
+
+
+
+
+
+
+
diff --git a/packages/void/src/runtime/error-pages/index.ts b/packages/void/src/runtime/error-pages/index.ts
new file mode 100644
index 000000000..0765607cd
--- /dev/null
+++ b/packages/void/src/runtime/error-pages/index.ts
@@ -0,0 +1,14 @@
+import notFoundPage from './404.html?raw';
+import serverErrorPage from './500.html?raw';
+import { inlineErrorPageAssets } from './inline.ts';
+import script from './script.js?raw';
+import stylesheet from './style.css?raw';
+
+const pages = {
+ 404: inlineErrorPageAssets(notFoundPage, stylesheet, script),
+ 500: inlineErrorPageAssets(serverErrorPage, stylesheet, script),
+};
+
+export function renderDefaultErrorPage(status: 404 | 500): string {
+ return pages[status];
+}
diff --git a/packages/void/src/runtime/error-pages/inline.ts b/packages/void/src/runtime/error-pages/inline.ts
new file mode 100644
index 000000000..92df8fb27
--- /dev/null
+++ b/packages/void/src/runtime/error-pages/inline.ts
@@ -0,0 +1,24 @@
+const closing = '';
+
+function replaceAssetBlock(html: string, name: 'css' | 'js', content: string): string {
+ const opening = ``;
+ const start = html.indexOf(opening);
+ const end = html.indexOf(closing, start + opening.length);
+ if (start === -1 || end === -1 || html.lastIndexOf(opening) !== start) {
+ throw new Error(`Expected one complete ${name} block in the default error page.`);
+ }
+ return html.slice(0, start) + content + html.slice(end + closing.length);
+}
+
+export function inlineErrorPageAssets(html: string, stylesheet: string, script: string): string {
+ if (html.split(closing).length !== 3) {
+ throw new Error('Expected two endinject markers in the default error page.');
+ }
+ for (const [name, content] of [
+ ['css', ``],
+ ['js', ``],
+ ] as const) {
+ html = replaceAssetBlock(html, name, content);
+ }
+ return html;
+}
diff --git a/packages/void/src/runtime/error-pages/script.js b/packages/void/src/runtime/error-pages/script.js
new file mode 100644
index 000000000..6d1a39fc3
--- /dev/null
+++ b/packages/void/src/runtime/error-pages/script.js
@@ -0,0 +1,79 @@
+(() => {
+ const designs = ['signal', 'grid', 'fold'];
+ document.documentElement.dataset.design = designs[Math.floor(Math.random() * designs.length)];
+
+ function setup() {
+ const back = document.getElementById('void-error-back');
+ const canGoBack = window.navigation ? window.navigation.canGoBack : window.history.length > 1;
+ if (back && canGoBack) {
+ back.hidden = false;
+ back.addEventListener('click', () => window.history.back());
+ }
+
+ if (document.documentElement.dataset.design !== 'signal') {
+ return;
+ }
+ const canvas = document.querySelector('.error-texture');
+ const ctx = canvas.getContext('2d');
+ if (!ctx) {
+ return;
+ }
+
+ function draw() {
+ const { width, height } = canvas.getBoundingClientRect();
+ const ratio = Math.min(window.devicePixelRatio || 1, 2);
+ canvas.width = Math.round(width * ratio);
+ canvas.height = Math.round(height * ratio);
+ ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
+ const colors = getComputedStyle(canvas);
+ const chars = ['·', '/', '0', '1', '+', '[', ']', '—'];
+ ctx.font = '8px ui-monospace, monospace';
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'middle';
+ ctx.fillStyle = colors.color;
+
+ for (let y = 8, row = 0; y < height; y += 12, row++) {
+ for (let x = 8, col = 0; x < width; x += 9, col++) {
+ const distance = Math.hypot(
+ (x - width / 2) / (width * 0.47),
+ (y - height * 0.48) / (height * 0.55),
+ );
+ const seed = (Math.imul(row + 11, 61) ^ Math.imul(col + 7, 137)) >>> 0;
+ const core = Math.max(0, Math.min(1, (distance - 0.28) / 0.55));
+ const opacity = core * (0.15 + (seed % 7) * 0.025) * 0.75;
+ if (seed % 17 === 0 || opacity < 0.015) {
+ continue;
+ }
+ ctx.globalAlpha = opacity;
+ ctx.fillText(chars[(seed >>> 3) % chars.length], x, y);
+ }
+ }
+
+ ctx.globalAlpha = 1;
+ ctx.strokeStyle = colors.borderTopColor;
+ for (const [x, y] of [
+ [28, 28],
+ [width - 28, 28],
+ [28, height - 28],
+ [width - 28, height - 28],
+ ]) {
+ ctx.beginPath();
+ ctx.moveTo(x - 4, y);
+ ctx.lineTo(x + 4, y);
+ ctx.moveTo(x, y - 4);
+ ctx.lineTo(x, y + 4);
+ ctx.stroke();
+ }
+ }
+
+ new ResizeObserver(draw).observe(canvas);
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', draw);
+ draw();
+ }
+
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', setup, { once: true });
+ } else {
+ setup();
+ }
+})();
diff --git a/packages/void/src/runtime/error-pages/style.css b/packages/void/src/runtime/error-pages/style.css
new file mode 100644
index 000000000..2602ba791
--- /dev/null
+++ b/packages/void/src/runtime/error-pages/style.css
@@ -0,0 +1,293 @@
+:root {
+ color-scheme: light dark;
+ --background: light-dark(#fff, #16171d);
+ --foreground: light-dark(#16171d, #f6f5f8);
+ --muted: light-dark(#6e6874, #a8a2ae);
+ --line: light-dark(#e5e4e7, #36323c);
+ --ink: light-dark(#4e4856, #c3bdcd);
+ --corner: light-dark(#e5e4e7, #3b3440);
+ background: var(--background);
+ color: var(--foreground);
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
+ -webkit-font-smoothing: antialiased;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+}
+
+.error-page {
+ position: relative;
+ isolation: isolate;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-block-size: 100svh;
+ overflow: hidden;
+}
+
+.error-content {
+ min-inline-size: 0;
+ padding: 32px 24px;
+ text-align: center;
+}
+
+.error-content h1 {
+ display: flex;
+ flex-direction: column;
+ gap: 18px;
+ margin: 0;
+ font-weight: 500;
+}
+
+.error-code {
+ font-size: 64px;
+ line-height: 1;
+ letter-spacing: -0.045em;
+}
+
+.error-title {
+ font-size: 15px;
+ line-height: 1.4;
+ letter-spacing: -0.02em;
+ text-wrap: balance;
+}
+
+.error-description {
+ margin: 6px 0 0;
+ color: var(--muted);
+ font-size: 12px;
+ line-height: 1.5;
+ letter-spacing: -0.01em;
+}
+
+.error-action {
+ position: relative;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 7px;
+ min-block-size: 32px;
+ margin-block-start: 20px;
+ padding: 6px 12px;
+ border: 1px solid var(--line);
+ border-radius: 6px;
+ background: var(--background);
+ color: var(--foreground);
+ font:
+ 500 12px/18px system-ui,
+ -apple-system,
+ BlinkMacSystemFont,
+ 'Segoe UI',
+ sans-serif;
+ letter-spacing: -0.01em;
+ text-decoration: none;
+ white-space: nowrap;
+ cursor: pointer;
+}
+
+.error-action::after {
+ position: absolute;
+ inset: -6px -3px;
+ content: '';
+}
+
+.error-action:hover {
+ border-color: var(--foreground);
+ background: var(--foreground);
+ color: var(--background);
+}
+
+.error-action:focus-visible {
+ outline: 2px solid var(--foreground);
+ outline-offset: 4px;
+}
+
+.error-action[hidden] {
+ visibility: hidden;
+}
+
+.error-arrow {
+ font-size: 15px;
+ line-height: 14px;
+}
+
+.error-texture {
+ position: absolute;
+ z-index: -1;
+ inset: 0;
+ display: block;
+ inline-size: 100%;
+ block-size: 100%;
+ border-color: var(--line);
+ color: var(--ink);
+}
+
+.grid-cell,
+.grid-tick {
+ display: none;
+}
+
+.grid-tick {
+ position: absolute;
+ z-index: 1;
+ inline-size: 0;
+ block-size: 0;
+ border: 5px solid transparent;
+ pointer-events: none;
+}
+
+:root[data-design='grid'] .error-page {
+ display: grid;
+ grid-template-columns: minmax(24px, 1fr) minmax(0, 456px) minmax(24px, 1fr);
+ grid-template-rows: minmax(24px, 1fr) minmax(272px, auto) minmax(24px, 1fr);
+ align-items: stretch;
+}
+
+:root[data-design='grid'] .grid-cell,
+:root[data-design='grid'] .error-content {
+ min-inline-size: 0;
+ border-inline-end: 1px solid var(--line);
+ border-block-end: 1px solid var(--line);
+}
+
+:root[data-design='grid'] .grid-cell {
+ display: block;
+}
+
+:root[data-design='grid'] .grid-cell:nth-child(3n) {
+ border-inline-end: 0;
+}
+
+:root[data-design='grid'] .grid-cell:nth-child(n + 7) {
+ border-block-end: 0;
+}
+
+:root[data-design='grid'] .error-content {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 32px;
+}
+
+:root[data-design='grid'] .grid-tick {
+ display: block;
+}
+
+.grid-tick.left {
+ left: -1px;
+ border-left-color: var(--corner);
+}
+
+.grid-tick.right {
+ right: 0;
+ border-right-color: var(--corner);
+}
+
+.grid-tick.top {
+ top: -5px;
+}
+
+.grid-tick.bottom {
+ bottom: -5px;
+}
+
+:root[data-design='grid'] .error-texture,
+:root[data-design='fold'] .error-texture {
+ display: none;
+}
+
+:root[data-design='fold'] .error-page {
+ padding: 40px 42px;
+}
+
+:root[data-design='fold'] .error-content {
+ flex: 0 1 560px;
+ margin-inline-end: auto;
+ padding: 0;
+ text-align: start;
+}
+
+:root[data-design='fold'] h1 {
+ flex-direction: row;
+ align-items: baseline;
+ gap: 14px;
+}
+
+:root[data-design='fold'] .error-code,
+:root[data-design='fold'] .error-title {
+ font-size: 26px;
+ line-height: 1.2;
+ letter-spacing: -0.025em;
+}
+
+:root[data-design='fold'] .error-code {
+ flex: none;
+ color: var(--muted);
+ font-family: ui-monospace, 'SFMono-Regular', Consolas, monospace;
+ font-weight: 400;
+ letter-spacing: -0.045em;
+}
+
+:root[data-design='fold'] .error-description {
+ margin-block-start: 12px;
+ font-size: 14px;
+ line-height: 1.6;
+ text-wrap: pretty;
+}
+
+:root[data-design='fold'] .error-action {
+ margin-block-start: 24px;
+}
+
+@supports (corner-shape: squircle) {
+ .error-action {
+ border-radius: 12px;
+ corner-shape: squircle;
+ }
+}
+
+@media (pointer: coarse) {
+ .error-action {
+ min-block-size: 40px;
+ }
+}
+
+@media (max-width: 620px) {
+ :root[data-design='fold'] .error-page {
+ padding: 36px 30px;
+ }
+
+ :root[data-design='fold'] .error-code,
+ :root[data-design='fold'] .error-title {
+ font-size: 24px;
+ }
+}
+
+@media (max-width: 440px) {
+ .error-code {
+ font-size: 56px;
+ }
+
+ :root[data-design='grid'] .error-page {
+ grid-template-columns: 24px minmax(0, 1fr) 24px;
+ }
+
+ :root[data-design='grid'] .error-content {
+ padding: 24px;
+ }
+
+ :root[data-design='fold'] .error-code {
+ display: none;
+ }
+
+ :root[data-design='fold'] .error-action {
+ margin-block-start: 20px;
+ }
+}
diff --git a/packages/void/src/runtime/routing-errors.ts b/packages/void/src/runtime/routing-errors.ts
index 060c41714..b7e66e967 100644
--- a/packages/void/src/runtime/routing-errors.ts
+++ b/packages/void/src/runtime/routing-errors.ts
@@ -1,4 +1,7 @@
import type { Context } from 'hono';
+import { renderDefaultErrorPage } from './error-pages/index.ts';
+
+type ErrorPageStatus = 404 | 500;
type StackFrame = {
functionName: string;
@@ -12,10 +15,15 @@ type StackFrame = {
type ErrorDetails = { name: string; message: string; stack: string; frames: Array