Colophon

This is the architecture-decisions log. Every load-bearing choice that shaped the site, with the rationale next to it. Read it the way you'd read a senior engineer's design doc — the *why* is the interesting part.

1. Why this site exists

A personal site should be the thing itself — every architectural decision visible in the source, every interaction considered, every constraint named. The brief I gave myself: build the site I'd want to read if I were trying to hire me. That meant prose over portfolio bullets, opinions over feature lists, and a colophon long enough to be load-bearing.

2. Stack

Astro 5, TypeScript strict, MDX, sitemap, View Transitions. Static output, zero JS by default — every interactive surface opts in as a vanilla TS island with a measured budget. I picked Astro over Next.js deliberately: the site is content, not application; islands beat hydration for surfaces this small; the "static-first, opt-in dynamic" posture matches the way I think about most marketing sites. The cases where I'd reach for Next.js — Server Actions, RSC streaming, middleware-heavy routing — aren't in evidence here.

Package manager: pnpm, pinned via packageManager + Corepack. Content-addressable store, strict peer-dep resolution, faster CI installs. Lockfile is single source of truth.

3. Content model

Two content collections — work and writing — defined in src/content.config.ts via Astro 5's glob loader. Frontmatter is validated at build time by Zod, so a misshapen MDX file fails astro build with a clear error rather than failing silently in a template at runtime. The shape:

const work = defineCollection({
  loader: glob({ pattern: "**/*.{md,mdx}", base: "./src/content/work" }),
  schema: ({ image }) =>
    z.object({
      title: z.string().min(1),
      summary: z.string().min(1).max(280),
      heroImage: image().optional(),
      tags: z.array(z.string()).default([]),
      date: z.coerce.date(),
      featured: z.boolean().default(false),
      role: z.string().optional(),
      company: z.string().optional(),
      period: z.string().optional(),
    }),
});

This is the headless-CMS-thinking artifact. The schema is the contract — anything that consumes a case study (the index, the home page, OG generation, future search) reads from the same types. Tomorrow's Payload / Sanity / Contentful migration is a loader swap, not a rewrite.

4. Design token architecture

Three layers: primitives → semantic → component. Primitives are raw hex (--terracotta-60: #c2625e), semantic tokens consume primitives via light-dark() (--color-action: var(--terracotta-60)), components consume semantic. There is no --blue- anywhere in the codebase — a button doesn't ask for blue, it asks for "action." That's the "thinking in colors → thinking in semantics" thesis, in code.

Cascade is layered @layer reset, theme, base, components, atoms; — same dialect as my open-source Trellis starter so the vocabulary reads across both codebases.

5. Color modes

Three preferences, one token model:

All three live in src/styles/theme/semantic.css. Adding a new surface means writing one set of declarations; the four modes resolve automatically.

6. Accessibility commitments

7. Performance budgets

Targets, enforced in CI on every pull request. Failures block the merge:

The gate lives in .github/workflows/ci.yml. Bundle size is measured by a small Node script (scripts/bundle-size.mjs) that gzips every inline and external asset on the home page; Lighthouse runs three times per route via @lhci/cli with the desktop preset. The thresholds aren't aspirational — a regression fails the PR before review.

Currently: home ships one external JS chunk (Astro's ClientRouter for View Transitions, ~5.3 KB gz) plus small inline scripts (theme toggle, hero pulse). Fonts are self-hosted woff2, subset to Latin + Latin-extended only — exactly six woff2 files in the bundle.

8. Localization model

Astro's native i18n routing, English default + Spanish at /es. Content is per-locale in sibling collections (work + work-es) — a Spanish page is a real translation, not an injected dictionary lookup. Pages without a Spanish version fall back to English behind a visible ribbon ("Esta página aún no está traducida — mostrando la versión en inglés") so a reader is never silently served the wrong language. The locale switcher preserves the current pathname across switches.

Spanish ships at launch as a craft demonstration, not as a market move — translating my own work into my own heritage language is something I should be doing regardless.

9. AI-powered tooling

I ship small open-source tools — mostly MCP servers, Claude skills, and Claude Code plugins — that make designers and engineers faster. The pattern: a problem I hit twice on a Tuesday becomes an open-source package by Friday. The current list:

brio-mcp
A Figma plugin paired with an MCP server. Lets Claude read, create, and edit designs in Figma directly, so the handoff between design and code stops losing context at every step.
cascade
A CLI and TypeScript library that gives AI agents a CSS-inspired cascade for context — org, project, feature, task — with scoping, inheritance, and specificity. Replaces the pile of scattered instruction files with a formal model.
carn
A CLI plus MCP server that stores typed, scoped notes on a dedicated git branch so agents and teammates can share work-in-progress context. Surfaces constraints and in-flight decisions before someone steps on them.
trellis
A desktop app for running multiple AI coding sessions in parallel across different workspaces, with integrated code review and git management. The player-coach view of an agent fleet.
redline
A terminal UI for reviewing Claude Code plans. Select a range of text, attach a comment, send the structured feedback back to Claude — no more arguing with long-form output in a single chat box.
abc (always be cooking)
A Claude Code plugin that drives a feature end to end — planning, issues, parallel implementation, review, merge — through Linear or GitHub Issues. The state lives in the tracker, not the chat, so you can invoke a slash command and walk away.
claude-time-context
A one-line bash hook that injects the current timestamp into Claude Code at session start, so long-running sessions stop hallucinating about now.

10. What I'd do at production scale

Honest list of "this is a portfolio, here's how it would change at production scale":

11. Colophon (literal)

Built in Astro 5. Type set in Fraunces (display) + Inter (body); numerals + metadata in JetBrains Mono. Self-hosted, OFL. Source on GitHub — MIT. Deployed by GitHub Actions to GitHub Pages. Contribution-graph data syncs nightly via a GraphQL workflow under .github/workflows/sync-contributions.yml.

I learn every day.