From 29b7dcc51d3d975207f1f844e3a35d09832e2623 Mon Sep 17 00:00:00 2001 From: Ken Date: Wed, 27 May 2026 01:25:52 +0000 Subject: [PATCH] feat: redesign homepage from flat card list to book-style pitch page --- src/pages/index.astro | 274 ++++++++++++++++++++++++++++-- tests/final-verification.test.mjs | 46 ++--- tests/index-page.test.mjs | 158 +++++++++-------- 3 files changed, 374 insertions(+), 104 deletions(-) diff --git a/src/pages/index.astro b/src/pages/index.astro index 3ca95df..113ef5d 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -2,38 +2,284 @@ import { getCollection } from 'astro:content'; import BaseLayout from '../layouts/BaseLayout.astro'; import NavBar from '../components/NavBar.astro'; -import ArticleCard from '../components/ArticleCard.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(/\/?$/, '/'); + +const rageSeries = [ + { title: 'Your Build Cache Is Lying to You', status: 'coming-soon' }, + { title: 'Two-Phase Fingerprinting: BuildXL\u2019s Deep Magic', status: 'coming-soon' }, + { title: 'Hooking Syscalls Without a Kernel Driver', status: 'coming-soon' }, + { title: 'TypeScript 7 Makes the Build Harness More Important', status: 'coming-soon' }, + { title: 'node_modules in a Content-Addressed Store', status: 'coming-soon' }, + { title: 'What I Learned Maintaining Lage and Why I Rewrote It', status: 'coming-soon' }, +]; --- -
+ +
+

Crash Test Dev

+

Engineering judgment, tested against reality.

+ +
+ +
+ + + +
+ +
+

Rage: Rebuilding the Build System

+

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.

+
    + {rageSeries.map((item) => ( +
  1. + {item.title} + Coming soon +
  2. + ))} +
+
+ +
+ +
+

All Posts

{sortedPosts.map((post) => ( - + ))}
diff --git a/tests/final-verification.test.mjs b/tests/final-verification.test.mjs index 5d8321b..e094974 100644 --- a/tests/final-verification.test.mjs +++ b/tests/final-verification.test.mjs @@ -35,39 +35,45 @@ describe('Clean build output', () => { }); // ============================================================ -// 2. Index page structure +// 2. Index page structure (book-style pitch page) // ============================================================ describe('Index page', () => { const html = readDist('index.html'); - test('contains two article cards', () => { - const cardMatches = html.match(/]*class="card"/g); - assert.ok(cardMatches, 'No article cards found'); - assert.equal(cardMatches.length, 2, `Expected 2 article cards, got ${cardMatches.length}`); + test('has hero section with site title', () => { + assert.ok(html.includes('class="hero"') || html.includes('class=\"hero\"'), + 'Should have hero section'); + assert.ok(html.includes('Crash Test Dev'), 'Should have site title'); }); - test('card surface uses warm off-white (--surface-card)', () => { - assert.ok(html.includes('var(--surface-card)'), 'Card should use --surface-card token'); + test('has featured excerpt section', () => { + assert.ok(html.includes('class="featured"') || html.includes('class=\"featured\"'), + 'Should have featured section'); + assert.ok(html.includes('class="excerpt"') || html.includes('class=\"excerpt\"'), + 'Should have excerpt blockquote'); }); - test('card has subtle shadow (--shadow-sm)', () => { - assert.ok(html.includes('var(--shadow-sm)'), 'Card should use --shadow-sm token'); + test('has series section with Rage', () => { + assert.ok(html.includes('class="series"') || html.includes('class=\"series\"'), + 'Should have series section'); + assert.ok(html.includes('Rage'), 'Should mention Rage series'); }); - test('card hover shows deeper shadow (--shadow-md)', () => { - assert.ok(html.includes('var(--shadow-md)'), 'Card hover should use --shadow-md token'); + test('has all-posts section with post entries', () => { + assert.ok(html.includes('class="all-posts"') || html.includes('class=\"all-posts\"'), + 'Should have all-posts section'); + const entryMatches = html.match(/class="post-entry"/g); + assert.ok(entryMatches, 'No post entries found'); + assert.ok(entryMatches.length >= 2, `Expected at least 2 post entries, got ${entryMatches.length}`); }); - test('card hover shows terracotta title (--accent-primary)', () => { - assert.ok( - html.includes(':hover .title') || html.includes(':hover .title{'), - 'Card hover should style the title' - ); - assert.ok(html.includes('var(--accent-primary)'), 'Title hover should use terracotta accent'); + test('uses terracotta accent (--accent-primary)', () => { + // Astro scoped styles compile to external CSS, check source instead + const source = readFileSync('src/pages/index.astro', 'utf-8'); + assert.ok(source.includes('var(--accent-primary)'), 'Should use terracotta accent in source'); }); - test('card links have correct URL format with slash separator', () => { - // The base is /crashtestdev and posts must be at /crashtestdev/posts/... + test('post links have correct URL format with slash separator', () => { const linkRegex = /href="([^"]*posts[^"]*)"/g; let match; let found = false; @@ -83,7 +89,7 @@ describe('Index page', () => { `URL "${url}" should not concatenate base and posts without slash` ); } - assert.ok(found, 'Should find article card links'); + assert.ok(found, 'Should find post links'); }); }); diff --git a/tests/index-page.test.mjs b/tests/index-page.test.mjs index af0ad9f..fcd1ae3 100644 --- a/tests/index-page.test.mjs +++ b/tests/index-page.test.mjs @@ -30,11 +30,6 @@ describe('Index page', () => { assert.match(content, /import\s+NavBar\s+from\s+['"].*NavBar\.astro['"]/, 'should import NavBar'); }); - it('should import ArticleCard', () => { - const content = readPage(); - assert.match(content, /import\s+ArticleCard\s+from\s+['"].*ArticleCard\.astro['"]/, 'should import ArticleCard'); - }); - it('should import getReadingTime', () => { const content = readPage(); assert.match(content, /import\s+.*getReadingTime.*from\s+['"].*reading-time['"]/, 'should import getReadingTime'); @@ -46,15 +41,89 @@ describe('Index page', () => { const content = readPage(); assert.match(content, /getCollection\s*\(\s*['"]posts['"]\s*\)/, 'should call getCollection("posts")'); }); + }); - it('should sort posts descending by date', () => { + describe('Hero section', () => { + it('should have a hero section', () => { const content = readPage(); - assert.match(content, /sort\s*\(/, 'should call sort'); - assert.match(content, /b\.data\.date\.getTime\(\)\s*-\s*a\.data\.date\.getTime\(\)/, 'should sort descending by date'); + assert.match(content, /class="hero"/, 'should have a section with class hero'); + }); + + it('should have the site title Crash Test Dev as a heading', () => { + const content = readPage(); + assert.match(content, /]*>Crash Test Dev<\/h1>/, 'should have h1 with site title'); + }); + + it('should have a tagline in prose font', () => { + const content = readPage(); + assert.match(content, /class="tagline"/, 'should have a tagline element'); + }); + + it('should have an author byline', () => { + const content = readPage(); + assert.match(content, /class="byline"/, 'should have a byline element'); }); }); - describe('Template', () => { + describe('Featured excerpt section', () => { + it('should have a featured excerpt section', () => { + const content = readPage(); + assert.match(content, /class="featured"/, 'should have a section with class featured'); + }); + + it('should contain an excerpt blockquote', () => { + const content = readPage(); + assert.match(content, /]*class="excerpt"/, 'should have a blockquote with class excerpt'); + }); + + 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'); + }); + }); + + describe('Series section', () => { + it('should have a series section', () => { + const content = readPage(); + assert.match(content, /class="series"/, 'should have a section with class series'); + }); + + it('should mention the Rage series', () => { + const content = readPage(); + assert.match(content, /Rage/, 'should mention Rage in the series section'); + }); + + it('should list planned posts', () => { + const content = readPage(); + assert.match(content, /class="series-item"/, 'should have series items'); + }); + + it('should indicate coming soon items', () => { + const content = readPage(); + assert.match(content, /coming soon/i, 'should indicate coming soon posts'); + }); + }); + + describe('All posts section', () => { + it('should have a posts listing section below the pitch', () => { + const content = readPage(); + assert.match(content, /class="all-posts"/, 'should have a section with class all-posts'); + }); + + it('should map posts to compact 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'); + }); + }); + + describe('Layout', () => { it('should use BaseLayout with title Crash Test Dev', () => { const content = readPage(); assert.match(content, / { const content = readPage(); assert.match(content, / { - const content = readPage(); - assert.match(content, / { - const content = readPage(); - assert.match(content, /sortedPosts\.map/, 'should map sortedPosts'); - assert.match(content, / { - const content = readPage(); - assert.match(content, /title\s*=\s*\{post\.data\.title\}/, 'should pass title'); - }); - - it('should pass date from post.data.date', () => { - const content = readPage(); - assert.match(content, /date\s*=\s*\{post\.data\.date\}/, 'should pass date'); - }); - - it('should pass type from post.data.type', () => { - const content = readPage(); - assert.match(content, /type\s*=\s*\{post\.data\.type\}/, 'should pass type'); - }); - - it('should pass summary from post.data.summary', () => { - const content = readPage(); - assert.match(content, /summary\s*=\s*\{post\.data\.summary\}/, 'should pass summary'); - }); - - it('should pass slug as post.id', () => { - const content = readPage(); - assert.match(content, /slug\s*=\s*\{post\.id\}/, 'should pass slug as post.id'); - }); - - it('should compute and pass readingTime from post.body', () => { - const content = readPage(); - assert.match(content, /getReadingTime\s*\(\s*post\.body/, 'should call getReadingTime(post.body...)'); - assert.match(content, /readingTime\s*=\s*\{/, 'should pass readingTime prop'); - }); }); describe('Style', () => { - it('should have .post-list with prose max-width', () => { + it('should use --width-prose for content width', () => { const content = readPage(); - assert.match(content, /\.post-list\s*\{/, 'should have .post-list selector'); - assert.match(content, /max-width\s*:\s*var\(--width-prose\)/, 'should have prose max-width'); + assert.match(content, /max-width\s*:\s*var\(--width-prose\)/, 'should constrain content to prose width'); }); - it('should have space-6 top and space-12 bottom margins', () => { + it('should use --font-structure for hero heading', () => { const content = readPage(); - assert.match(content, /margin-top\s*:\s*var\(--space-6\)/, 'should have space-6 top margin'); - assert.match(content, /margin-bottom\s*:\s*var\(--space-12\)/, 'should have space-12 bottom margin'); + assert.match(content, /\.hero[\s\S]*?font-family\s*:\s*var\(--font-structure\)/, 'should use structure font in hero'); }); - it('should have auto left and right margins', () => { + it('should use --font-prose for excerpt', () => { const content = readPage(); - assert.match(content, /margin-left\s*:\s*auto/, 'should have auto left margin'); - assert.match(content, /margin-right\s*:\s*auto/, 'should have auto right margin'); + assert.match(content, /\.excerpt[\s\S]*?font-family\s*:\s*var\(--font-prose\)/, 'should use prose font for excerpt'); }); - it('should have flex column display', () => { + it('should not have the old .post-list as the main layout', () => { const content = readPage(); - assert.match(content, /display\s*:\s*flex/, 'should have flex display'); - assert.match(content, /flex-direction\s*:\s*column/, 'should have column direction'); - }); - - it('should have space-2 gap', () => { - const content = readPage(); - assert.match(content, /gap\s*:\s*var\(--space-2\)/, 'should have space-2 gap'); + // The old flat list structure should be gone + assert.doesNotMatch(content, /