- Killed 16 em-dashes (Claude fingerprint, 15.9/1000 words -> 0) - Replaced all clause-dash-elaboration patterns with periods, colons, restructuring - Removed 'The One-Sentence Version' section (restated the intro, voice anti-pattern) - Broke tricolon at lines 56-58 (too-clean parallel structure) - Collapsed 'How to Actually Help' listicle into connective prose - Added receipt link for ~100x inference cost claim - Heading dashes replaced with colons (Tradesman Analogy, Excellence vs Functional) New tooling: - scripts/lint-voice.sh: mechanical anti-slop linter (em-dashes, trigger words, hedging, filler, receipts, sentence uniformity) - .amplifier/skills/voice-check/SKILL.md: LLM-as-judge voice authenticity check (8 dimensions against VOICE.md profile) - .amplifier/AGENTS.md: standing rule requiring both checks before any publish 🤖 Generated with Amplifier Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
51 KiB
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
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
{
"name": "crashtestdev",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview"
}
}
Step 3: Install dependencies
npm install astro @astrojs/mdx @astrojs/rss
npm install -D typescript
Step 4: Create astro.config.mjs
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
{
"extends": "astro/tsconfigs/strict"
}
Step 6: Create src/env.d.ts
/// <reference types="astro/client" />
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:
---
---
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Crash Test Dev</title>
</head>
<body>
<h1>Crash Test Dev</h1>
<p>Site is being rebuilt.</p>
</body>
</html>
Step 9: Verify
npm run build
Expected: Build succeeds, output in dist/ directory. You should see something like:
building ...
✓ Completed in X.XXs.
Step 10: Commit
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):
: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
grep -c '\-\-' src/styles/tokens.css
Expected: A number around 60–70 (the count of CSS custom properties defined).
Step 3: Commit
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
#!/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
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
ls -la src/fonts/*.woff2 | wc -l
Expected: 8
If the download fails (network issues, API changes), download manually from Google Fonts and save to src/fonts/ using the same naming convention.
Step 4: Create src/styles/fonts.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
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
---
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;
---
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content={description} />
<title>{siteTitle}</title>
<link rel="alternate" type="application/rss+xml" title="Crash Test Dev" href={`${base}rss.xml`} />
</head>
<body>
<a href="#main-content" class="skip-link">Skip to content</a>
<slot name="nav" />
<main id="main-content">
<slot />
</main>
</body>
</html>
<style is:global>
/* Reset */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
font-size: 100%;
scroll-behavior: smooth;
}
body {
font-family: var(--font-prose);
font-size: var(--type-base);
line-height: var(--leading-prose);
color: var(--text-primary);
background-color: var(--surface-base);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Links - base treatment */
a {
color: var(--accent-primary);
text-decoration: none;
transition: color var(--duration-fast) var(--ease-default);
}
a:hover {
color: var(--accent-primary-hover);
text-decoration: underline;
}
a:focus-visible {
outline: 2px solid var(--focus-ring);
outline-offset: 2px;
}
/* Skip-to-content link */
.skip-link {
position: absolute;
top: -100%;
left: var(--space-2);
padding: var(--space-1) var(--space-2);
background: var(--surface-card);
color: var(--text-primary);
font-family: var(--font-structure);
font-size: var(--type-sm);
border-radius: var(--rounded-md);
z-index: 100;
text-decoration: none;
}
.skip-link:focus {
top: var(--space-1);
}
/* Page container */
main {
max-width: var(--width-container);
margin: 0 auto;
padding: 0 var(--gutter);
}
/* Selection */
::selection {
background-color: var(--accent-primary);
color: var(--surface-card);
}
</style>
Step 2: Update src/pages/index.astro to use the layout
Replace the entire file:
---
import BaseLayout from '../layouts/BaseLayout.astro';
---
<BaseLayout title="Crash Test Dev">
<div style="max-width: var(--width-prose); margin: var(--space-8) auto;">
<h1 style="font-family: var(--font-structure); font-size: var(--type-3xl); font-weight: 700; color: var(--text-primary); letter-spacing: -0.02em; line-height: var(--leading-tight);">
Crash Test Dev
</h1>
<p style="margin-top: var(--space-3); color: var(--text-secondary);">
Site rebuild in progress. Fonts, tokens, and layout are loading.
</p>
</div>
</BaseLayout>
Step 3: Verify
npm run build
Expected: Build succeeds. The output dist/index.html should contain the CSS custom properties and @font-face declarations inlined/linked.
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
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
---
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/`);
}
---
<nav aria-label="Site navigation">
<div class="nav-inner">
<a href={base} class="site-name">Crash Test Dev</a>
<div class="nav-links">
{navLinks.map(link => (
<a
href={link.href}
class:list={['nav-link', { active: isActive(link.href) }]}
aria-current={isActive(link.href) ? 'page' : undefined}
>
{link.label}
</a>
))}
</div>
</div>
</nav>
<style>
nav {
border-bottom: 1px solid var(--border-subtle);
position: static;
}
.nav-inner {
max-width: var(--width-container);
margin: 0 auto;
padding: var(--space-2) var(--gutter);
display: flex;
align-items: center;
justify-content: space-between;
}
.site-name {
font-family: var(--font-structure);
font-size: var(--type-base);
font-weight: 600;
color: var(--text-primary);
text-decoration: none;
letter-spacing: -0.01em;
transition: color var(--duration-fast) var(--ease-default);
}
.site-name:hover {
color: var(--accent-primary);
text-decoration: none;
}
.nav-links {
display: flex;
gap: var(--space-4);
}
.nav-link {
font-family: var(--font-structure);
font-size: var(--type-sm);
font-weight: 400;
color: var(--text-secondary);
text-decoration: none;
transition: color var(--duration-fast) var(--ease-default);
}
.nav-link:hover {
color: var(--text-primary);
text-decoration: none;
}
.nav-link.active {
font-weight: 500;
color: var(--text-primary);
text-decoration: underline;
text-decoration-color: var(--accent-primary);
text-decoration-thickness: 2px;
text-underline-offset: 6px;
}
/* Responsive: stack on narrow viewports */
@media (max-width: 599px) {
.nav-inner {
flex-direction: column;
gap: var(--space-1);
text-align: center;
}
.nav-links {
gap: var(--space-3);
}
}
</style>
Step 2: Update src/pages/index.astro to include the NavBar
Replace the entire file:
---
import BaseLayout from '../layouts/BaseLayout.astro';
import NavBar from '../components/NavBar.astro';
---
<BaseLayout title="Crash Test Dev">
<NavBar slot="nav" />
<div style="max-width: var(--width-prose); margin: var(--space-8) auto;">
<h1 style="font-family: var(--font-structure); font-size: var(--type-3xl); font-weight: 700; color: var(--text-primary); letter-spacing: -0.02em; line-height: var(--leading-tight);">
Crash Test Dev
</h1>
<p style="margin-top: var(--space-3); color: var(--text-secondary);">
Site rebuild in progress. NavBar, fonts, and tokens are working.
</p>
</div>
</BaseLayout>
Step 3: Verify
npm run build
Expected: Build succeeds. The HTML output should contain the <nav> element with "Crash Test Dev" and "Posts".
Step 4: Commit
git add src/components/NavBar.astro src/pages/index.astro
git commit -m "feat: NavBar component with active state and responsive layout"
Task 6: Build PostHeader component
Files:
- Create:
src/components/PostHeader.astro
Spec reference: .design/specs/components-2026-05-26.md section 5 (post-header).
Step 1: Create src/components/PostHeader.astro
---
interface Props {
title: string;
date: Date;
type?: string;
readingTime?: number;
}
const { title, date, type = 'post', readingTime } = Astro.props;
const formattedDate = date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
const isoDate = date.toISOString().split('T')[0];
---
<header class="post-header">
<span class="badge">{type}</span>
<h1 class="title">{title}</h1>
<p class="meta">
<time datetime={isoDate}>{formattedDate}</time>
{readingTime && <span> · {readingTime} min read</span>}
</p>
</header>
<style>
.post-header {
max-width: var(--width-prose);
margin: 0 auto;
padding-top: var(--space-8);
padding-bottom: var(--space-8);
text-align: center;
}
.badge {
display: inline-block;
font-family: var(--font-structure);
font-size: var(--type-xs);
font-weight: 600;
color: var(--accent-primary);
text-transform: uppercase;
letter-spacing: 0.05em;
line-height: var(--leading-normal);
}
.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-top: var(--space-2);
}
.meta {
font-family: var(--font-structure);
font-size: var(--type-sm);
font-weight: 400;
color: var(--text-tertiary);
line-height: var(--leading-normal);
margin-top: var(--space-2);
}
/* Responsive title sizing */
@media (max-width: 719px) {
.title {
font-size: var(--type-2xl);
line-height: var(--leading-snug);
}
.post-header {
padding-top: var(--space-6);
padding-bottom: var(--space-6);
}
}
@media (max-width: 479px) {
.title {
font-size: var(--type-xl);
line-height: var(--leading-snug);
}
.post-header {
padding-top: var(--space-5);
padding-bottom: var(--space-5);
}
}
</style>
Step 2: Verify
npm run build
Expected: Build succeeds. The component is not yet used on any page, but it compiles without errors.
Step 3: Commit
git add src/components/PostHeader.astro
git commit -m "feat: PostHeader component with responsive title sizing"
Task 7: Smoke test page and verify foundation
Files:
- Modify:
src/pages/index.astro(replace with a full smoke test)
Step 1: Replace src/pages/index.astro with a foundation test page
---
import BaseLayout from '../layouts/BaseLayout.astro';
import NavBar from '../components/NavBar.astro';
import PostHeader from '../components/PostHeader.astro';
---
<BaseLayout title="Crash Test Dev">
<NavBar slot="nav" />
<PostHeader
title="Speeding Up Webpack Typescript Incremental Builds by 7x"
date={new Date('2019-08-16')}
type="post"
readingTime={12}
/>
<div style="max-width: var(--width-prose); margin: 0 auto;">
<p>This is body text in Source Serif 4 (prose voice). It should be 18px with generous line-height.</p>
<p style="margin-top: var(--space-3); font-family: var(--font-structure); font-size: var(--type-sm); color: var(--text-secondary);">
This is meta text in Inter (structure voice). 14px, secondary color.
</p>
<p style="margin-top: var(--space-3); font-family: var(--font-evidence); color: var(--code-text); background: var(--surface-code); padding: var(--space-2); border-radius: var(--rounded-md); border-left: 3px solid var(--accent-primary);">
const evidence = "JetBrains Mono (evidence voice)";
</p>
</div>
</BaseLayout>
Step 2: Start dev server and verify visually
npm run dev
Open the URL shown (likely http://localhost:4321/crashtestdev/). Verify:
- Page background is warm off-white (#F5F0E8), not pure white
- Nav bar shows "Crash Test Dev" left, "Posts" right, with a subtle bottom border
- Post header shows "POST" badge in terracotta, large title, date + reading time
- Body text is in a serif font (Source Serif 4)
- Meta text is in a sans font (Inter)
- Code line is in a monospace font (JetBrains Mono) with warm code background
- The terracotta accent (#A0522D) appears on the badge and code block left border
Stop the dev server (Ctrl+C) after verification.
Step 3: Verify build
npm run build
Expected: Clean build with no errors.
Step 4: Commit
git add src/pages/index.astro
git commit -m "feat: smoke test page verifying foundation (fonts, tokens, layout)"
Phase 2: Content + Components
Task 8: Write prose styling for markdown content
Files:
- Create:
src/styles/prose.css
This is the full typographic treatment for rendered markdown content. Every value traces to a design token.
Spec references:
DESIGN.mdTypography and Layout sections.design/specs/design-tokens-2026-05-26.mdspacing.semantic values
Step 1: Create src/styles/prose.css
/* ============================================================
Prose Styling — Markdown content rendering
Applied via a .prose wrapper around <Content /> in post pages.
Every value traces to a CSS custom property from tokens.css.
============================================================ */
/* --- Headings (Structure voice: Inter) --- */
.prose h2 {
font-family: var(--font-structure);
font-size: var(--type-2xl);
font-weight: 600;
line-height: var(--leading-snug);
letter-spacing: -0.015em;
color: var(--text-primary);
margin-top: var(--space-8);
margin-bottom: var(--space-2);
}
.prose h3 {
font-family: var(--font-structure);
font-size: var(--type-xl);
font-weight: 600;
line-height: 1.3;
letter-spacing: -0.01em;
color: var(--text-primary);
margin-top: var(--space-5);
margin-bottom: var(--space-1-5);
}
.prose h4 {
font-family: var(--font-structure);
font-size: var(--type-lg);
font-weight: 600;
line-height: var(--leading-heading);
letter-spacing: -0.005em;
color: var(--text-primary);
margin-top: var(--space-4);
margin-bottom: var(--space-1);
}
/* --- Body text (Prose voice: Source Serif 4) --- */
.prose p {
margin-bottom: var(--space-3);
}
.prose > *:last-child {
margin-bottom: 0;
}
/* --- Links --- */
.prose a {
color: var(--accent-primary);
text-decoration: none;
transition: color var(--duration-fast) var(--ease-default);
}
.prose a:hover {
color: var(--accent-primary-hover);
text-decoration: underline;
}
/* --- Blockquotes --- */
.prose blockquote {
font-family: var(--font-prose);
font-size: 1.25rem;
font-style: italic;
line-height: var(--leading-relaxed);
color: var(--text-primary);
border-left: 3px solid var(--accent-primary);
padding-left: var(--space-3);
margin-top: var(--space-3);
margin-bottom: var(--space-3);
}
.prose blockquote p {
margin-bottom: var(--space-2);
}
.prose blockquote p:last-child {
margin-bottom: 0;
}
/* --- Lists --- */
.prose ul,
.prose ol {
padding-left: var(--space-3);
margin-bottom: var(--space-3);
}
.prose li {
margin-bottom: var(--space-1);
}
.prose li:last-child {
margin-bottom: 0;
}
.prose li > ul,
.prose li > ol {
margin-top: var(--space-1);
margin-bottom: 0;
}
/* --- Inline code (Evidence voice: JetBrains Mono) --- */
.prose code {
font-family: var(--font-evidence);
font-size: 0.9em;
font-weight: 400;
color: var(--code-text);
background-color: var(--surface-code);
padding: 0.15em 0.35em;
border-radius: var(--rounded-sm);
}
/* Reset inline code styles inside code blocks (Shiki handles those) */
.prose pre code {
font-size: 1rem;
background: none;
padding: 0;
border-radius: 0;
color: inherit;
}
/* --- Code blocks (breakout width) --- */
.prose pre {
/* Break out of prose column */
position: relative;
left: 50%;
transform: translateX(-50%);
width: min(var(--width-code), calc(100vw - var(--gutter) * 2));
max-width: var(--width-code);
/* Styling */
border: 1px solid var(--border-subtle);
border-left: 3px solid var(--accent-primary);
border-radius: var(--rounded-md);
padding: var(--space-3);
margin-top: var(--space-5);
margin-bottom: var(--space-5);
overflow-x: auto;
line-height: var(--leading-relaxed);
font-family: var(--font-evidence);
tab-size: 2;
}
/* Warm scrollbar for code blocks (where supported) */
.prose pre::-webkit-scrollbar {
height: 6px;
}
.prose pre::-webkit-scrollbar-track {
background: transparent;
}
.prose pre::-webkit-scrollbar-thumb {
background: var(--border-default);
border-radius: var(--rounded-full);
}
.prose pre::-webkit-scrollbar-thumb:hover {
background: var(--text-tertiary);
}
/* Focus for keyboard-scrollable code blocks */
.prose pre:focus-visible {
outline: 2px solid var(--focus-ring);
outline-offset: -2px;
}
/* --- Horizontal rules --- */
.prose hr {
border: none;
border-top: 1px solid var(--border-subtle);
margin: var(--space-6) auto;
max-width: 5rem;
}
/* --- Strong and emphasis --- */
.prose strong {
font-weight: 600;
}
.prose em {
font-style: italic;
}
/* --- Images (if any appear in content) --- */
.prose img {
max-width: 100%;
height: auto;
border-radius: var(--rounded-md);
margin-top: var(--space-3);
margin-bottom: var(--space-3);
}
/* --- Tables --- */
.prose table {
width: 100%;
border-collapse: collapse;
margin-top: var(--space-3);
margin-bottom: var(--space-3);
font-size: var(--type-sm);
}
.prose th {
font-family: var(--font-structure);
font-weight: 600;
text-align: left;
padding: var(--space-1) var(--space-2);
border-bottom: 2px solid var(--border-default);
color: var(--text-primary);
}
.prose td {
padding: var(--space-1) var(--space-2);
border-bottom: 1px solid var(--border-subtle);
color: var(--text-primary);
}
Step 2: Verify
npm run build
Expected: Build succeeds. The CSS file is valid (no import errors).
Step 3: Commit
git add src/styles/prose.css
git commit -m "feat: prose styling for markdown content (headings, code, lists, blockquotes)"
Task 9: Configure Shiki syntax highlighting with warm theme
Files:
- Modify:
astro.config.mjs(add shikiConfig with custom workbench theme)
Spec reference: .design/specs/components-2026-05-26.md section 2 (code-block), Syntax Highlighting Palette Direction table.
Step 1: Update astro.config.mjs
Replace the entire file:
import { defineConfig } from 'astro/config';
import mdx from '@astrojs/mdx';
// Custom Shiki theme: warm colors matching the workbench design system.
// Keywords = terracotta, strings = tarnished brass, comments = tertiary gray.
const workbenchTheme = {
name: 'workbench',
type: 'light',
colors: {
'editor.background': '#EEEAE2',
'editor.foreground': '#3D342E',
},
tokenColors: [
{
scope: ['comment', 'punctuation.definition.comment'],
settings: { foreground: '#948880', fontStyle: 'italic' },
},
{
scope: ['keyword', 'storage.type', 'storage.modifier', 'keyword.control', 'keyword.operator.expression'],
settings: { foreground: '#A0522D' },
},
{
scope: ['string', 'string.quoted', 'string.template'],
settings: { foreground: '#7D6C2F' },
},
{
scope: ['constant.numeric', 'constant.language'],
settings: { foreground: '#7D6C2F' },
},
{
scope: ['entity.name.function', 'support.function', 'meta.function-call'],
settings: { foreground: '#2B2421', fontStyle: 'bold' },
},
{
scope: ['entity.name.type', 'support.type', 'entity.name.class', 'support.class'],
settings: { foreground: '#B07051' },
},
{
scope: ['variable', 'variable.other', 'variable.parameter'],
settings: { foreground: '#3D342E' },
},
{
scope: ['punctuation', 'meta.brace', 'meta.bracket'],
settings: { foreground: '#6B5F55' },
},
{
scope: ['entity.name.tag', 'punctuation.definition.tag'],
settings: { foreground: '#A0522D' },
},
{
scope: ['entity.other.attribute-name'],
settings: { foreground: '#7D6C2F' },
},
{
scope: ['support.type.property-name', 'meta.object-literal.key'],
settings: { foreground: '#2B2421' },
},
{
scope: ['keyword.operator', 'keyword.operator.assignment'],
settings: { foreground: '#6B5F55' },
},
{
scope: ['meta.import', 'keyword.control.import', 'keyword.control.from', 'keyword.control.export'],
settings: { foreground: '#A0522D' },
},
],
};
export default defineConfig({
site: 'https://kenotron.github.io',
base: '/crashtestdev',
integrations: [mdx()],
markdown: {
shikiConfig: {
theme: workbenchTheme,
},
},
});
Step 2: Verify
npm run build
Expected: Build succeeds. The Shiki theme is registered and will be used for code blocks.
Step 3: Commit
git add astro.config.mjs
git commit -m "feat: custom Shiki 'workbench' theme with warm syntax colors"
Task 10: Build Callout component
Files:
- Create:
src/components/Callout.astro
Spec reference: .design/specs/components-2026-05-26.md section 4 (callout).
Step 1: Create src/components/Callout.astro
---
interface Props {
type?: 'note' | 'pattern' | 'antipattern' | 'warning' | 'tip';
}
const { type = 'note' } = Astro.props;
const config: Record<string, { borderColor: string; labelColor: string; label: string }> = {
note: { borderColor: 'var(--border-default)', labelColor: 'var(--text-secondary)', label: 'Note' },
pattern: { borderColor: 'var(--accent-primary)', labelColor: 'var(--accent-primary)', label: 'Pattern' },
antipattern: { borderColor: 'var(--error-border)', labelColor: 'var(--error-text)', label: 'Anti-pattern' },
warning: { borderColor: 'var(--warning-border)', labelColor: 'var(--warning-text)', label: 'Warning' },
tip: { borderColor: 'var(--accent-secondary)',labelColor: 'var(--accent-secondary)', label: 'Tip' },
};
const { borderColor, labelColor, label } = config[type];
---
<aside
class="callout"
role="note"
aria-label={`${label} callout`}
style={`border-left-color: ${borderColor};`}
>
<span class="callout-label" aria-hidden="true" style={`color: ${labelColor};`}>
{label}
</span>
<div class="callout-body">
<slot />
</div>
</aside>
<style>
.callout {
max-width: var(--width-prose);
background-color: var(--surface-inset);
border: 1px solid var(--border-subtle);
border-left-width: 3px;
border-radius: var(--rounded-lg);
padding: var(--space-3);
margin-top: var(--space-4);
margin-bottom: var(--space-4);
}
.callout-label {
display: block;
font-family: var(--font-structure);
font-size: var(--type-xs);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
line-height: var(--leading-normal);
margin-bottom: var(--space-1);
}
.callout-body {
font-family: var(--font-prose);
font-size: var(--type-base);
font-weight: 400;
line-height: var(--leading-prose);
color: var(--text-primary);
}
.callout-body :global(p) {
margin-bottom: var(--space-3);
}
.callout-body :global(p:last-child) {
margin-bottom: 0;
}
</style>
Step 2: Verify
npm run build
Expected: Build succeeds. The Callout component is available for use in MDX files.
Step 3: Commit
git add src/components/Callout.astro
git commit -m "feat: Callout component with 5 types (note, pattern, antipattern, warning, tip)"
Task 11: Set up content collection with schema
Files:
- Create:
src/content.config.ts - Create:
src/content/posts/directory (empty for now) - Create:
src/utils/reading-time.ts
Step 1: Create src/content.config.ts
import { defineCollection } from 'astro:content';
import { glob } from 'astro/loaders';
import { z } from 'astro/zod';
const posts = defineCollection({
loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/posts' }),
schema: z.object({
title: z.string(),
date: z.coerce.date(),
type: z.enum(['post', 'pattern', 'lesson']).default('post'),
tags: z.array(z.string()).default([]),
summary: z.string().optional(),
}),
});
export const collections = { posts };
Step 2: Create the posts directory
mkdir -p src/content/posts
Step 3: Create src/utils/reading-time.ts
/**
* Estimate reading time from raw markdown content.
* Average reading speed: 200 words per minute.
*/
export function getReadingTime(text: string): number {
const words = text.trim().split(/\s+/).length;
return Math.max(1, Math.ceil(words / 200));
}
Step 4: Verify
npm run build
Expected: Build succeeds. The content collection is configured but empty (no posts yet).
Step 5: Commit
git add src/content.config.ts src/content/posts/.gitkeep src/utils/reading-time.ts
# If .gitkeep doesn't work, the directory will be committed with posts in the next task
git commit -m "feat: content collection schema and reading time utility" || \
(touch src/content/posts/.gitkeep && git add -A && git commit -m "feat: content collection schema and reading time utility")
Task 12: Create post page template
Files:
- Create:
src/pages/posts/[...slug].astro
Step 1: Create src/pages/posts/[...slug].astro
---
import { getCollection, render } from 'astro:content';
import type { CollectionEntry } from 'astro:content';
import BaseLayout from '../../layouts/BaseLayout.astro';
import NavBar from '../../components/NavBar.astro';
import PostHeader from '../../components/PostHeader.astro';
import { getReadingTime } from '../../utils/reading-time';
import '../../styles/prose.css';
export async function getStaticPaths() {
const posts = await getCollection('posts');
return posts.map((post) => ({
params: { slug: post.id },
props: { post },
}));
}
interface Props {
post: CollectionEntry<'posts'>;
}
const { post } = Astro.props;
const { Content } = await render(post);
const readingTime = post.body ? getReadingTime(post.body) : undefined;
---
<BaseLayout title={post.data.title} description={post.data.summary}>
<NavBar slot="nav" />
<PostHeader
title={post.data.title}
date={post.data.date}
type={post.data.type}
readingTime={readingTime}
/>
<article class="prose-wrapper">
<div class="prose">
<Content />
</div>
</article>
</BaseLayout>
<style>
.prose-wrapper {
max-width: var(--width-prose);
margin: 0 auto;
padding-bottom: var(--space-12);
}
</style>
Step 2: Verify
npm run build
Expected: Build succeeds. No post pages are generated yet (no content in the collection). The template is ready.
Step 3: Commit
git add src/pages/posts/
git commit -m "feat: post page template with prose styling and reading time"
Task 13: Migrate existing posts
Files:
- Create:
src/content/posts/localize-react-without-bloating-the-bundle.md - Create:
src/content/posts/speeding-up-webpack-typescript-incremental-builds-by-7x.md
The two existing posts live at content/posts/ (old Gatsby location). They need updated frontmatter for the new schema. Key changes:
- Remove
path(Astro routes from filename) - Remove
heroImage(no hero images in new design) - Add
type,tags,summary - Simplify
datetoYYYY-MM-DD - Convert the
.mdxfile to.md(it doesn't use any MDX features)
Step 1: Create src/content/posts/localize-react-without-bloating-the-bundle.md
Copy from content/posts/localize-react-without-bloating-the-bundle.md but replace the frontmatter. The new frontmatter:
---
title: Localize React without Bloating the Bundle
date: 2019-08-16
type: post
tags: [react, localization, webpack, performance]
summary: A performant approach to React localization using dynamic imports and react-intl-universal, keeping your main bundle free of locale strings.
---
The body content stays exactly the same (everything after the original --- closing delimiter). Copy the body text verbatim — do not modify the prose.
Step 2: Create src/content/posts/speeding-up-webpack-typescript-incremental-builds-by-7x.md
Copy from content/posts/speeding-up-webpack-typescript-incremental-builds-by-7x.mdx but replace the frontmatter. The new frontmatter:
---
title: Speeding Up Webpack Typescript Incremental Builds by 7x
date: 2019-08-16
type: post
tags: [webpack, typescript, performance, microsoft]
summary: How the Outlook team at Microsoft cut incremental build times from 35s to 5s by defeating four performance enemies in the Webpack pipeline.
---
Same rule: body content stays verbatim. This was .mdx but contains no JSX/MDX syntax, so save as .md.
Step 3: Verify
npm run build
Expected: Build succeeds and generates two post pages. Look for output like:
▶ src/pages/posts/[...slug].astro
└─ ...posts/localize-react-without-bloating-the-bundle/index.html
└─ ...posts/speeding-up-webpack-typescript-incremental-builds-by-7x/index.html
Step 4: Commit
git add src/content/posts/
git commit -m "feat: migrate 2 existing posts to Astro content collection"
Task 14: Build ArticleCard component
Files:
- Create:
src/components/ArticleCard.astro
Spec reference: .design/specs/components-2026-05-26.md section 1 (article-card).
Step 1: Create src/components/ArticleCard.astro
---
interface Props {
title: string;
date: Date;
type: string;
summary?: string;
slug: string;
readingTime?: number;
}
const { title, date, type, summary, slug, readingTime } = Astro.props;
const base = import.meta.env.BASE_URL;
const formattedDate = date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
---
<article class="card" aria-label={`${type}: ${title}`}>
<a href={`${base}posts/${slug}/`} class="card-link">
<div class="badge-row">
<span class="badge">{type}</span>
{readingTime && <span class="reading-time">{readingTime} min read</span>}
</div>
<h2 class="title">{title}</h2>
{summary && <p class="summary">{summary}</p>}
<time datetime={date.toISOString().split('T')[0]}>{formattedDate}</time>
</a>
</article>
<style>
.card {
max-width: var(--width-prose);
}
.card-link {
display: block;
background: var(--surface-card);
border: 1px solid var(--border-subtle);
border-radius: var(--rounded-lg);
padding: var(--space-3);
box-shadow: var(--shadow-sm);
text-decoration: none;
color: inherit;
transition:
box-shadow var(--duration-fast) var(--ease-default),
transform var(--duration-fast) var(--ease-default);
}
.card-link:hover {
box-shadow: var(--shadow-md);
text-decoration: none;
}
.card-link:hover .title {
color: var(--accent-primary);
}
.card-link:active {
box-shadow: var(--shadow-sm);
transform: translateY(1px);
}
.card-link:focus-visible {
outline: 2px solid var(--focus-ring);
outline-offset: 2px;
}
.badge-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--space-1-5);
}
.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;
line-height: var(--leading-normal);
}
.reading-time {
font-family: var(--font-structure);
font-size: var(--type-xs);
font-weight: 400;
color: var(--text-tertiary);
line-height: var(--leading-normal);
}
.title {
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);
transition: color var(--duration-fast) var(--ease-default);
}
.summary {
font-family: var(--font-prose);
font-size: var(--type-base);
font-weight: 400;
color: var(--text-secondary);
line-height: var(--leading-prose);
margin-bottom: var(--space-2);
}
time {
font-family: var(--font-structure);
font-size: var(--type-sm);
font-weight: 400;
color: var(--text-tertiary);
line-height: var(--leading-normal);
}
</style>
Step 2: Verify
npm run build
Expected: Build succeeds.
Step 3: Commit
git add src/components/ArticleCard.astro
git commit -m "feat: ArticleCard component with hover states and card-on-desk styling"
Task 15: Build index page
Files:
- Modify:
src/pages/index.astro(replace smoke test with real index)
Step 1: Replace src/pages/index.astro
---
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()
);
---
<BaseLayout title="Crash Test Dev">
<NavBar slot="nav" />
<section class="post-list" aria-label="All posts">
{sortedPosts.map((post) => (
<ArticleCard
title={post.data.title}
date={post.data.date}
type={post.data.type}
summary={post.data.summary}
slug={post.id}
readingTime={post.body ? getReadingTime(post.body) : undefined}
/>
))}
</section>
</BaseLayout>
<style>
.post-list {
max-width: var(--width-prose);
margin: var(--space-6) auto var(--space-12);
display: flex;
flex-direction: column;
gap: var(--space-2);
}
</style>
Step 2: Verify
npm run build
Expected: Build succeeds. The index page lists both posts as ArticleCards.
Optionally preview:
npm run preview
Open the preview URL. Verify:
- Two article cards appear, sorted by date (newest first)
- Each card has: badge, title, summary, date
- Cards have warm off-white background with subtle shadow
- Hover shows deeper shadow and title turns terracotta
Step 3: Commit
git add src/pages/index.astro
git commit -m "feat: index page listing posts with ArticleCards"
Task 16: Add RSS feed
Files:
- Create:
src/pages/rss.xml.ts
Step 1: Create src/pages/rss.xml.ts
import rss from '@astrojs/rss';
import { getCollection } from 'astro:content';
import type { APIContext } from 'astro';
export async function GET(context: APIContext) {
const posts = await getCollection('posts');
const base = import.meta.env.BASE_URL;
return rss({
title: 'Crash Test Dev',
description: 'Technical publishing by Ken Chau',
site: context.site!,
items: posts
.sort((a, b) => b.data.date.getTime() - a.data.date.getTime())
.map((post) => ({
title: post.data.title,
pubDate: post.data.date,
description: post.data.summary ?? '',
link: `${base}posts/${post.id}/`,
})),
});
}
Step 2: Verify
npm run build
Expected: Build succeeds and generates an rss.xml file in the output. Check:
find dist -name 'rss.xml'
Expected: A path like dist/crashtestdev/rss.xml (or dist/rss.xml depending on base config).
Step 3: Commit
git add src/pages/rss.xml.ts
git commit -m "feat: RSS feed with @astrojs/rss"
Task 17: Configure GitHub Pages deployment
Files:
- Create:
.github/workflows/deploy.yml
The existing .github/workflows/azure-static-web-apps-salmon-sea-0f0c8771e.yml is for the old Azure deployment. Keep it for now (it won't trigger on the new branch). The new workflow deploys to GitHub Pages.
Step 1: Create .github/workflows/deploy.yml
name: Deploy to GitHub Pages
on:
push:
branches: [master]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- name: Install dependencies
run: npm ci
- name: Build site
run: npm run build
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: dist/
deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
Step 2: Verify
npm run build
Expected: Build succeeds. The workflow file is valid YAML. Deployment will activate when the branch is merged to master.
Step 3: Commit
git add .github/workflows/deploy.yml
git commit -m "ci: add GitHub Pages deployment workflow"
Task 18: Final verification and cleanup
Files:
- No new files. This is a verification and cleanup task.
Step 1: Run a clean build
rm -rf dist .astro
npm run build
Expected: Clean build with no errors and no warnings.
Step 2: Preview the site
npm run preview
Open the preview URL (likely http://localhost:4321/crashtestdev/). Walk through the full site:
- Index page: Two article cards. Warm off-white background. Cards have subtle shadow.
- Card interaction: Hover shows deeper shadow, title turns terracotta. Click navigates to post.
- Post page: NavBar at top with "Posts" link active (terracotta underline). Post header centered with badge, title, date, reading time. Prose content in serif font.
- Typography: Body text in Source Serif 4. Headings in Inter. Code in JetBrains Mono. All three voices are visually distinct.
- Code blocks: Break wider than prose. Warm background (#EEEAE2). 3px terracotta left border. Syntax colors are warm (terracotta keywords, brass strings).
- Colors: Page background is warm off-white (#F5F0E8), not pure white. Text is brown-black (#2B2421), not blue-black. Links are terracotta (#A0522D).
- Spacing: Generous vertical rhythm. H2s have big gaps above them. Code blocks have 40px vertical margins.
- Responsive: Narrow the browser. Nav stacks. Title size decreases. Code blocks respect gutters.
- RSS: Navigate to
{base}rss.xml. Should show valid XML with both posts.
Stop the preview (Ctrl+C).
Step 3: Lint DESIGN.md tokens
design.md lint DESIGN.md
Expected: 0 errors (same as before — the site implementation doesn't change DESIGN.md).
Step 4: Verify CSS token compliance
Spot-check that implemented CSS matches DESIGN.md tokens:
# Check that the surface-base color is used
grep -r '#F5F0E8' src/styles/tokens.css
# Check that all three font families are referenced
grep -c 'font-prose\|font-structure\|font-evidence' src/styles/tokens.css
Expected: Matches found for each check.
Step 5: Final commit
git add -A
git status
If there are any uncommitted changes, commit them:
git commit -m "chore: final verification cleanup"
Step 6: Push the branch
git push -u origin astro-rebuild
The branch is ready for review. When merged to master, the GitHub Pages workflow will deploy the site.
Summary
| Phase | Tasks | What you get |
|---|---|---|
| Phase 1: Foundation | Tasks 1–7 | Astro scaffold, design tokens, self-hosted fonts, BaseLayout, NavBar, PostHeader. A working page with correct fonts, colors, and layout. |
| Phase 2: Content + Components | Tasks 8–18 | Prose styling, warm Shiki theme, Callout, content collection, post template, 2 migrated posts, ArticleCard, index page, RSS, GitHub Pages deployment. A complete working blog. |
Total: 18 tasks across 2 phases.
What's deferred (future phases):
- Audio player component + TTS narration pipeline
- Patterns and Lessons content types (nav links, collection schemas)
- Search functionality
- Analytics
- Custom domain configuration