Production system failure postmortems, documented as criminal case files.
A static blog built with Astro 4.x + Tailwind CSS + TypeScript. Drop a single .md file into src/content/cases/ and the full UI renders automatically — zero per-post UI work.
# Install dependencies
npm install
# Start dev server
npm run dev # → http://localhost:4321
# Build for production
npm run build
# Preview production build
npm run previewThat's the whole DX — three steps:
-
Copy the template:
cp src/content/cases/_TEMPLATE.md src/content/cases/my-incident-slug.md
-
Fill in the frontmatter (the YAML block at the top). Every field drives UI automatically:
title,caseNumber,summary→ card + header + OG imagedomain→ filing cabinet tabseverity→ color-coded stamp (P0 red, P1 amber, P2 yellow, P3 blue)timeline→ animated red-thread timelineactionItems→ tri-color checklist with progress barevidence→ polaroid grid with lightboxsystems→ auto-links related cases
-
Write the body in Markdown/MDX below the frontmatter. Suggested sections:
## What happened## Root cause## What we did to fix it## What we should have done differently
That's it. Save the file, the dev server hot-reloads, and the case appears in the filing cabinet.
Set draft: true in any case file's frontmatter to hide it from production builds. Drafts:
- ✅ Visible in
npm run dev(local development) - ❌ Hidden in
npm run build(production)
This means you can commit work-in-progress cases to the repo without them appearing on the live site. When ready, just flip draft: false.
Push to main → Vercel auto-deploys.
The project is configured with:
vercel.json— build settings and OG image caching headers@astrojs/verceladapter — hybrid output (static SSG + server-side OG endpoint).nvmrc— pins Node.js 22
Vercel will:
- Install dependencies
- Run
npm run build - Deploy static HTML + one serverless function (OG image generation)
src/
├── content/
│ ├── config.ts ← Zod schema for case frontmatter
│ └── cases/
│ ├── _TEMPLATE.md ← Copy this to create a new case
│ └── *.md ← Your cases go here
├── components/
│ ├── case/ ← CaseCard, CaseHeader, MetaBar, Timeline, etc.
│ ├── index/ ← FilingCabinet, DomainTab, SeverityCalendar, SearchBar
│ └── global/ ← Nav, Footer, SEOHead
├── layouts/
│ ├── BaseLayout.astro ← HTML shell + dark mode + view transitions
│ └── CaseLayout.astro ← Case detail wrapper + reading progress bar
├── pages/
│ ├── index.astro ← Filing cabinet index
│ ├── cases/[slug].astro ← Case detail pages (auto-generated)
│ ├── domain/[domain].astro ← Domain-filtered views
│ └── api/og/[slug].ts ← OG image generation (SVG)
├── lib/ ← Utility functions (cases, severity, reading-time)
├── store/ ← Nanostores (read cases, bookmarks, filters)
├── styles/ ← Global CSS + Tailwind base
└── types/ ← TypeScript types
| Script | Description |
|---|---|
npm run dev |
Start dev server with hot reload |
npm run build |
Production build (SSG + serverless) |
npm run preview |
Preview production build locally |
npm run lint |
Lint with Biome |
npm run format |
Format with Biome |
npm run test |
Run tests with Vitest |
- Astro 4.x — Static site generation with content collections
- Tailwind CSS 3.x — Custom manila/stamp theme with dark mode
- TypeScript — Strict mode throughout
- Framer Motion — React islands for animations
- nanostores — Persistent state (read cases, bookmarks, filters)
- fuse.js — Client-side fuzzy search
- Biome — Linting + formatting
- Vitest — Unit testing