Files
crashtestdev/src/pages/index.astro
T
Ken aea20d335d feat: redesign homepage as multi-column visual landing page
- Full-width hero with CSS grid: site title/tagline (left) + hand-coded
  SVG workbench illustration (right), stacks on mobile
- Featured excerpt section with terracotta left border and inline
  diagram from 'Your Build Cache Is Lying to You'
- Rage series as 2-column card grid at container width (1100px):
  each card shows number, title, description, reading time or
  'Coming soon' badge
- All posts as 2-column grid at reading width (720px)
- Three-tier width system: hero (100%), series (--width-container),
  all-posts (--width-prose)
- Responsive: collapses to single column below 768px
- Updated tests to verify multi-column layout, SVG illustration,
  card grid structure, and responsive breakpoints (41 tests, all pass)
2026-05-27 04:57:55 +00:00

552 lines
20 KiB
Plaintext

---
import { getCollection } from 'astro:content';
import BaseLayout from '../layouts/BaseLayout.astro';
import NavBar from '../components/NavBar.astro';
import { getReadingTime } from '../utils/reading-time';
const posts = await getCollection('posts');
const sortedPosts = posts.sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
const base = import.meta.env.BASE_URL.replace(/\/?$/, '/');
// Match rage series titles to actual published posts by slug
const ragePosts: Record<string, string> = {
'Your Build Cache Is Lying to You': 'your-build-cache-is-lying-to-you',
'Two-Phase Fingerprinting: BuildXL\u2019s Deep Magic': 'two-phase-fingerprinting-buildxls-deep-magic',
'Hooking Syscalls Without a Kernel Driver': 'hooking-syscalls-without-a-kernel-driver',
'TypeScript 7 Makes the Build Harness More Important': 'typescript-7-makes-the-build-harness-more-important',
};
const rageDescriptions = [
'The silent stale cache problem. Every JS monorepo build cache shares this flaw.',
'Weak fingerprints find candidates. Strong fingerprints verify them. The pathset is the bridge.',
'Three operating systems. Three interception mechanisms. macOS interpose, Linux eBPF, Windows Detours.',
'Counter-intuitive: tsgo\u2019s 10x speedup makes distributed coordination more important, not less.',
'Per-package content-addressed storage. The unsexy problem that kills CI build times.',
'Personal narrative. From lage maintainer to Rust rewrite. The architecture\u2019s original sin.',
];
const rageSeries = [
'Your Build Cache Is Lying to You',
'Two-Phase Fingerprinting: BuildXL\u2019s Deep Magic',
'Hooking Syscalls Without a Kernel Driver',
'TypeScript 7 Makes the Build Harness More Important',
'node_modules in a Content-Addressed Store',
'What I Learned Maintaining Lage and Why I Rewrote It',
].map((title, i) => {
const slug = ragePosts[title];
const post = slug ? posts.find(p => p.id === slug) : null;
return {
title,
slug: slug || null,
published: !!post,
readingTime: post?.body ? getReadingTime(post.body) : null,
number: i + 1,
description: rageDescriptions[i],
};
});
---
<BaseLayout title='Crash Test Dev'>
<NavBar slot="nav" />
<!-- Hero: full-width, two-column -->
<section class="hero" aria-label="Introduction">
<div class="hero-text">
<h1 class="site-title">Crash Test Dev</h1>
<p class="tagline">Engineering judgment, tested against reality.</p>
<p class="byline">By Ken Chau</p>
</div>
<div class="hero-illustration" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400" role="img" aria-label="Engineering workbench illustration">
<!-- Background -->
<rect width="400" height="400" fill="#F5F0E8" rx="6"/>
<!-- Pegboard grid (subtle dots) -->
<g fill="#DDD7CD">
<circle cx="60" cy="60" r="2"/><circle cx="100" cy="60" r="2"/><circle cx="140" cy="60" r="2"/><circle cx="180" cy="60" r="2"/><circle cx="220" cy="60" r="2"/><circle cx="260" cy="60" r="2"/><circle cx="300" cy="60" r="2"/><circle cx="340" cy="60" r="2"/>
<circle cx="60" cy="100" r="2"/><circle cx="100" cy="100" r="2"/><circle cx="140" cy="100" r="2"/><circle cx="180" cy="100" r="2"/><circle cx="220" cy="100" r="2"/><circle cx="260" cy="100" r="2"/><circle cx="300" cy="100" r="2"/><circle cx="340" cy="100" r="2"/>
<circle cx="60" cy="140" r="2"/><circle cx="100" cy="140" r="2"/><circle cx="140" cy="140" r="2"/><circle cx="180" cy="140" r="2"/><circle cx="220" cy="140" r="2"/><circle cx="260" cy="140" r="2"/><circle cx="300" cy="140" r="2"/><circle cx="340" cy="140" r="2"/>
</g>
<!-- Wrench (hanging tool) -->
<line x1="100" y1="60" x2="100" y2="90" stroke="#2B2421" stroke-width="2"/>
<rect x="88" y="90" width="24" height="8" rx="2" fill="none" stroke="#2B2421" stroke-width="1.5"/>
<rect x="92" y="98" width="4" height="32" rx="1" fill="#2B2421"/>
<circle cx="94" cy="134" r="8" fill="none" stroke="#2B2421" stroke-width="1.5"/>
<!-- Caliper (hanging tool) -->
<line x1="220" y1="60" x2="220" y2="85" stroke="#2B2421" stroke-width="2"/>
<rect x="205" y="85" width="30" height="6" rx="1" fill="#7D6C2F"/>
<line x1="210" y1="91" x2="210" y2="140" stroke="#2B2421" stroke-width="1.5"/>
<line x1="230" y1="91" x2="230" y2="130" stroke="#2B2421" stroke-width="1.5"/>
<line x1="210" y1="140" x2="218" y2="148" stroke="#2B2421" stroke-width="1.5"/>
<line x1="230" y1="130" x2="222" y2="138" stroke="#2B2421" stroke-width="1.5"/>
<!-- Gear 1 (large) -->
<circle cx="160" cy="260" r="44" fill="none" stroke="#A0522D" stroke-width="2"/>
<circle cx="160" cy="260" r="32" fill="none" stroke="#A0522D" stroke-width="1.5"/>
<circle cx="160" cy="260" r="8" fill="#A0522D"/>
<!-- Gear teeth -->
<rect x="155" y="212" width="10" height="10" rx="1" fill="#A0522D"/>
<rect x="155" y="298" width="10" height="10" rx="1" fill="#A0522D"/>
<rect x="200" y="255" width="10" height="10" rx="1" fill="#A0522D"/>
<rect x="112" y="255" width="10" height="10" rx="1" fill="#A0522D"/>
<rect x="188" y="225" width="10" height="10" rx="1" fill="#A0522D" transform="rotate(45 193 230)"/>
<rect x="122" y="285" width="10" height="10" rx="1" fill="#A0522D" transform="rotate(45 127 290)"/>
<rect x="122" y="225" width="10" height="10" rx="1" fill="#A0522D" transform="rotate(-45 127 230)"/>
<rect x="188" y="285" width="10" height="10" rx="1" fill="#A0522D" transform="rotate(-45 193 290)"/>
<!-- Gear 2 (smaller, interlocking) -->
<circle cx="280" cy="240" r="28" fill="none" stroke="#7D6C2F" stroke-width="2"/>
<circle cx="280" cy="240" r="18" fill="none" stroke="#7D6C2F" stroke-width="1.5"/>
<circle cx="280" cy="240" r="5" fill="#7D6C2F"/>
<!-- Gear teeth -->
<rect x="275" y="208" width="10" height="8" rx="1" fill="#7D6C2F"/>
<rect x="275" y="264" width="10" height="8" rx="1" fill="#7D6C2F"/>
<rect x="304" y="235" width="8" height="10" rx="1" fill="#7D6C2F"/>
<rect x="248" y="235" width="8" height="10" rx="1" fill="#7D6C2F"/>
<!-- Workbench surface -->
<rect x="30" y="340" width="340" height="12" rx="2" fill="#2B2421"/>
<rect x="24" y="352" width="16" height="28" rx="1" fill="#2B2421"/>
<rect x="360" y="352" width="16" height="28" rx="1" fill="#2B2421"/>
<!-- Blueprint/ruler on bench -->
<rect x="50" y="324" width="80" height="14" rx="1" fill="none" stroke="#7D6C2F" stroke-width="1"/>
<line x1="60" y1="324" x2="60" y2="338" stroke="#7D6C2F" stroke-width="0.5"/>
<line x1="70" y1="328" x2="70" y2="338" stroke="#7D6C2F" stroke-width="0.5"/>
<line x1="80" y1="324" x2="80" y2="338" stroke="#7D6C2F" stroke-width="0.5"/>
<line x1="90" y1="328" x2="90" y2="338" stroke="#7D6C2F" stroke-width="0.5"/>
<line x1="100" y1="324" x2="100" y2="338" stroke="#7D6C2F" stroke-width="0.5"/>
<line x1="110" y1="328" x2="110" y2="338" stroke="#7D6C2F" stroke-width="0.5"/>
<line x1="120" y1="324" x2="120" y2="338" stroke="#7D6C2F" stroke-width="0.5"/>
</svg>
</div>
</section>
<!-- Featured excerpt: full-width with visual -->
<section class="featured" aria-label="Featured excerpt">
<div class="featured-inner">
<div class="featured-content">
<p class="featured-label">From the series</p>
<blockquote class="excerpt">
Your CI passed. Your tests passed. The build cache said nothing changed. It was wrong. You shipped a broken build to production because a cache returned stale output from three weeks ago. The bug took two days to find. The fix was one line. The root cause was not your code. It was your build system's trust model.
</blockquote>
<a class="featured-link" href={`${base}posts/your-build-cache-is-lying-to-you/`}>
Read "Your Build Cache Is Lying to You" &rarr;
</a>
</div>
<div class="featured-diagram" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 420 200" role="img" aria-label="The Silent Lie: how an undeclared input causes a stale cache hit">
<rect width="420" height="200" fill="#F5F0E8" rx="4"/>
<text x="210" y="24" text-anchor="middle" font-family="Georgia, serif" font-size="12" font-weight="bold" fill="#2B2421">The Silent Lie</text>
<rect x="14" y="40" width="100" height="44" rx="3" fill="none" stroke="#2B2421" stroke-width="1.2"/>
<text x="64" y="58" text-anchor="middle" font-family="monospace" font-size="8" fill="#2B2421">Developer changes</text>
<text x="64" y="72" text-anchor="middle" font-family="monospace" font-size="8" font-weight="bold" fill="#A0522D">feature-flags.json</text>
<line x1="114" y1="62" x2="136" y2="62" stroke="#2B2421" stroke-width="1.2"/>
<polygon points="134,58 142,62 134,66" fill="#2B2421"/>
<rect x="144" y="40" width="100" height="44" rx="3" fill="none" stroke="#2B2421" stroke-width="1.2"/>
<text x="194" y="56" text-anchor="middle" font-family="monospace" font-size="7.5" fill="#2B2421">Cache check:</text>
<text x="194" y="68" text-anchor="middle" font-family="monospace" font-size="7.5" fill="#2B2421">hash(src/**/*.ts)</text>
<text x="194" y="78" text-anchor="middle" font-family="monospace" font-size="7.5" fill="#2B2421">Nothing changed</text>
<line x1="244" y1="62" x2="266" y2="62" stroke="#2B2421" stroke-width="1.2"/>
<polygon points="264,58 272,62 264,66" fill="#2B2421"/>
<rect x="274" y="40" width="60" height="44" rx="3" fill="#A0522D"/>
<text x="304" y="60" text-anchor="middle" font-family="Georgia, serif" font-size="11" font-weight="bold" fill="#F5F0E8">MATCH</text>
<text x="304" y="74" text-anchor="middle" font-family="monospace" font-size="7" fill="#F5F0E8">(stale!)</text>
<line x1="334" y1="62" x2="356" y2="62" stroke="#2B2421" stroke-width="1.2"/>
<polygon points="354,58 362,62 354,66" fill="#2B2421"/>
<rect x="364" y="44" width="44" height="36" rx="3" fill="#A0522D"/>
<text x="386" y="62" text-anchor="middle" font-family="Georgia, serif" font-size="8" font-weight="bold" fill="#F5F0E8">Bug ships</text>
<text x="386" y="72" text-anchor="middle" font-family="Georgia, serif" font-size="8" font-weight="bold" fill="#F5F0E8">to prod</text>
<rect x="120" y="110" width="200" height="36" rx="3" fill="none" stroke="#A0522D" stroke-width="1" stroke-dasharray="4,2"/>
<text x="220" y="128" text-anchor="middle" font-family="Georgia, serif" font-size="9" fill="#A0522D">feature-flags.json was never declared.</text>
<text x="220" y="140" text-anchor="middle" font-family="Georgia, serif" font-size="9" fill="#A0522D">The cache didn't know it existed.</text>
</svg>
</div>
</div>
</section>
<!-- Rage Series: 2-column grid at container width -->
<section class="series" aria-label="Rage series">
<h2 class="section-heading">Rage: Rebuilding the Build System</h2>
<p class="series-description">A six-part investigation into rage, a Rust monorepo build system that fuses BuildXL's provable correctness with lage's ergonomics. From syscall sandboxing to content-addressed storage.</p>
<div class="series-grid">
{rageSeries.map((item) => (
<div class={`series-card ${item.published ? 'published' : ''}`}>
<span class="card-number">{String(item.number).padStart(2, '0')}</span>
<div class="card-body">
<span class="card-title">{item.title}</span>
<span class="card-description">{item.description}</span>
{item.published ? (
<a href={`${base}posts/${item.slug}/`} class="card-link">
Read &middot; {item.readingTime} min
</a>
) : (
<span class="card-status">Coming soon</span>
)}
</div>
</div>
))}
</div>
</section>
<!-- All Posts: compact 2-column grid at reading width -->
<section class="all-posts" aria-label="All posts">
<h2 class="section-heading">All Posts</h2>
<div class="posts-grid">
{sortedPosts.map((post) => (
<article class="post-entry">
<a href={`${base}posts/${post.id}/`} class="post-entry-link">
<span class="post-entry-badge">{post.data.type}</span>
<span class="post-entry-title">{post.data.title}</span>
{post.body && (
<span class="post-entry-meta">{getReadingTime(post.body)} min</span>
)}
</a>
</article>
))}
</div>
</section>
</BaseLayout>
<style>
/* ── Hero: full-width two-column ────────────────────────────── */
.hero {
max-width: 100%;
display: grid;
grid-template-columns: 1fr 1fr;
align-items: center;
gap: var(--space-6);
padding: var(--space-10) var(--space-8);
border-bottom: 1px solid var(--border-subtle);
}
.hero-text {
max-width: 540px;
justify-self: end;
}
.site-title {
font-family: var(--font-structure);
font-size: var(--type-3xl);
font-weight: 700;
color: var(--text-primary);
line-height: var(--leading-tight);
letter-spacing: -0.02em;
margin-bottom: var(--space-2);
}
.tagline {
font-family: var(--font-prose);
font-size: var(--type-lg);
color: var(--text-secondary);
line-height: var(--leading-prose);
font-style: italic;
margin-bottom: var(--space-1-5);
}
.byline {
font-family: var(--font-structure);
font-size: var(--type-sm);
color: var(--text-tertiary);
letter-spacing: 0.03em;
text-transform: uppercase;
}
.hero-illustration {
max-width: 400px;
justify-self: start;
}
.hero-illustration svg {
width: 100%;
height: auto;
border-radius: var(--rounded-lg);
box-shadow: var(--shadow-md);
}
/* ── Featured excerpt: full-width with terracotta border ────── */
.featured {
max-width: var(--width-container);
margin-left: auto;
margin-right: auto;
padding: var(--space-8) 0;
border-left: 4px solid var(--accent-primary);
margin-top: var(--space-6);
margin-bottom: var(--space-6);
}
.featured-inner {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-6);
align-items: center;
padding-left: var(--space-4);
}
.featured-label {
font-family: var(--font-structure);
font-size: var(--type-xs);
font-weight: 600;
color: var(--accent-primary);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: var(--space-2);
}
.excerpt {
font-family: var(--font-prose);
font-size: var(--type-lg);
color: var(--text-primary);
line-height: var(--leading-prose);
margin-bottom: var(--space-3);
font-style: italic;
}
.featured-link {
font-family: var(--font-structure);
font-size: var(--type-sm);
font-weight: 500;
}
.featured-diagram svg {
width: 100%;
height: auto;
border-radius: var(--rounded-md);
box-shadow: var(--shadow-sm);
}
/* ── Series: container-width 2-column grid ─────────────────── */
.series {
max-width: var(--width-container);
margin-left: auto;
margin-right: auto;
margin-bottom: var(--space-8);
padding: var(--space-6) 0;
}
.section-heading {
font-family: var(--font-structure);
font-size: var(--type-xl);
font-weight: 600;
color: var(--text-primary);
line-height: var(--leading-snug);
margin-bottom: var(--space-1-5);
}
.series-description {
font-family: var(--font-prose);
font-size: var(--type-base);
color: var(--text-secondary);
line-height: var(--leading-prose);
margin-bottom: var(--space-4);
max-width: var(--width-prose);
}
.series-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-3);
}
.series-card {
background: var(--surface-card);
border: 1px solid var(--border-subtle);
border-radius: var(--rounded-lg);
padding: var(--space-3);
box-shadow: var(--shadow-sm);
display: flex;
gap: var(--space-2);
align-items: flex-start;
transition: box-shadow var(--duration-normal) var(--ease-default);
}
.series-card:hover {
box-shadow: var(--shadow-md);
}
.series-card.published {
border-color: var(--border-default);
}
.card-number {
font-family: var(--font-evidence);
font-size: var(--type-xl);
font-weight: 700;
color: var(--accent-primary);
line-height: 1;
flex-shrink: 0;
}
.card-body {
display: flex;
flex-direction: column;
gap: var(--space-0-5);
}
.card-title {
font-family: var(--font-structure);
font-size: var(--type-base);
font-weight: 600;
color: var(--text-primary);
line-height: var(--leading-snug);
}
.card-description {
font-family: var(--font-prose);
font-size: var(--type-sm);
color: var(--text-secondary);
line-height: var(--leading-normal);
}
.card-link {
font-family: var(--font-structure);
font-size: var(--type-sm);
font-weight: 500;
margin-top: var(--space-0-5);
transition: color var(--duration-fast) var(--ease-default);
}
.card-status {
font-family: var(--font-evidence);
font-size: var(--type-xs);
color: var(--text-tertiary);
margin-top: var(--space-0-5);
background: var(--surface-inset);
padding: var(--space-0-5) var(--space-1);
border-radius: var(--rounded-full);
display: inline-block;
width: fit-content;
}
/* ── All posts: reading-width 2-column grid ────────────────── */
.all-posts {
max-width: var(--width-prose);
margin-left: auto;
margin-right: auto;
margin-bottom: var(--space-12);
padding: var(--space-6) 0;
}
.all-posts .section-heading {
margin-bottom: var(--space-3);
}
.posts-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0 var(--space-4);
}
.post-entry {
border-bottom: 1px solid var(--border-subtle);
}
.post-entry-link {
display: flex;
align-items: baseline;
gap: var(--space-1-5);
padding: var(--space-1-5) 0;
text-decoration: none;
color: inherit;
transition: color var(--duration-fast) var(--ease-default);
}
.post-entry-link:hover {
text-decoration: none;
}
.post-entry-link:hover .post-entry-title {
color: var(--accent-primary);
}
.post-entry-badge {
font-family: var(--font-structure);
font-size: var(--type-xs);
font-weight: 600;
color: var(--accent-primary);
text-transform: uppercase;
letter-spacing: 0.05em;
flex-shrink: 0;
}
.post-entry-title {
font-family: var(--font-structure);
font-size: var(--type-base);
font-weight: 500;
color: var(--text-primary);
flex: 1;
transition: color var(--duration-fast) var(--ease-default);
}
.post-entry-meta {
font-family: var(--font-evidence);
font-size: var(--type-xs);
color: var(--text-tertiary);
flex-shrink: 0;
}
/* ── Responsive: tablet ────────────────────────────────────── */
@media (max-width: 1023px) {
.hero {
padding: var(--space-8) var(--space-4);
gap: var(--space-4);
}
.hero-text {
max-width: none;
}
.hero-illustration {
max-width: 320px;
}
.featured-inner {
grid-template-columns: 1fr 1fr;
}
.posts-grid {
grid-template-columns: 1fr;
}
}
/* ── Responsive: mobile ────────────────────────────────────── */
@media (max-width: 768px) {
.hero {
grid-template-columns: 1fr;
text-align: center;
padding: var(--space-6) var(--space-3);
gap: var(--space-4);
}
.hero-text {
justify-self: center;
}
.hero-illustration {
justify-self: center;
max-width: 280px;
}
.featured {
border-left: none;
border-top: 4px solid var(--accent-primary);
padding: var(--space-4) 0;
}
.featured-inner {
grid-template-columns: 1fr;
padding-left: 0;
}
.series-grid {
grid-template-columns: 1fr;
}
.posts-grid {
grid-template-columns: 1fr;
}
}
</style>