fix: improve 404/500 error pages

- Remove 'clack' text from 404 gear (looked off)
- Add 2 random extension cards to 404 page
- Improve gear SVG: more teeth, outer dashed ring, bolt detail
- Smoother wobble animation on 404 (5s, wider arc)
- More sparks + smoother seizure animation on 500
- Wider max-width on 404 to fit extension cards
This commit is contained in:
Jose Juan Moñino
2026-07-10 19:28:26 +02:00
parent f2d5e74f19
commit ffabbdde14
2 changed files with 176 additions and 41 deletions
+147 -15
View File
@@ -1,15 +1,24 @@
--- ---
import Base from '../layouts/Base.astro'; import Base from '../layouts/Base.astro';
import { getCollection } from 'astro:content';
const gearSvg = `<svg width="160" height="160" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg"> const extensions = await getCollection('extensions');
// Pick 2 random extensions
const shuffled = [...extensions].sort(() => Math.random() - 0.5);
const randomExts = shuffled.slice(0, 2);
const gearSvg = `<svg width="180" height="180" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<g class="gear-spin-slow"> <g class="gear-spin-slow">
<path d="M50 10 L54 18 L62 16 L64 24 L72 24 L70 32 L78 36 L72 42 L78 48 L70 52 L72 60 L64 60 L62 68 L54 66 L50 74 L46 66 L38 68 L36 60 L28 60 L30 52 L22 48 L28 42 L22 36 L30 32 L28 24 L36 24 L38 16 L46 18 Z" <path d="M50 8 L55 17 L64 14 L66 23 L75 23 L72 32 L80 36 L73 42 L80 49 L71 53 L73 62 L64 61 L62 70 L53 67 L50 76 L47 67 L38 70 L36 61 L27 62 L29 53 L20 49 L27 42 L20 36 L28 32 L25 23 L34 23 L36 14 L45 17 Z"
fill="none" stroke="var(--accent)" stroke-width="2.5" stroke-linejoin="round" /> fill="none" stroke="var(--accent)" stroke-width="2" stroke-linejoin="round" />
<circle cx="50" cy="50" r="14" fill="none" stroke="var(--accent)" stroke-width="2.5" /> <circle cx="50" cy="50" r="15" fill="none" stroke="var(--accent)" stroke-width="2" />
<circle cx="50" cy="50" r="5" fill="var(--accent)" /> <circle cx="50" cy="50" r="4" fill="var(--accent)" />
</g> </g>
<rect x="44" y="4" width="12" height="10" fill="var(--bg)" /> <g class="gear-spin-rev">
<text x="50" y="14" text-anchor="middle" font-size="8" font-family="var(--font-mono)" fill="var(--text-muted)">clack</text> <circle cx="50" cy="50" r="26" fill="none" stroke="var(--border-bright)" stroke-width="1" stroke-dasharray="2 4" />
</g>
<circle cx="50" cy="76" r="2" fill="var(--accent)" class="gear-bolt" />
</svg>`; </svg>`;
--- ---
@@ -34,6 +43,24 @@ const gearSvg = `<svg width="160" height="160" viewBox="0 0 100 100" fill="none"
<div class="error-hint"> <div class="error-hint">
<code>try &#123; find('/page') &#125; catch (GearStrippedError) &#123; redirect('/') &#125;</code> <code>try &#123; find('/page') &#125; catch (GearStrippedError) &#123; redirect('/') &#125;</code>
</div> </div>
{randomExts.length > 0 && (
<div class="error-extras">
<p class="error-extras-label">While you're here, check these out</p>
<div class="ext-grid">
{randomExts.map((ext) => (
<article class="ext-card">
<span class="ext-tag">{ext.slug}</span>
<h3>{ext.data.name}</h3>
<p>{ext.data.tagline}</p>
<div class="links">
<a href={`/${ext.slug}`}>Learn more →</a>
</div>
</article>
))}
</div>
</div>
)}
</div> </div>
</section> </section>
</Base> </Base>
@@ -49,26 +76,45 @@ const gearSvg = `<svg width="160" height="160" viewBox="0 0 100 100" fill="none"
.error-content { .error-content {
text-align: center; text-align: center;
max-width: 520px; max-width: 640px;
} }
.error-gear { .error-gear {
margin-bottom: 28px; margin-bottom: 24px;
display: flex; display: flex;
justify-content: center; justify-content: center;
} }
.gear-spin-slow { .gear-spin-slow {
transform-origin: 50px 50px; transform-origin: 50px 50px;
animation: gear-wobble 4s ease-in-out infinite; animation: gear-wobble 5s ease-in-out infinite;
}
.gear-spin-rev {
transform-origin: 50px 50px;
animation: gear-wobble-rev 8s linear infinite;
}
.gear-bolt {
animation: bolt-pulse 2s ease-in-out infinite;
} }
@keyframes gear-wobble { @keyframes gear-wobble {
0%, 100% { transform: rotate(0deg); } 0%, 100% { transform: rotate(0deg); }
20% { transform: rotate(-12deg); } 20% { transform: rotate(-15deg); }
40% { transform: rotate(8deg); } 40% { transform: rotate(10deg); }
60% { transform: rotate(-5deg); } 60% { transform: rotate(-6deg); }
80% { transform: rotate(3deg); } 80% { transform: rotate(4deg); }
}
@keyframes gear-wobble-rev {
from { transform: rotate(0deg); }
to { transform: rotate(-360deg); }
}
@keyframes bolt-pulse {
0%, 100% { opacity: 0.3; }
50% { opacity: 1; }
} }
.error-code { .error-code {
@@ -102,7 +148,7 @@ const gearSvg = `<svg width="160" height="160" viewBox="0 0 100 100" fill="none"
gap: 10px; gap: 10px;
justify-content: center; justify-content: center;
flex-wrap: wrap; flex-wrap: wrap;
margin-bottom: 32px; margin-bottom: 28px;
} }
.error-hint { .error-hint {
@@ -117,6 +163,92 @@ const gearSvg = `<svg width="160" height="160" viewBox="0 0 100 100" fill="none"
overflow-x: auto; overflow-x: auto;
} }
.error-extras {
margin-top: 40px;
border-top: 1px solid var(--border);
padding-top: 32px;
}
.error-extras-label {
font-family: var(--font-mono);
font-size: 12px;
color: var(--accent);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 16px;
}
.ext-grid {
display: grid;
grid-template-columns: 1fr;
gap: 12px;
text-align: left;
}
@media (min-width: 520px) {
.ext-grid { grid-template-columns: repeat(2, 1fr); gap: 16px; }
}
.ext-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: 20px;
transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
}
.ext-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background: var(--accent);
transform: scaleX(0);
transform-origin: left;
transition: transform 300ms ease;
}
.ext-card:hover {
border-color: var(--border-bright);
background: var(--bg-elevated);
}
.ext-card:hover::before {
transform: scaleX(1);
}
.ext-tag {
display: inline-block;
font-family: var(--font-mono);
font-size: 11px;
color: var(--text-muted);
margin-bottom: 8px;
}
.ext-card h3 {
font-size: 18px;
font-weight: 700;
margin-bottom: 6px;
letter-spacing: -0.01em;
}
.ext-card p {
color: var(--text-muted);
font-size: 14px;
margin-bottom: 14px;
line-height: 1.5;
}
.ext-card .links a {
font-size: 14px;
font-weight: 500;
color: var(--accent);
}
@media (min-width: 768px) { @media (min-width: 768px) {
.error-code { font-size: 80px; } .error-code { font-size: 80px; }
.error-page h1 { font-size: 32px; } .error-page h1 { font-size: 32px; }
+29 -26
View File
@@ -1,20 +1,21 @@
--- ---
import Base from '../layouts/Base.astro'; import Base from '../layouts/Base.astro';
const gearSvg = `<svg width="160" height="160" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg"> const gearSvg = `<svg width="180" height="180" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<g class="gear-spin-fast"> <g class="gear-spin-fast">
<path d="M50 10 L54 18 L62 16 L64 24 L72 24 L70 32 L78 36 L72 42 L78 48 L70 52 L72 60 L64 60 L62 68 L54 66 L50 74 L46 66 L38 68 L36 60 L28 60 L30 52 L22 48 L28 42 L22 36 L30 32 L28 24 L36 24 L38 16 L46 18 Z" <path d="M50 8 L55 17 L64 14 L66 23 L75 23 L72 32 L80 36 L73 42 L80 49 L71 53 L73 62 L64 61 L62 70 L53 67 L50 76 L47 67 L38 70 L36 61 L27 62 L29 53 L20 49 L27 42 L20 36 L28 32 L25 23 L34 23 L36 14 L45 17 Z"
fill="none" stroke="var(--accent)" stroke-width="2.5" stroke-linejoin="round" /> fill="none" stroke="var(--accent)" stroke-width="2" stroke-linejoin="round" />
<circle cx="50" cy="50" r="14" fill="none" stroke="var(--accent)" stroke-width="2.5" /> <circle cx="50" cy="50" r="15" fill="none" stroke="var(--accent)" stroke-width="2" />
<circle cx="50" cy="50" r="5" fill="var(--accent)" /> <circle cx="50" cy="50" r="4" fill="var(--accent)" />
</g> </g>
<path d="M50 50 L70 30 L75 15" stroke="var(--accent)" stroke-width="1.5" stroke-dasharray="3 3" opacity="0.6" /> <path d="M50 50 L70 28 L74 12" stroke="var(--accent)" stroke-width="1.5" stroke-dasharray="3 3" opacity="0.5" />
<path d="M50 50 L25 35 L15 20" stroke="var(--accent)" stroke-width="1.5" stroke-dasharray="3 3" opacity="0.6" /> <path d="M50 50 L24 34 L14 18" stroke="var(--accent)" stroke-width="1.5" stroke-dasharray="3 3" opacity="0.5" />
<path d="M50 50 L60 80 L55 95" stroke="var(--accent)" stroke-width="1.5" stroke-dasharray="3 3" opacity="0.4" /> <path d="M50 50 L60 78 L54 94" stroke="var(--accent)" stroke-width="1.5" stroke-dasharray="3 3" opacity="0.3" />
<circle cx="80" cy="20" r="1.5" fill="var(--accent)" class="spark spark-1" /> <circle cx="78" cy="16" r="2" fill="var(--accent)" class="spark spark-1" />
<circle cx="12" cy="25" r="1.5" fill="var(--accent)" class="spark spark-2" /> <circle cx="14" cy="22" r="2" fill="var(--accent)" class="spark spark-2" />
<circle cx="85" cy="75" r="1.5" fill="var(--accent)" class="spark spark-3" /> <circle cx="82" cy="72" r="2" fill="var(--accent)" class="spark spark-3" />
<circle cx="18" cy="85" r="1" fill="var(--accent)" class="spark spark-4" /> <circle cx="20" cy="82" r="1.5" fill="var(--accent)" class="spark spark-4" />
<circle cx="66" cy="88" r="1.5" fill="var(--accent)" class="spark spark-5" />
</svg>`; </svg>`;
--- ---
@@ -58,35 +59,37 @@ const gearSvg = `<svg width="160" height="160" viewBox="0 0 100 100" fill="none"
} }
.error-gear { .error-gear {
margin-bottom: 28px; margin-bottom: 24px;
display: flex; display: flex;
justify-content: center; justify-content: center;
} }
.gear-spin-fast { .gear-spin-fast {
transform-origin: 50px 50px; transform-origin: 50px 50px;
animation: gear-seize 1.5s ease-in-out infinite; animation: gear-seize 2s ease-in-out infinite;
} }
@keyframes gear-seize { @keyframes gear-seize {
0%, 100% { transform: rotate(0deg); } 0%, 100% { transform: rotate(0deg); }
15% { transform: rotate(25deg); } 12% { transform: rotate(28deg); }
20% { transform: rotate(22deg); } 18% { transform: rotate(24deg); }
25% { transform: rotate(35deg); } 24% { transform: rotate(38deg); }
30% { transform: rotate(33deg); } 30% { transform: rotate(35deg); }
50% { transform: rotate(33deg); } 48% { transform: rotate(35deg); }
55% { transform: rotate(10deg); } 54% { transform: rotate(8deg); }
60% { transform: rotate(15deg); } 60% { transform: rotate(14deg); }
75% { transform: rotate(5deg); }
100% { transform: rotate(0deg); } 100% { transform: rotate(0deg); }
} }
.spark { .spark {
animation: spark-blink 0.8s ease-in-out infinite; animation: spark-blink 0.9s ease-in-out infinite;
} }
.spark-1 { animation-delay: 0s; } .spark-1 { animation-delay: 0s; }
.spark-2 { animation-delay: 0.15s; } .spark-2 { animation-delay: 0.18s; }
.spark-3 { animation-delay: 0.3s; } .spark-3 { animation-delay: 0.36s; }
.spark-4 { animation-delay: 0.45s; } .spark-4 { animation-delay: 0.54s; }
.spark-5 { animation-delay: 0.72s; }
@keyframes spark-blink { @keyframes spark-blink {
0%, 100% { opacity: 0; } 0%, 100% { opacity: 0; }
@@ -124,7 +127,7 @@ const gearSvg = `<svg width="160" height="160" viewBox="0 0 100 100" fill="none"
gap: 10px; gap: 10px;
justify-content: center; justify-content: center;
flex-wrap: wrap; flex-wrap: wrap;
margin-bottom: 32px; margin-bottom: 28px;
} }
.error-hint { .error-hint {