Skip to content

Commit b8df37e

Browse files
committed
Add error pages.
1 parent c89b9dd commit b8df37e

40 files changed

Lines changed: 1043 additions & 85 deletions

File tree

‎frontend/README.md‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,26 @@ Imprint and Privacy Policy content lives in Git-ignored MDX files. Missing files
3333
are created automatically with placeholders; fill them before publishing.
3434
See [Private legal content](legal-content.md) for paths and setup.
3535

36+
## Error pages
37+
38+
Preview the missing-page design at `/error/404/` and the general-error design at
39+
`/error/`. Their MDX files and compact shared header/footer live in
40+
`purestack.studio/error/`; `components/recovery/` supplies their shared content and
41+
two decorative SVG illustrations. Layout and presentation use native `Flex`,
42+
`Grid`, buttons, icon frames, and generated utilities. `styles/recovery.ts` only
43+
adds viewport layout, a content width limit, and fluid heading sizing.
44+
45+
Both pages use `index: false`, which keeps them out of navigation, Pagefind, and
46+
the sitemap, and emits `noindex`. The general-error page enables a retry button through
47+
`modules/recovery.ts`; it reloads the current URL, including when a host serves
48+
the page for a different failing URL.
49+
50+
The development server automatically serves `error/404/index.html` for missing
51+
pages and falls back to `error/index.html` for other errors without a specific
52+
status page. It preserves the requested URL and HTTP error status, with
53+
`Cache-Control: no-store`. Direct preview requests return 200. Configure the
54+
production static host to use the same error-page convention.
55+
3656
## Source map
3757

3858
| Location | Responsibility |

‎frontend/components/index.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import { defineFeatureShowcaseComponent } from './landing/feature-showcase'
66
import { definePackageLinkComponent } from './landing/package-link'
77
import { defineRegorRelationshipComponent } from './landing/regor-relationship'
88
import { defineSourceWorkbenchComponent } from './landing/source-workbench'
9+
import { defineInterruptedConnectionComponent } from './recovery/interrupted-connection'
10+
import { defineMissingDestinationComponent } from './recovery/missing-destination'
11+
import { defineRecoveryPageComponent } from './recovery/recovery-page'
912
import { defineCopyButtonComponent } from './shared/copy-button'
1013
import { defineBrandComponent } from './shell/brand'
1114
import { definePalettePickerComponent } from './shell/palette-picker'
@@ -23,5 +26,8 @@ export function defineFrontendComponents() {
2326
forgeOverview: defineForgeOverviewComponent(),
2427
activityChartPreview: defineActivityChartPreviewComponent(),
2528
sourceWorkbench: defineSourceWorkbenchComponent(),
29+
recoveryPage: defineRecoveryPageComponent(),
30+
missingDestination: defineMissingDestinationComponent(),
31+
interruptedConnection: defineInterruptedConnectionComponent(),
2632
}
2733
}
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import { defineComponent, html } from 'regor'
2+
3+
export interface InterruptedConnection {}
4+
5+
const template = html`<svg
6+
viewBox="0 0 480 350"
7+
class="w-full d-block tone--accent tone-text"
8+
aria-hidden="true"
9+
focusable="false"
10+
xmlns="http://www.w3.org/2000/svg"
11+
>
12+
<defs>
13+
<radialGradient id="interrupted-connection-glow">
14+
<stop stop-color="currentColor" stop-opacity="0.14"/>
15+
<stop offset="1" stop-color="currentColor" stop-opacity="0"/>
16+
</radialGradient>
17+
<filter
18+
id="interrupted-connection-shadow"
19+
x="-50%"
20+
y="-30%"
21+
width="200%"
22+
height="180%"
23+
>
24+
<feDropShadow
25+
dx="0"
26+
dy="12"
27+
stdDeviation="10"
28+
flood-color="currentColor"
29+
flood-opacity="0.1"/>
30+
</filter>
31+
</defs>
32+
<ellipse
33+
cx="240"
34+
cy="175"
35+
rx="190"
36+
ry="158"
37+
fill="url(#interrupted-connection-glow)"/>
38+
<g fill="none" stroke="currentColor">
39+
<ellipse
40+
cx="240"
41+
cy="177"
42+
rx="185"
43+
ry="119"
44+
transform="rotate(-18 240 177)"
45+
stroke-opacity="0.12"
46+
stroke-dasharray="3 8"/>
47+
<path
48+
d="M42 292H70Q94 292 94 268V248"
49+
stroke-opacity="0.1"
50+
stroke-width="2"/>
51+
<path
52+
d="M34 270H56Q78 270 78 248V188Q78 166 104 166"
53+
stroke-opacity="0.45"
54+
stroke-width="7"
55+
stroke-linecap="round"/>
56+
<path
57+
d="M376 192H396Q422 192 422 218V270"
58+
stroke-opacity="0.45"
59+
stroke-width="7"
60+
stroke-linecap="round"/>
61+
<path
62+
d="M422 285V295"
63+
stroke-opacity="0.2"
64+
stroke-width="7"
65+
stroke-linecap="round"/>
66+
</g>
67+
<g filter="url(#interrupted-connection-shadow)">
68+
<rect
69+
x="104"
70+
y="102"
71+
width="92"
72+
height="128"
73+
rx="20"
74+
fill="var(--ps-semantic-tone-neutral-canvas)"
75+
stroke="currentColor"
76+
stroke-opacity="0.35"
77+
stroke-width="1.5"/>
78+
<rect
79+
x="148"
80+
y="114"
81+
width="36"
82+
height="104"
83+
rx="11"
84+
fill="currentColor"
85+
opacity="0.08"/>
86+
<path
87+
d="M196 146H220M196 172H220"
88+
stroke="currentColor"
89+
stroke-opacity="0.8"
90+
stroke-width="8"
91+
stroke-linecap="round"/>
92+
<path
93+
d="M116 192V124Q116 114 126 114"
94+
fill="none"
95+
stroke="currentColor"
96+
stroke-opacity="0.12"
97+
stroke-width="2"
98+
stroke-linecap="round"/>
99+
<circle cx="125" cy="208" r="3" fill="currentColor" opacity="0.6"/>
100+
<rect
101+
x="284"
102+
y="128"
103+
width="92"
104+
height="128"
105+
rx="20"
106+
fill="var(--ps-semantic-tone-neutral-canvas)"
107+
stroke="currentColor"
108+
stroke-opacity="0.35"
109+
stroke-width="1.5"/>
110+
<rect
111+
x="296"
112+
y="140"
113+
width="36"
114+
height="104"
115+
rx="11"
116+
fill="currentColor"
117+
opacity="0.08"/>
118+
<path
119+
d="M260 170H284M260 196H284"
120+
stroke="currentColor"
121+
stroke-opacity="0.8"
122+
stroke-width="8"
123+
stroke-linecap="round"/>
124+
<path
125+
d="M364 218V150Q364 140 354 140"
126+
fill="none"
127+
stroke="currentColor"
128+
stroke-opacity="0.12"
129+
stroke-width="2"
130+
stroke-linecap="round"/>
131+
<circle cx="355" cy="234" r="3" fill="currentColor" opacity="0.3"/>
132+
</g>
133+
<g
134+
fill="none"
135+
stroke="currentColor"
136+
stroke-width="2.5"
137+
stroke-linecap="round"
138+
opacity="0.7"
139+
>
140+
<path d="M236 141L247 130M242 154H254M232 165L241 175"/>
141+
</g>
142+
<g transform="translate(251 71)">
143+
<circle
144+
r="35"
145+
fill="none"
146+
stroke="currentColor"
147+
stroke-opacity="0.12"
148+
stroke-dasharray="2 6"/>
149+
<circle
150+
r="23"
151+
fill="var(--ps-semantic-tone-neutral-canvas)"
152+
stroke="currentColor"
153+
stroke-opacity="0.35"/>
154+
<path
155+
d="M0-9V2"
156+
stroke="currentColor"
157+
stroke-width="3"
158+
stroke-linecap="round"/>
159+
<circle cy="9" r="1.8" fill="currentColor"/>
160+
</g>
161+
<g fill="currentColor">
162+
<circle cx="160" cy="284" r="3" opacity="0.2"/>
163+
<circle cx="177" cy="284" r="3" opacity="0.35"/>
164+
<circle cx="194" cy="284" r="3" opacity="0.6"/>
165+
</g>
166+
<path
167+
d="M365 78H377M371 72V84"
168+
stroke="currentColor"
169+
stroke-opacity="0.3"
170+
stroke-width="1.5"
171+
stroke-linecap="round"/>
172+
</svg>`
173+
174+
export function defineInterruptedConnectionComponent() {
175+
return defineComponent<InterruptedConnection>(template)
176+
}
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import { defineComponent, html } from 'regor'
2+
3+
export interface MissingDestination {}
4+
5+
const template = html`<svg
6+
viewBox="0 0 480 350"
7+
class="w-full d-block tone--accent tone-text"
8+
aria-hidden="true"
9+
focusable="false"
10+
xmlns="http://www.w3.org/2000/svg"
11+
>
12+
<defs>
13+
<radialGradient id="missing-destination-glow">
14+
<stop stop-color="currentColor" stop-opacity="0.12"/>
15+
<stop offset="1" stop-color="currentColor" stop-opacity="0"/>
16+
</radialGradient>
17+
<pattern
18+
id="missing-destination-grid"
19+
width="20"
20+
height="20"
21+
patternUnits="userSpaceOnUse"
22+
>
23+
<circle cx="1" cy="1" r="0.8" fill="currentColor" opacity="0.2"/>
24+
</pattern>
25+
<filter
26+
id="missing-destination-shadow"
27+
x="-30%"
28+
y="-30%"
29+
width="160%"
30+
height="180%"
31+
>
32+
<feDropShadow
33+
dx="0"
34+
dy="12"
35+
stdDeviation="12"
36+
flood-color="currentColor"
37+
flood-opacity="0.08"/>
38+
</filter>
39+
</defs>
40+
<ellipse
41+
cx="290"
42+
cy="170"
43+
rx="185"
44+
ry="155"
45+
fill="url(#missing-destination-glow)"/>
46+
<rect
47+
x="20"
48+
y="30"
49+
width="440"
50+
height="280"
51+
rx="32"
52+
fill="url(#missing-destination-grid)"/>
53+
<ellipse
54+
cx="295"
55+
cy="173"
56+
rx="162"
57+
ry="115"
58+
transform="rotate(-18 295 173)"
59+
fill="none"
60+
stroke="currentColor"
61+
stroke-opacity="0.12"
62+
stroke-dasharray="3 8"/>
63+
<path
64+
d="M34 280H94Q158 280 158 216V202"
65+
fill="none"
66+
stroke="currentColor"
67+
stroke-opacity="0.12"
68+
stroke-width="2"/>
69+
<path
70+
d="M44 246H98Q122 246 122 222V166Q122 142 146 142H194"
71+
fill="none"
72+
stroke="currentColor"
73+
stroke-opacity="0.7"
74+
stroke-width="3"
75+
stroke-linecap="round"/>
76+
<path
77+
d="M204 142H226"
78+
fill="none"
79+
stroke="currentColor"
80+
stroke-opacity="0.5"
81+
stroke-width="3"
82+
stroke-linecap="round"
83+
stroke-dasharray="2 8"/>
84+
<circle
85+
cx="44"
86+
cy="246"
87+
r="9"
88+
fill="var(--ps-semantic-tone-neutral-canvas)"
89+
stroke="currentColor"
90+
stroke-width="2"/>
91+
<circle cx="44" cy="246" r="3" fill="currentColor"/>
92+
<circle
93+
cx="122"
94+
cy="194"
95+
r="5"
96+
fill="var(--ps-semantic-tone-neutral-canvas)"
97+
stroke="currentColor"
98+
stroke-width="2"/>
99+
<circle cx="194" cy="142" r="4" fill="currentColor"/>
100+
<g filter="url(#missing-destination-shadow)">
101+
<rect
102+
x="224"
103+
y="64"
104+
width="210"
105+
height="182"
106+
rx="18"
107+
fill="var(--ps-semantic-tone-neutral-canvas)"
108+
stroke="currentColor"
109+
stroke-opacity="0.3"/>
110+
<path d="M224 102H434" stroke="currentColor" stroke-opacity="0.16"/>
111+
<g fill="currentColor" opacity="0.35">
112+
<circle cx="244" cy="84" r="3"/>
113+
<circle cx="256" cy="84" r="3"/>
114+
<circle cx="268" cy="84" r="3"/>
115+
</g>
116+
<path
117+
d="M294 84H350"
118+
stroke="currentColor"
119+
stroke-opacity="0.12"
120+
stroke-width="4"
121+
stroke-linecap="round"/>
122+
<text
123+
x="329"
124+
y="181"
125+
text-anchor="middle"
126+
fill="currentColor"
127+
font-family="system-ui, sans-serif"
128+
font-size="68"
129+
font-weight="650"
130+
letter-spacing="-4"
131+
>
132+
404
133+
</text>
134+
<path
135+
d="M291 208H367"
136+
stroke="currentColor"
137+
stroke-opacity="0.18"
138+
stroke-width="3"
139+
stroke-linecap="round"/>
140+
<path
141+
d="M310 220H348"
142+
stroke="currentColor"
143+
stroke-opacity="0.1"
144+
stroke-width="3"
145+
stroke-linecap="round"/>
146+
</g>
147+
<g transform="translate(393 289)" stroke="currentColor">
148+
<circle
149+
r="18"
150+
fill="var(--ps-semantic-tone-neutral-canvas)"
151+
stroke-opacity="0.2"/>
152+
<path d="M0-25V-21M0 21V25M-25 0H-21M21 0H25" stroke-opacity="0.3"/>
153+
<path
154+
d="M-6 6L-2-2L6-6L2 2Z"
155+
fill="currentColor"
156+
fill-opacity="0.3"
157+
stroke-width="1.5"
158+
stroke-linejoin="round"/>
159+
</g>
160+
<path
161+
d="M76 74H88M82 68V80"
162+
stroke="currentColor"
163+
stroke-opacity="0.4"
164+
stroke-width="1.5"
165+
stroke-linecap="round"/>
166+
<circle cx="178" cy="286" r="3" fill="currentColor" opacity="0.3"/>
167+
</svg>`
168+
169+
export function defineMissingDestinationComponent() {
170+
return defineComponent<MissingDestination>(template)
171+
}

0 commit comments

Comments
 (0)