How I Built This
The decisions behind this portfolio site. What I chose, why, and what I deliberately left out.
If you Inspect Element the home page, this is the first thing you'll find in the <body> tag:
Good morning. Or afternoon. Or evening. This is static HTML so I genuinely don't know what time it is for you. But hi. Abe here. You're inspecting a personal site. I respect that. Let me walk you through what's under the hood.
The comment continues for about 60 lines. It explains the framework, the architecture, why CSS animations run the hero instead of JavaScript, and ends with a table:
┌─────────────────┬────────┬────────────────────────┐ │ Component │ Type │ Why it needs JS │ ├─────────────────┼────────┼────────────────────────┤ │ ResumeModal │ React │ open/close + animation │ │ SkillsSpotlight │ React │ SSE streaming + state │ │ SkillsTrigger │ React │ keyboard shortcuts │ ├─────────────────┼────────┼────────────────────────┤ │ ArtifactPanel │ .astro │ CSS + DOM only │ │ SelectionTooltip │ .astro │ CSS + DOM only │ │ ContactModal │ .astro │ CSS + DOM only │ └─────────────────┴────────┴────────────────────────┘ 3 islands. That's the entire JS footprint. The rest was finished before your browser woke up.
As you scroll through the markup, each section has its own note:
And the console has an API:
> abe.ai("your question") ask me anything, streamed to console
> abe.search() open the AI spotlight (or hit ⌘K)
> abe.contact() open the contact modalThat's what's in the source. This artifact explains the decisions that got it there.
The design system
I chose IBM's Carbon Design System, Gray-100 dark theme because it gave me a complete set of rules: spacing scale, type scale, color tokens, grid. That removed hundreds of small decisions I didn't want to make.
The trade-off is that it's austere. Seven colors, two typefaces (IBM Plex Sans and Plex Mono), no gradients, no shadows. The visual identity comes from typography weight and spacing rhythm, not decoration. The one override is the accent color: Carbon's default interactive blue replaced with #50FFB4, a mint green carried over from my previous portfolio. It cascades through every interactive state, link, and label. One color, one job: signal what's active or important.
The framework
This is a content site. Most of it is static text and layout. The question was: how much JavaScript does that actually require?
Astro's answer is zero by default. Every .astro component compiles to HTML at build time. The framework removes itself from the output. Interactive components opt in explicitly with client:load, creating what Astro calls "islands": isolated React components that hydrate independently. That table in the source comment above is the full list. Three React islands. Everything else was finished rendering before the browser's JavaScript engine initialized.
I could have built this in Next.js or Remix. I've used both. But they ship a client runtime whether you need it or not. For a site where 90% of the content is static prose, that felt like paying a tax for convenience I wasn't using. Astro let me keep the component model (composability, props, slots) without the runtime cost.
The content layer
Every piece of long-form writing on this site is an artifact. They live as .mdx files in a content collection with typed frontmatter validated at build time by a Zod schema. If a field is wrong, the build fails. The system supports two rendering paths: standard MDX for prose, and custom React components for interactive content. The Code Samples artifact renders side-by-side code comparisons with inline annotations. Banh Mi Ops has an animated pipeline diagram. Both paths flow through the same ArtifactBody.astro wrapper.
Featured artifacts also render in an off-canvas side panel on the home page. One shared component, two contexts, scoped CSS adjustments for the narrower panel column.
The build pipeline catches a few other things. A custom Astro plugin strips font preload links because Astro's default preloading conflicts with @fontsource. A post-build script verifies the Pagefind search index generated correctly. And the glossary system (inline definition tooltips you'll find on some artifacts) has the strictest check: every definition lives in a single TypeScript registry, referenced by slug. If the slug doesn't exist, the build fails.
Other decisions
A few choices that don't fit neatly into the sections above but shaped the site just as much.
That's how it's built
I used Claude Code as my primary development tool throughout this project, the same tool I built Banh Mi Ops to orchestrate. The methodology I describe in the other artifacts on this site is the same methodology I used to build the site itself. If you want to see how that looks in practice, the markup is open. Go inspect it.
STACK