# Crash Test Dev Blog Rebuild — Implementation Plan
> **Execution:** Use the subagent-driven-development workflow to implement this plan.
**Goal:** Replace the Gatsby 2 blog with a modern Astro 6 site implementing the workbench design system from DESIGN.md.
**Architecture:** Static site generated by Astro 6. CSS custom properties from the design token system (no Tailwind, no preprocessors). Self-hosted fonts (Source Serif 4, Inter, JetBrains Mono). Markdown/MDX content collections with Zod-validated frontmatter. Three typographic voices: serif for prose, sans for structure, mono for evidence. Code blocks break wider than prose to create the narrow-wide-narrow scroll rhythm.
**Tech Stack:** Astro 6, TypeScript, @astrojs/mdx, @astrojs/rss, Shiki (built-in)
**Design references (read before implementing):**
- `DESIGN.md` — All token values and component descriptions
- `.design/specs/design-tokens-2026-05-26.md` — CSS custom properties block (lines 459–563), font stacks, spacing semantics
- `.design/specs/components-2026-05-26.md` — Six component specs with exact token references
---
## Phase 1: Foundation
### Task 1: Create branch and scaffold Astro project
**Files:**
- Remove: `gatsby-config.js`, `yarn.lock`, `src/gatsby-theme-blog/` (entire directory)
- Create: `package.json`, `astro.config.mjs`, `tsconfig.json`, `src/env.d.ts`
- Modify: `.gitignore`
**Step 1: Create the branch and remove Gatsby files**
```bash
cd /home/ken/workspace/crashtestdev
git checkout -b astro-rebuild
rm gatsby-config.js yarn.lock
rm -rf src/gatsby-theme-blog
rm -rf node_modules
```
**Step 2: Create `package.json`**
```json
{
"name": "crashtestdev",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview"
}
}
```
**Step 3: Install dependencies**
```bash
npm install astro @astrojs/mdx @astrojs/rss
npm install -D typescript
```
**Step 4: Create `astro.config.mjs`**
```js
import { defineConfig } from 'astro/config';
import mdx from '@astrojs/mdx';
export default defineConfig({
// Update these for your deployment:
// - For project site: site = 'https://kenotron.github.io', base = '/crashtestdev'
// - For custom domain: site = 'https://yourdomain.com', remove base
site: 'https://kenotron.github.io',
base: '/crashtestdev',
integrations: [mdx()],
});
```
**Step 5: Create `tsconfig.json`**
```json
{
"extends": "astro/tsconfigs/strict"
}
```
**Step 6: Create `src/env.d.ts`**
```ts
///
```
**Step 7: Update `.gitignore`**
Replace the "gatsby files" section at the bottom of `.gitignore`. Find these lines:
```
# gatsby files
.cache/
public
```
Replace with:
```
# astro files
dist/
.astro/
```
**Step 8: Create a minimal page so the build succeeds**
Create `src/pages/index.astro`:
```astro
---
---
Crash Test Dev
Crash Test Dev
Site is being rebuilt.
```
**Step 9: Verify**
```bash
npm run build
```
Expected: Build succeeds, output in `dist/` directory. You should see something like:
```
building ...
✓ Completed in X.XXs.
```
**Step 10: Commit**
```bash
git add -A
git commit -m "feat: scaffold Astro project, remove Gatsby"
```
---
### Task 2: Add design tokens as CSS custom properties
**Files:**
- Create: `src/styles/tokens.css`
**Step 1: Create `src/styles/tokens.css`**
This is the exact CSS custom properties block from `.design/specs/design-tokens-2026-05-26.md` (lines 459–563):
```css
:root {
/* Surfaces */
--surface-base: #F5F0E8;
--surface-card: #FEFCF7;
--surface-code: #EEEAE2;
--surface-inset: #E8E3DA;
/* Text */
--text-primary: #2B2421;
--text-secondary: #6B5F55;
--text-tertiary: #948880;
/* Accents */
--accent-primary: #A0522D;
--accent-primary-hover: #8B4726;
--accent-secondary: #7D6C2F;
--accent-secondary-hover: #6B5C28;
/* Code */
--code-text: #3D342E;
/* Borders */
--border-subtle: #DDD7CD;
--border-default: #C2BAA9;
--border-strong: #8E857A;
/* Focus */
--focus-ring: #A0522D;
/* State: Success */
--success-bg: #E8EDDF;
--success-text: #4A5E3A;
--success-border: #B5C4A0;
/* State: Warning */
--warning-bg: #F2E8D0;
--warning-text: #7A5C1F;
--warning-border: #D4BC7C;
/* State: Error */
--error-bg: #F2DED6;
--error-text: #8B3A2A;
--error-border: #CFA090;
/* State: Info */
--info-bg: #E5E2DA;
--info-text: #4A4540;
--info-border: #B8B2A8;
/* Shadows */
--shadow-sm: 0 1px 3px rgba(43, 36, 33, 0.04);
--shadow-md: 0 2px 8px rgba(43, 36, 33, 0.06);
/* Typography families */
--font-prose: 'Source Serif 4', 'Source Serif Pro', Georgia, 'Times New Roman', serif;
--font-structure: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--font-evidence: 'JetBrains Mono', 'Fira Code', 'SF Mono', Consolas, monospace;
/* Typography scale */
--type-xs: 0.75rem;
--type-sm: 0.875rem;
--type-base: 1.125rem;
--type-lg: 1.375rem;
--type-xl: 1.75rem;
--type-2xl: 2.25rem;
--type-3xl: 2.75rem;
/* Line heights */
--leading-tight: 1.15;
--leading-snug: 1.25;
--leading-heading: 1.35;
--leading-normal: 1.5;
--leading-relaxed: 1.6;
--leading-prose: 1.7;
/* Spacing */
--space-0: 0;
--space-px: 1px;
--space-0-5: 0.25rem;
--space-1: 0.5rem;
--space-1-5: 0.75rem;
--space-2: 1rem;
--space-2-5: 1.25rem;
--space-3: 1.5rem;
--space-4: 2rem;
--space-5: 2.5rem;
--space-6: 3rem;
--space-8: 4rem;
--space-10: 5rem;
--space-12: 6rem;
/* Border radius */
--rounded-none: 0;
--rounded-sm: 2px;
--rounded-md: 4px;
--rounded-lg: 6px;
--rounded-xl: 8px;
--rounded-full: 9999px;
/* Layout */
--width-prose: 45rem;
--width-code: 52.5rem;
--width-container: 68.75rem;
--gutter: 1.5rem;
/* Motion */
--duration-fast: 100ms;
--duration-normal: 150ms;
--duration-slow: 250ms;
--ease-default: cubic-bezier(0.25, 0.1, 0.25, 1.0);
--ease-in-out: cubic-bezier(0.45, 0.05, 0.55, 0.95);
}
@media (prefers-reduced-motion: reduce) {
:root {
--duration-fast: 0ms;
--duration-normal: 0ms;
--duration-slow: 0ms;
}
}
```
**Step 2: Verify the file exists and has the right token count**
```bash
grep -c '\-\-' src/styles/tokens.css
```
Expected: A number around 60–70 (the count of CSS custom properties defined).
**Step 3: Commit**
```bash
git add src/styles/tokens.css
git commit -m "feat: add design tokens as CSS custom properties"
```
---
### Task 3: Download and self-host fonts
**Files:**
- Create: `scripts/download-fonts.py`
- Create: `src/fonts/` (8 woff2 files, created by the script)
- Create: `src/styles/fonts.css`
Fonts are placed in `src/fonts/` (not `public/fonts/`) so Vite processes them at build time. This ensures font URLs work correctly regardless of the `base` path configuration.
**Step 1: Create the font download script at `scripts/download-fonts.py`**
```python
#!/usr/bin/env python3
"""Download Google Fonts woff2 files for self-hosting (latin subset only)."""
import re
import os
import urllib.request
FONTS_DIR = os.path.join(os.path.dirname(__file__), "..", "src", "fonts")
os.makedirs(FONTS_DIR, exist_ok=True)
GOOGLE_FONTS_URL = (
"https://fonts.googleapis.com/css2?"
"family=Source+Serif+4:ital,wght@0,400;0,600;1,400"
"&family=Inter:wght@400;500;600;700"
"&family=JetBrains+Mono:wght@400"
"&display=swap"
)
headers = {
"User-Agent": (
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
)
}
req = urllib.request.Request(GOOGLE_FONTS_URL, headers=headers)
css = urllib.request.urlopen(req).read().decode()
# Parse: /* subset */ @font-face { ... } — keep only 'latin' blocks
pattern = r'/\*\s*([\w-]+)\s*\*/\s*@font-face\s*\{([^}]+)\}'
blocks = re.findall(pattern, css)
downloaded = []
for subset, block in blocks:
if subset != "latin":
continue
family_m = re.search(r"font-family:\s*'([^']+)'", block)
weight_m = re.search(r"font-weight:\s*(\d+)", block)
style_m = re.search(r"font-style:\s*(\w+)", block)
url_m = re.search(r"url\(([^)]+\.woff2)\)", block)
if not all([family_m, weight_m, url_m]):
continue
family = family_m.group(1).lower().replace(" ", "-")
weight = weight_m.group(1)
style = style_m.group(1) if style_m else "normal"
suffix = "-italic" if style == "italic" else ""
filename = f"{family}-{weight}{suffix}.woff2"
filepath = os.path.join(FONTS_DIR, filename)
print(f"Downloading {filename}...")
urllib.request.urlretrieve(url_m.group(1), filepath)
downloaded.append(filename)
print(f"\nDownloaded {len(downloaded)} font files to src/fonts/:")
for f in sorted(downloaded):
print(f" {f}")
```
**Step 2: Run the script**
```bash
python3 scripts/download-fonts.py
```
Expected output:
```
Downloading source-serif-4-400.woff2...
Downloading source-serif-4-600.woff2...
Downloading source-serif-4-400-italic.woff2...
Downloading inter-400.woff2...
Downloading inter-500.woff2...
Downloading inter-600.woff2...
Downloading inter-700.woff2...
Downloading jetbrains-mono-400.woff2...
Downloaded 8 font files to src/fonts/:
inter-400.woff2
inter-500.woff2
inter-600.woff2
inter-700.woff2
jetbrains-mono-400.woff2
source-serif-4-400-italic.woff2
source-serif-4-400.woff2
source-serif-4-600.woff2
```
**Step 3: Verify the files exist**
```bash
ls -la src/fonts/*.woff2 | wc -l
```
Expected: `8`
If the download fails (network issues, API changes), download manually from [Google Fonts](https://fonts.google.com/) and save to `src/fonts/` using the same naming convention.
**Step 4: Create `src/styles/fonts.css`**
```css
/* Source Serif 4 — Prose voice (Ken's voice on the page) */
@font-face {
font-family: 'Source Serif 4';
src: url('../fonts/source-serif-4-400.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Source Serif 4';
src: url('../fonts/source-serif-4-600.woff2') format('woff2');
font-weight: 600;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Source Serif 4';
src: url('../fonts/source-serif-4-400-italic.woff2') format('woff2');
font-weight: 400;
font-style: italic;
font-display: swap;
}
/* Inter — Structure voice (headings, UI, navigation) */
@font-face {
font-family: 'Inter';
src: url('../fonts/inter-400.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Inter';
src: url('../fonts/inter-500.woff2') format('woff2');
font-weight: 500;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Inter';
src: url('../fonts/inter-600.woff2') format('woff2');
font-weight: 600;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Inter';
src: url('../fonts/inter-700.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
/* JetBrains Mono — Evidence voice (code, proof, receipts) */
@font-face {
font-family: 'JetBrains Mono';
src: url('../fonts/jetbrains-mono-400.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}
```
**Step 5: Commit**
```bash
git add scripts/download-fonts.py src/fonts/ src/styles/fonts.css
git commit -m "feat: self-host fonts (Source Serif 4, Inter, JetBrains Mono)"
```
---
### Task 4: Create BaseLayout component
**Files:**
- Create: `src/layouts/BaseLayout.astro`
- Modify: `src/pages/index.astro` (update to use layout)
**Step 1: Create `src/layouts/BaseLayout.astro`**
```astro
---
import '../styles/tokens.css';
import '../styles/fonts.css';
interface Props {
title: string;
description?: string;
}
const {
title,
description = 'Technical publishing by Ken Chau',
} = Astro.props;
const siteTitle = title === 'Crash Test Dev' ? title : `${title} — Crash Test Dev`;
const base = import.meta.env.BASE_URL;
---
{siteTitle}
Skip to content
```
**Step 2: Update `src/pages/index.astro` to use the layout**
Replace the entire file:
```astro
---
import BaseLayout from '../layouts/BaseLayout.astro';
---
Crash Test Dev
Site rebuild in progress. Fonts, tokens, and layout are loading.
```
**Step 3: Verify**
```bash
npm run build
```
Expected: Build succeeds. The output `dist/index.html` should contain the CSS custom properties and @font-face declarations inlined/linked.
```bash
grep -c 'Source Serif' dist/crashtestdev/index.html 2>/dev/null || grep -c 'Source Serif' dist/index.html
```
Expected: At least 1 match (confirming fonts are included in the build output).
**Step 4: Commit**
```bash
git add src/layouts/BaseLayout.astro src/pages/index.astro
git commit -m "feat: base layout with tokens, fonts, and global styles"
```
---
### Task 5: Build NavBar component
**Files:**
- Create: `src/components/NavBar.astro`
- Modify: `src/pages/index.astro` (add NavBar)
Spec reference: `.design/specs/components-2026-05-26.md` section 6 (nav-bar).
**Step 1: Create `src/components/NavBar.astro`**
```astro
---
const currentPath = Astro.url.pathname;
const base = import.meta.env.BASE_URL;
const navLinks = [
{ label: 'Posts', href: base },
];
function isActive(href: string): boolean {
// "Posts" is active on the index and any post page
return currentPath === href || currentPath.startsWith(`${base}posts/`);
}
---
```
**Step 2: Update `src/pages/index.astro` to include the NavBar**
Replace the entire file:
```astro
---
import BaseLayout from '../layouts/BaseLayout.astro';
import NavBar from '../components/NavBar.astro';
---
Crash Test Dev
Site rebuild in progress. NavBar, fonts, and tokens are working.
```
**Step 3: Verify**
```bash
npm run build
```
Expected: Build succeeds. The HTML output should contain the `