Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions packages/void/src/assets.d.ts
Original file line number Diff line number Diff line change
@@ -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;
}
4 changes: 0 additions & 4 deletions packages/void/src/css.d.ts

This file was deleted.

50 changes: 50 additions & 0 deletions packages/void/src/runtime/error-pages/404.html

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One HTML file for each of 404/500 so that each can be opened to preview directly. It's duplicated markup but relatively manageable.

Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>404: Not Found</title>
<!-- inject:css -->
<!--
Do not add code here. At runtime, the file referenced by the tag below is
inlined, replacing this entire block (including this comment).
The tag is kept so you can preview this HTML file by opening it directly in a browser.
-->
<link rel="stylesheet" href="./style.css" />
<!-- endinject -->
<!-- inject:js -->
<!--
Do not add code here. At runtime, the file referenced by the tag below is
inlined, replacing this entire block (including this comment).
The tag is kept so you can preview this HTML file by opening it directly in a browser.
-->
<script src="./script.js"></script>
<!-- endinject -->
</head>
<body>
<div class="error-page">
<div class="grid-cell" aria-hidden="true"></div>
<div class="grid-cell" aria-hidden="true"></div>
<div class="grid-cell" aria-hidden="true"></div>
<div class="grid-cell" aria-hidden="true"></div>
<main class="error-content">
<span class="grid-tick top left" aria-hidden="true"></span>
<span class="grid-tick top right" aria-hidden="true"></span>
<span class="grid-tick bottom left" aria-hidden="true"></span>
<span class="grid-tick bottom right" aria-hidden="true"></span>
<h1>
<span class="error-code">404</span><span class="error-title">Lost in the void</span>
</h1>
<p class="error-description">This page doesn't exist</p>
<button class="error-action" type="button" id="void-error-back" hidden>
<span class="error-arrow" aria-hidden="true">←</span>Go back
</button>
</main>
<div class="grid-cell" aria-hidden="true"></div>
<div class="grid-cell" aria-hidden="true"></div>
<div class="grid-cell" aria-hidden="true"></div>
<div class="grid-cell" aria-hidden="true"></div>
<canvas class="error-texture" aria-hidden="true"></canvas>
</div>
</body>
</html>
50 changes: 50 additions & 0 deletions packages/void/src/runtime/error-pages/500.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>500: Internal Server Error</title>
<!-- inject:css -->
<!--
Do not add code here. At runtime, the file referenced by the tag below is
inlined, replacing this entire block (including this comment).
The tag is kept so you can preview this HTML file by opening it directly in a browser.
-->
<link rel="stylesheet" href="./style.css" />
<!-- endinject -->
<!-- inject:js -->
<!--
Do not add code here. At runtime, the file referenced by the tag below is
inlined, replacing this entire block (including this comment).
The tag is kept so you can preview this HTML file by opening it directly in a browser.
-->
<script src="./script.js"></script>
<!-- endinject -->
</head>
<body>
<div class="error-page">
<div class="grid-cell" aria-hidden="true"></div>
<div class="grid-cell" aria-hidden="true"></div>
<div class="grid-cell" aria-hidden="true"></div>
<div class="grid-cell" aria-hidden="true"></div>
<main class="error-content">
<span class="grid-tick top left" aria-hidden="true"></span>
<span class="grid-tick top right" aria-hidden="true"></span>
<span class="grid-tick bottom left" aria-hidden="true"></span>
<span class="grid-tick bottom right" aria-hidden="true"></span>
<h1>
<span class="error-code">500</span><span class="error-title">Something went wrong</span>
</h1>
<p class="error-description">Please try again in a moment</p>
<a class="error-action" href=""
><span class="error-arrow" aria-hidden="true">↻</span>Reload page</a
>
</main>
<div class="grid-cell" aria-hidden="true"></div>
<div class="grid-cell" aria-hidden="true"></div>
<div class="grid-cell" aria-hidden="true"></div>
<div class="grid-cell" aria-hidden="true"></div>
<canvas class="error-texture" aria-hidden="true"></canvas>
</div>
</body>
</html>
14 changes: 14 additions & 0 deletions packages/void/src/runtime/error-pages/index.ts
Original file line number Diff line number Diff line change
@@ -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];
}
24 changes: 24 additions & 0 deletions packages/void/src/runtime/error-pages/inline.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const closing = '<!-- endinject -->';

function replaceAssetBlock(html: string, name: 'css' | 'js', content: string): string {
const opening = `<!-- inject:${name} -->`;
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', `<style>\n${stylesheet}</style>`],
['js', `<script>\n${script}</script>`],
] as const) {
html = replaceAssetBlock(html, name, content);
}
return html;
}
79 changes: 79 additions & 0 deletions packages/void/src/runtime/error-pages/script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
(() => {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Script contains all the variants instead of 3 different scripts since they share a lot of stuff and the combined version is also quite small.

const designs = ['signal', 'grid', 'fold'];
document.documentElement.dataset.design = designs[Math.floor(Math.random() * designs.length)];

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The design is decided at runtime. It's done in <head>, so there's no FOUC or loading states of any sort. Two reasons:

  • No need to worry about SEO as well because search engines don't index 404 and 500 responses. If one day we decide that we want the final HTML on the server, it's an easy fix.
  • The SSG build can use the same 404.html and also achieve a random result. If we pick on the server, the SSG build can only pick one of the options (I mean it's still possible for SSG 404 page to be random, but it's gonna be more troublesome).


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();
}
})();
Loading