Repository navigation
feat(pages): refresh design of default 404 and 500 pages #607
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
dbf0fcb
1c8808a
a249afb
7364e19
2a959db
663c4fa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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; | ||
| } |
This file was deleted.
| 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> |
| 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> |
| 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]; | ||
| } |
| 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; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| (() => { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)]; | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The design is decided at runtime. It's done in
|
||
|
|
||
| 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(); | ||
| } | ||
| })(); | ||
There was a problem hiding this comment.
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.