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)
This commit is contained in:
Ken
2026-05-27 04:57:55 +00:00
parent f7851fb3a7
commit aea20d335d
2 changed files with 461 additions and 158 deletions
+349 -142
View File
@@ -16,6 +16,15 @@ const ragePosts: Record<string, string> = {
'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',
@@ -23,7 +32,7 @@ const rageSeries = [
'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 => {
].map((title, i) => {
const slug = ragePosts[title];
const post = slug ? posts.find(p => p.id === slug) : null;
return {
@@ -31,6 +40,8 @@ const rageSeries = [
slug: slug || null,
published: !!post,
readingTime: post?.body ? getReadingTime(post.body) : null,
number: i + 1,
description: rageDescriptions[i],
};
});
---
@@ -38,93 +49,185 @@ const rageSeries = [
<BaseLayout title='Crash Test Dev'>
<NavBar slot="nav" />
<!-- Hero: full-width, two-column -->
<section class="hero" aria-label="Introduction">
<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 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>
<hr class="divider" />
<!-- Featured excerpt: full-width with visual -->
<section class="featured" aria-label="Featured excerpt">
<p class="featured-label">From the latest</p>
<blockquote class="excerpt">
AI raises the floor dramatically. It does not flatten the ceiling. The profession that survives is the one hardest to demo without, not just hardest to do without. The professional sees second-order failures coming. The amateur with AI doesn't know there is a second order.
</blockquote>
<a class="featured-link" href={`${base}posts/ai-commoditizes-features-but-judgment-compounds/`}>
Read "AI Commoditizes Features, But Judgment Compounds"
</a>
<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>
<hr class="divider" />
<!-- 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>
<ol class="series-list">
<div class="series-grid">
{rageSeries.map((item) => (
<li class={`series-item ${item.published ? 'published' : ''}`}>
{item.published ? (
<a href={`${base}posts/${item.slug}/`} class="series-item-link">
<span class="series-item-title">{item.title}</span>
<span class="series-item-meta">{item.readingTime} min</span>
</a>
) : (
<>
<span class="series-item-title">{item.title}</span>
<span class="series-item-status">Coming soon</span>
</>
)}
</li>
<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>
))}
</ol>
</div>
</section>
<hr class="divider" />
<!-- All Posts: compact 2-column grid at reading width -->
<section class="all-posts" aria-label="All posts">
<h2 class="section-heading">All Posts</h2>
{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 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>
/* Layout column */
.hero,
.featured,
.series,
.all-posts {
max-width: var(--width-prose);
margin-left: auto;
margin-right: auto;
}
/* Dividers */
.divider {
max-width: var(--width-prose);
margin: var(--space-6) auto;
border: none;
border-top: 1px solid var(--border-subtle);
}
/* Hero */
/* ── Hero: full-width two-column ────────────────────────────── */
.hero {
margin-top: var(--space-10);
margin-bottom: var(--space-6);
text-align: center;
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 .site-title {
.hero-text {
max-width: 540px;
justify-self: end;
}
.site-title {
font-family: var(--font-structure);
font-size: var(--type-3xl);
font-weight: 700;
@@ -134,15 +237,16 @@ const rageSeries = [
margin-bottom: var(--space-2);
}
.hero .tagline {
.tagline {
font-family: var(--font-prose);
font-size: var(--type-lg);
color: var(--text-secondary);
line-height: var(--leading-prose);
margin-bottom: var(--space-1);
font-style: italic;
margin-bottom: var(--space-1-5);
}
.hero .byline {
.byline {
font-family: var(--font-structure);
font-size: var(--type-sm);
color: var(--text-tertiary);
@@ -150,11 +254,37 @@ const rageSeries = [
text-transform: uppercase;
}
/* Featured excerpt */
.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);
@@ -170,8 +300,6 @@ const rageSeries = [
font-size: var(--type-lg);
color: var(--text-primary);
line-height: var(--leading-prose);
border-left: 3px solid var(--accent-primary);
padding-left: var(--space-3);
margin-bottom: var(--space-3);
font-style: italic;
}
@@ -182,9 +310,20 @@ const rageSeries = [
font-weight: 500;
}
/* Series */
.featured-diagram svg {
width: 100%;
height: auto;
border-radius: var(--rounded-md);
box-shadow: var(--shadow-sm);
}
/* ── Series: container-width 2-column grid ─────────────────── */
.series {
margin-bottom: var(--space-6);
max-width: var(--width-container);
margin-left: auto;
margin-right: auto;
margin-bottom: var(--space-8);
padding: var(--space-6) 0;
}
.section-heading {
@@ -202,100 +341,106 @@ const rageSeries = [
color: var(--text-secondary);
line-height: var(--leading-prose);
margin-bottom: var(--space-4);
max-width: var(--width-prose);
}
.series-list {
list-style: none;
counter-reset: series;
display: flex;
flex-direction: column;
gap: var(--space-1-5);
.series-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-3);
}
.series-item {
counter-increment: series;
display: flex;
justify-content: space-between;
align-items: baseline;
padding: var(--space-1) var(--space-1-5);
border-radius: var(--rounded-md);
background-color: var(--surface-card);
.series-card {
background: var(--surface-card);
border: 1px solid var(--border-subtle);
}
.series-item::before {
content: counter(series) ".";
font-family: var(--font-evidence);
font-size: var(--type-sm);
color: var(--text-tertiary);
margin-right: var(--space-1-5);
flex-shrink: 0;
}
.series-item-title {
font-family: var(--font-structure);
font-size: var(--type-sm);
font-weight: 500;
color: var(--text-primary);
flex: 1;
}
.series-item-status {
font-family: var(--font-evidence);
font-size: var(--type-xs);
color: var(--text-tertiary);
flex-shrink: 0;
margin-left: var(--space-2);
}
.series-item-link {
border-radius: var(--rounded-lg);
padding: var(--space-3);
box-shadow: var(--shadow-sm);
display: flex;
justify-content: space-between;
align-items: baseline;
width: 100%;
text-decoration: none;
color: inherit;
gap: var(--space-2);
align-items: flex-start;
transition: box-shadow var(--duration-normal) var(--ease-default);
}
.series-item-link:hover {
text-decoration: none;
.series-card:hover {
box-shadow: var(--shadow-md);
}
.series-item-link:hover .series-item-title {
color: var(--accent-primary);
}
.series-item-meta {
font-family: var(--font-evidence);
font-size: var(--type-xs);
color: var(--text-tertiary);
flex-shrink: 0;
margin-left: var(--space-2);
}
.series-item.published {
.series-card.published {
border-color: var(--border-default);
}
.series-item.published .series-item-title {
.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);
}
/* All posts */
.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);
}
.post-entry {
border-bottom: 1px solid var(--border-subtle);
.posts-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0 var(--space-4);
}
.post-entry:last-child {
border-bottom: none;
.post-entry {
border-bottom: 1px solid var(--border-subtle);
}
.post-entry-link {
@@ -341,4 +486,66 @@ const rageSeries = [
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>
+112 -16
View File
@@ -63,6 +63,24 @@ describe('Index page', () => {
const content = readPage();
assert.match(content, /class="byline"/, 'should have a byline element');
});
it('should contain an inline SVG hero illustration', () => {
const content = readPage();
// The hero section should contain an SVG element
assert.match(content, /class="hero-illustration"/, 'should have a hero illustration');
assert.match(content, /<svg[^>]*viewBox/, 'should have an inline SVG with viewBox');
});
it('should use CSS grid for hero layout', () => {
const content = readPage();
assert.match(content, /\.hero\s*\{[^}]*display:\s*grid/s, 'hero should use CSS grid');
assert.match(content, /\.hero\s*\{[^}]*grid-template-columns/s, 'hero should define grid-template-columns');
});
it('should be full viewport width', () => {
const content = readPage();
assert.match(content, /\.hero\s*\{[^}]*max-width:\s*100%/s, 'hero should be full viewport width');
});
});
describe('Featured excerpt section', () => {
@@ -78,12 +96,22 @@ describe('Index page', () => {
it('should have a link to the full post', () => {
const content = readPage();
// The featured section should contain a link to read the full post
assert.match(content, /class="featured-link"/, 'should have a featured-link to the full post');
});
it('should have a terracotta left border on the featured section', () => {
const content = readPage();
assert.match(content, /\.featured[\s\S]*?border-left.*accent-primary/s, 'should have terracotta left border');
});
it('should include an inline SVG diagram from a post', () => {
const content = readPage();
// The featured section should contain a small SVG from one of the posts
assert.match(content, /class="featured-diagram"/, 'should have a featured-diagram element');
});
});
describe('Series section', () => {
describe('Series section - multi-column grid', () => {
it('should have a series section', () => {
const content = readPage();
assert.match(content, /class="series"/, 'should have a section with class series');
@@ -94,33 +122,77 @@ describe('Index page', () => {
assert.match(content, /Rage/, 'should mention Rage in the series section');
});
it('should list planned posts', () => {
it('should use a CSS grid for series cards', () => {
const content = readPage();
assert.match(content, /class="series-item"/, 'should have series items');
assert.match(content, /\.series-grid\s*\{[^}]*display:\s*grid/s, 'should use CSS grid for series cards');
assert.match(content, /\.series-grid\s*\{[^}]*grid-template-columns:\s*1fr\s+1fr/s, 'should have 2-column grid');
});
it('should have series card elements', () => {
const content = readPage();
assert.match(content, /series-card/, 'should have series-card elements');
});
it('should show card numbers in mono font with accent color', () => {
const content = readPage();
assert.match(content, /class="card-number"/, 'should have card-number elements');
assert.match(content, /\.card-number\s*\{[^}]*font-family:\s*var\(--font-evidence\)/s, 'card number should use mono font');
assert.match(content, /\.card-number\s*\{[^}]*color:\s*var\(--accent-primary\)/s, 'card number should use accent color');
});
it('should show card descriptions', () => {
const content = readPage();
assert.match(content, /class="card-description"/, 'should have card-description elements');
});
it('should indicate coming soon items', () => {
const content = readPage();
assert.match(content, /coming soon/i, 'should indicate coming soon posts');
});
it('should use --width-container for series section width', () => {
const content = readPage();
assert.match(content, /\.series\s*\{[^}]*max-width:\s*var\(--width-container\)/s, 'series should use container width');
});
it('should collapse to single column on mobile', () => {
const content = readPage();
// Check for media query that changes grid to 1 column
assert.match(content, /@media.*768px[\s\S]*?grid-template-columns:\s*1fr\s*[;}]/s, 'should collapse to 1 column on mobile');
});
it('should have card surface background', () => {
const content = readPage();
assert.match(content, /\.series-card\s*\{[^}]*background[^:]*:\s*var\(--surface-card\)/s, 'cards should have card surface bg');
});
});
describe('All posts section', () => {
it('should have a posts listing section below the pitch', () => {
describe('All posts section - 2-column grid', () => {
it('should have a posts listing section', () => {
const content = readPage();
assert.match(content, /class="all-posts"/, 'should have a section with class all-posts');
});
it('should map posts to compact entries', () => {
it('should map posts to entries', () => {
const content = readPage();
assert.match(content, /sortedPosts\.map/, 'should map sortedPosts');
});
it('should show post titles as links', () => {
const content = readPage();
// Each post entry should link to its page
assert.match(content, /posts\/\$\{post\.id\}/, 'should link to post pages');
});
it('should use --width-prose for all-posts width', () => {
const content = readPage();
assert.match(content, /\.all-posts\s*\{[^}]*max-width:\s*var\(--width-prose\)/s, 'all-posts should use prose width');
});
it('should use CSS grid for posts on desktop', () => {
const content = readPage();
assert.match(content, /\.posts-grid\s*\{[^}]*display:\s*grid/s, 'should use CSS grid for posts');
assert.match(content, /\.posts-grid\s*\{[^}]*grid-template-columns:\s*1fr\s+1fr/s, 'should have 2-column posts grid');
});
});
describe('Layout', () => {
@@ -136,25 +208,49 @@ describe('Index page', () => {
});
describe('Style', () => {
it('should use --width-prose for content width', () => {
const content = readPage();
assert.match(content, /max-width\s*:\s*var\(--width-prose\)/, 'should constrain content to prose width');
});
it('should use --font-structure for hero heading', () => {
const content = readPage();
assert.match(content, /\.hero[\s\S]*?font-family\s*:\s*var\(--font-structure\)/, 'should use structure font in hero');
assert.match(content, /\.hero[\s\S]*?font-family:\s*var\(--font-structure\)/, 'should use structure font in hero');
});
it('should use --font-prose for excerpt', () => {
const content = readPage();
assert.match(content, /\.excerpt[\s\S]*?font-family\s*:\s*var\(--font-prose\)/, 'should use prose font for excerpt');
assert.match(content, /\.excerpt[\s\S]*?font-family:\s*var\(--font-prose\)/, 'should use prose font for excerpt');
});
it('should not have the old .post-list as the main layout', () => {
const content = readPage();
// The old flat list structure should be gone
assert.doesNotMatch(content, /<ArticleCard/, 'should not use ArticleCard component');
});
it('should use different widths for different sections', () => {
const content = readPage();
// Hero: full width (100%)
assert.match(content, /\.hero[\s\S]*?max-width:\s*100%/s, 'hero should be full width');
// Series: container width
assert.match(content, /\.series[\s\S]*?max-width:\s*var\(--width-container\)/s, 'series should use container width');
// All posts: prose width
assert.match(content, /\.all-posts[\s\S]*?max-width:\s*var\(--width-prose\)/s, 'all-posts should use prose width');
});
});
describe('Rage series data', () => {
it('should have descriptions for each series item', () => {
const content = readPage();
// Check for at least a couple of the descriptions
assert.match(content, /silent stale cache/i, 'should have description for post 1');
assert.match(content, /Weak fingerprints find candidates/i, 'should have description for post 2');
assert.match(content, /content-addressed storage/i, 'should have description for post 5');
});
it('should include all 6 rage series items', () => {
const content = readPage();
assert.match(content, /Your Build Cache Is Lying to You/, 'should have post 1');
assert.match(content, /Two-Phase Fingerprinting/, 'should have post 2');
assert.match(content, /Hooking Syscalls/, 'should have post 3');
assert.match(content, /TypeScript 7/, 'should have post 4');
assert.match(content, /node_modules/, 'should have post 5');
assert.match(content, /Maintaining Lage/, 'should have post 6');
});
});
});