diff --git a/docs/404.md b/docs/404.md new file mode 100644 index 000000000..20948d579 --- /dev/null +++ b/docs/404.md @@ -0,0 +1,73 @@ + + +# Page not found + +Sorry, but we can’t find the page you requested. + +Here’s something inspired by [Bridget Riley](https://en.wikipedia.org/wiki/Bridget_Riley) instead. + +```js +import * as d3 from "npm:d3"; + +const height = Math.min(640, width); +const point = (cx, cy, r, a) => [cx + r * Math.cos(a), cy + r * Math.sin(a)]; +const circles = []; +const random = d3.randomLcg(42); +const n = 80; +let a = 0.2; +let x = width / 2; +let y = height / 2; +let r = Math.hypot(width, height) / 2; +let dr = r / 6.5; + +while (r > 0) { + circles.push({x, y, r, a}); + const t = random() * 2 * Math.PI; + const s = Math.sqrt((random() * dr * dr) / 4); + x += Math.cos(t) * s; + y += Math.sin(t) * s; + r -= dr; + a = -a; +} + +const canvas = display(document.createElement("canvas")); +canvas.width = width * devicePixelRatio; +canvas.height = height * devicePixelRatio; +canvas.style.width = `${width}px`; + +const context = canvas.getContext("2d"); +context.scale(devicePixelRatio, devicePixelRatio); + +(function frame(elapsed) { + context.save(); + context.clearRect(0, 0, width, height); + context.translate(width / 2, height / 2); + context.rotate(Math.sin(elapsed / 50000)); + context.translate(-width / 2, -height / 2); + context.beginPath(); + for (let i = 0; i < n; ++i) { + let move = true; + d3.pairs(circles, ({x: x1, y: y1, r: r1, a: a1}, {x: x2, y: y2, r: r2, a: a2}) => { + const ai = ((i * 2) / n) * Math.PI; + context[move ? ((move = false), "moveTo") : "lineTo"](...point(x1, y1, r1, a1 + ai)); + context.lineTo(...point(x2, y2, r2, a2 + ai)); + }); + d3.pairs(circles.slice().reverse(), ({x: x1, y: y1, r: r1, a: a1}, {x: x2, y: y2, r: r2, a: a2}) => { + const ai = ((i * 2 + 1) / n) * Math.PI; + context.lineTo(...point(x1, y1, r1, a1 + ai)); + context.lineTo(...point(x2, y2, r2, a2 + ai)); + }); + context.closePath(); + } + context.fillStyle = getComputedStyle(canvas).getPropertyValue("color"); + context.fill(); + context.restore(); + if (canvas.isConnected) requestAnimationFrame(frame); +})(); +``` diff --git a/src/navigation.ts b/src/navigation.ts index 39fd270a2..5ade6c3f0 100644 --- a/src/navigation.ts +++ b/src/navigation.ts @@ -11,7 +11,7 @@ export async function readPages(root: string): Promise { console.log(req.method, req.url); + let pages; try { const url = new URL(req.url!, "http://localhost"); let {pathname} = url; @@ -124,7 +125,7 @@ class Server { // Otherwise, serve the corresponding Markdown file, if it exists. // Anything else should 404; static files should be matched above. try { - const pages = await readPages(this.root); // TODO cache? watcher? + pages = await readPages(this.root); // TODO cache? watcher? const {html} = await renderPreview(await readFile(path + ".md", "utf-8"), { root: this.root, path: pathname, @@ -141,6 +142,20 @@ class Server { } catch (error) { console.error(error); res.statusCode = isHttpError(error) ? error.statusCode : 500; + if (req.method === "GET" && res.statusCode === 404) { + try { + const {html} = await renderPreview(await readFile(join(this.root, "404.md"), "utf-8"), { + root: this.root, + path: "/404", + pages, + resolver: this._resolver! + }); + end(req, res, html, "text/html"); + return; + } catch { + // ignore secondary error (e.g., no 404.md); show the original 404 + } + } res.setHeader("Content-Type", "text/plain; charset=utf-8"); res.end(error instanceof Error ? error.message : "Oops, an error occurred"); } diff --git a/src/render.ts b/src/render.ts index eb6f26bb4..ad09fa625 100644 --- a/src/render.ts +++ b/src/render.ts @@ -55,7 +55,7 @@ function render( {path, pages, title, preview, hash, resolver}: RenderOptions & RenderInternalOptions ): string { return ` - +${path === "/404" ? `\n` : ""} ${ parseResult.title || title