Portfolio Website

This is how I built my portfolio. I started with Mark Horn’s Astro Nano theme and heavily modified it — adding structured SEO data, a Mermaid diagram integration, and a UI redesign centered around a Cmd+K command palette.

My goal was a static site with just enough interactivity that visitors can navigate, search, and ask questions without needing to email me.

Architecture

The site uses Astro 6 to generate static HTML. A lightweight Cloudflare Worker handles the AI backend. All content is managed through Astro’s Content Layer API with typed Zod schemas.

Third-Party Services

Serverless Infrastructure

Astro Application

command / search mode

AI chat mode — Fetch API

Authenticated Request

Model Invocation

CommandPalette.astro

Static Routes

TableOfContents.astro

Head.astro

Cloudflare Worker

CF AI Gateway

Google Gemini API

Command Palette (Cmd+K)

The palette is a multi-module TypeScript system under src/lib/cmdk/ with three modes accessible from a single entry point:

ModeTriggerWhat it does
CommandDefault on openNavigate pages, toggle theme, jump to external links
SearchType → EnterFuzzy-search across all blog posts and projects
AI ChatCmd+EnterStreaming AI responses via Cloudflare Worker

The system is built as a state machine — state.ts owns the current mode and query; registry.ts holds the action definitions; renderer.ts reacts to state changes and repaints the list. Icons are centralized in icons.ts using Lucide SVGs and injected at init, keeping the Astro component markup clean.

open palette

Enter with query

Backspace on empty input

Cmd+Enter

Cmd+Enter

close / reopen

Command

Search

Chat

AI Chat Mode

The chat subsystem (src/lib/cmdk/chat/) handles streaming responses from a Cloudflare Worker. The worker acts as a proxy to Google Gemini with a rigid system prompt — it only discusses my professional background and refuses everything else. Responses stream back incrementally via TransformStream.

The Worker is also routed through Cloudflare’s AI Gateway for request caching (cf-aig-cache-ttl). Common questions are served from cache instead of hitting the Gemini API again.

Gemini APICF AI GatewayCF Worker (index.ts)Command Palette (chat mode)Gemini APICF AI GatewayCF Worker (index.ts)Command Palette (chat mode)POST / { message, history }1Validate CORS & env keys2Route through gateway3Process prompt4Stream output chunks5Forward chunks6TransformStream to client7

Table of Contents Sidebar

Long-form content (blog posts, project pages) gets an auto-generated sticky TOC sidebar (src/components/TableOfContents.astro). It reads the heading structure from the rendered content and highlights the active section as you scroll.

Mermaid Diagrams

Mermaid diagrams in fenced code blocks are rendered at build time using rehype-mermaid. This generates static SVGs that ship without any client-side JavaScript. Dark mode support uses CSS filters to invert the diagram colors when the theme changes.

Structured JSON-LD Data for SEO

src/lib/schema.ts builds Schema.org-compliant objects for blog posts and projects. During the build, Head.astro calls these functions and feeds the output to Schema.astro, which injects the JSON-LD block into the HTML <head> for rich search result indexing.

D3 Knowledge Graph

Each blog post page includes an embedded force-directed graph (ForceGraph.astro) that maps every blog post as a node and draws edges between posts that share tags. It gives a visual overview of which topics are related across the blog.

The graph is rendered client-side from a data payload generated at build time — Astro collects all posts and their tags, serializes the node/link data into the page, and D3 takes over in the browser to lay out and animate the simulation.

Content Layer API

The site uses Astro 6’s Content Layer API with Zod schemas to type-check all content at build time. Collections are queried via getCollection() and render() — no runtime Astro.glob() calls.