# 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}
``` **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 `