The Problem With Static Portfolios
Traditional developer portfolios face a structural tension: they need to demonstrate technical sophistication while remaining accessible to non-technical recruiters. Static resume PDFs lack interactivity. GitHub profiles show code but not context. LinkedIn emphasizes credentials over capabilities.
I wanted something that served multiple audiences simultaneously. Recruiters browsing project cards and demo videos. Technical evaluators digging into tutorials and architectural decisions. Developers playing with a terminal interface that reveals personality alongside competence. This platform is the result of that constraint — multiple entry points, same codebase, each one appropriate for a different kind of reader.
What the Platform Does
The terminal interface is the most distinctive piece. It’s a fully functional browser-based terminal built with React and Xterm.js that responds to Unix-style commands: about for a bio, skills to browse technical competencies, projects to explore featured work, resume for formatted experience. Command history navigation, tab completion hints, blinking cursor animation. It makes exploring the portfolio feel like using a tool rather than reading a brochure.
The content architecture manages five distinct content types — projects, tutorials, blog posts, categories, and author profiles — with full TypeScript type inference through Astro’s Content Collections API and Zod schema validation. Every project links to related tutorials, tutorials reference parent projects. The schema defines required fields (title, description, publish date) and optional metadata (featured status, difficulty level, estimated reading time). If you forget the publishDate or misspell a category reference, the build fails with a clear error message. Broken links and missing metadata get caught before deployment.
The 3D particle background is a Three.js-powered animated canvas with 2000 particles (500 on mobile) in a developer-themed color palette that responds to mouse movement. It includes FPS tracking and automatic quality degradation when frame rates drop below 30fps — adaptive, not just decorative.
Search is powered by Pagefind, which indexes the entire site at build time and loads only when the user opens the search dialog. Zero JavaScript until needed. Results appear instantly without server round-trips. You get the user experience of dynamic search with the performance and hosting simplicity of static assets.
Why Astro
Astro’s Islands Architecture solves a real problem for content-heavy sites: JavaScript bloat. Traditional React frameworks hydrate the entire page, shipping megabytes of JavaScript even for static content. Astro renders pages to static HTML by default, only hydrating interactive components when needed. The terminal loads immediately with client:load because it’s the page’s primary interaction. The 3D background uses client:idle to defer rendering until the main thread is free.
The result is a Time to Interactive under 2 seconds despite having a WebGL canvas and terminal emulator. For a portfolio site where first impressions matter, this performance advantage is decisive.
Content Collections over a headless CMS gives the developer experience of structured content and type safety without the operational overhead of running a database or API server. The Zod schemas in content.config.ts define a contract. TypeScript enforces it at build time. The alternative — fetching content from Contentful or Strapi — introduces network latency, API rate limits, and deployment complexity that a portfolio with infrequent content updates doesn’t need.
Three.js over CSS animations for the background was necessary. CSS animations can create impressive effects, but they’re limited to 2D transforms and predefined keyframes. The particle system requires 3D spatial positioning, dynamic camera movement based on mouse coordinates, and per-particle color variation. The implementation uses BufferGeometry with typed arrays for particle positions and colors, achieving 60fps with 2000 particles through GPU parallelism. The adaptive quality system that reduces particle size, lowers opacity, and decrements pixel ratio when FPS drops below 30 is something CSS animations simply can’t provide.
The Interesting Engineering Problems
Bridging Astro’s static generation with React’s dynamic state. Astro components render to HTML at build time; React components maintain runtime state. The terminal needs to persist command history across user interactions, but Astro’s build process doesn’t have access to future user input. The architecture separates concerns cleanly: terminal.astro defines the static shell, while TerminalPortfolio.tsx manages all dynamic behavior. The terminal component maintains command history, output history, and current input as three distinct state pieces. Command history navigation uses a separate historyIndex state variable that tracks position in the history array — arrow-up increments it, arrow-down decrements, and pressing Enter or typing resets it to -1.
Performance optimization for 2000 particles. The naive approach — 2000 individual mesh objects — would overwhelm the scene graph with draw calls. The optimized implementation uses a single Points object with BufferGeometry, storing all particle positions in a flat Float32Array of 6000 elements. Spherical coordinate distribution (theta, phi, radius) avoids the clustering at the poles that Cartesian random generation produces. The FPS monitoring system updates every second by counting frames and pushing to a rolling history array. If the average drops below 30 over five seconds, the system enters low-performance mode. Camera movement uses linear interpolation rather than snapping — camera.position.x += (targetX - camera.position.x) * 0.05 creates smooth following behavior without an animation library.
Cross-reference resolution in Content Collections. Astro’s reference() function validates that a referenced ID exists, but doesn’t automatically fetch the related content. The project detail page fetches the main project entry, then maps over data.relatedTutorials, fetches the full tutorials collection, and finds matching entries by ID. Promise.all ensures all lookups complete before rendering. This pattern repeats across the site — category pages, tag pages, author pages all follow the same explicit fetch-and-resolve approach. It adds some boilerplate, but gives each page control over exactly which relationships to resolve without over-fetching.
What I’d Change or Emphasize Differently
Type safety compounds over time. The upfront investment in Zod schemas and TypeScript types paid off as the project grew. Adding a new project requires filling out a structured frontmatter block. Missing or malformed fields fail the build with a clear error message. This prevents the gradual entropy that plagues content-heavy sites — broken links and missing metadata accumulate over months when there’s no validation. Treating content as code, with the same validation standards you’d apply to application logic, is the right instinct.
Measure before you optimize. The Three.js background’s FPS tracking and automatic quality degradation illustrate a principle that transfers beyond graphics: measure first, adapt second. Without the FPS counter, the site would either run poorly on low-end devices or disable the background entirely on mobile. Monitoring performance and adapting dynamically finds the right balance for each user’s hardware.
Where This Goes
The blog content directory exists but is empty. The schema supports blog posts with the same metadata as projects, and the RSS feed generation already supports multiple content types. Migrating long-form content from external platforms into the blog collection would centralize everything in one place — a natural next step.
Adding support for embedded CodeSandbox or StackBlitz instances in project detail pages would let visitors interact with code directly in the browser without leaving the site. The MDX format already supports custom components, so a <DemoEmbed> component accepting a sandbox URL is straightforward to build.
Integrating privacy-focused analytics (Plausible or Cloudflare Web Analytics) through the existing Partytown integration would surface which projects and tutorials actually resonate with visitors — which would directly inform what to build and write next.
Try It Out
Check out the live demo.