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:
prefers-color-scheme— resolved vialight-dark()on every semantic token. No[data-theme]selectors duplicating values.prefers-contrast: more— tightens text/bg to near-black/white and strengthens borders. Same tokens, different resolved values.forced-colors: active— hands control to the OS via system color keywords (Canvas,CanvasText,LinkText,Highlight). Tested under Windows High Contrast.
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
- Keyboard reachable on every interactive surface. Visible
:focus-visiblering in--color-focus. No mouse-only affordances. - SVG content gets
role="img"+ descriptivearia-label. Tooltips mirror intoaria-live="polite"regions. - Every animation is gated on
prefers-reduced-motion: no-preference, and reduced-motion is the default in CI's Lighthouse runs. - Color contrast minimums met against both light and dark backgrounds, enforced by Lighthouse's a11y category (axe-core under the hood) at score ≥ 98 on every PR.
- Semantic HTML first:
<nav>,<main>,<article>, real<time datetime>elements, real<button type>on every clickable.
7. Performance budgets
Targets, enforced in CI on every pull request. Failures block the merge:
- LCP < 1.5s
- INP < 100ms
- CLS < 0.02
- Home JS < 8 KB gzipped
- Home CSS < 12 KB gzipped
- Lighthouse performance & SEO & accessibility ≥ 98 on
/,/work,/work/linkedin-coach
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":
- Headless CMS — Payload or Sanity, with the Zod schemas mapped to their type systems. Editorial workflow with preview environments, scheduled publishes, author-driven revisions.
- Real observability — RUM (Real User Monitoring), error tracking, a perf budget dashboard that fires alerts when LCP drifts. Sentry or Datadog.
- CDN strategy — edge caching with stale-while-revalidate, per-route cache rules, signed URLs for previews.
- Internationalization vendor — TMS integration (Phrase, Lokalise) so translators see strings in context, not in spreadsheets.
- Design-system release pipeline — semver, changelog, release notes per package, codemods for breaking changes.
- A11y in CI as a hard gate — not just axe-core green, but accessibility-tree diffs against a baseline, screen-reader walkthroughs scripted with NVDA + VoiceOver in CI.
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.