The Goal
Every developer eventually needs a portfolio site. The challenge isn't the scope — it's that the site itself becomes a statement about your skills and taste. Decisions that seem trivial (which framework? static or dynamic? CSS approach?) are all visible to anyone who reads the source code or runs Lighthouse.
This breakdown covers the key decisions made when building a modern, performant developer portfolio from scratch, and what each choice looked like in practice.
Tech Stack Chosen
- Framework: Astro — generates fully static HTML by default, with islands of interactivity where needed
- Styling: Tailwind CSS — utility-first, no runtime overhead, easy to keep consistent
- Content: Markdown files with frontmatter — no database, no CMS, version-controlled alongside code
- Deployment: Vercel — automatic deploys from Git, free tier is more than enough for a portfolio
- Analytics: Plausible (self-hosted) — privacy-respecting, no cookie banner needed
Why Astro Over Next.js or Gatsby?
The portfolio is almost entirely static content — a home page, a projects section, a blog, and a contact form. Next.js is excellent, but it ships a JavaScript runtime to the client even for pages that don't need it. Astro's zero-JS-by-default approach meant the initial build produced pure HTML and CSS for every page.
The one exception was the contact form, which used an Astro Island — a React component that hydrates only when it becomes visible. The rest of the site: no JavaScript bundle.
Performance Results
Performance wasn't an afterthought — it was a design constraint from the start. The decisions that had the most impact:
- Self-hosted fonts — downloaded the Inter and Space Grotesk font files and served them from the same origin. Eliminated the Google Fonts round-trip and the Flash of Invisible Text (FOIT).
- Image optimisation — used Astro's built-in
<Image />component, which automatically generates WebP variants and adds correctwidth/heightattributes to prevent layout shift. - Critical CSS inlined — Astro inlines the styles needed for the above-the-fold content automatically in production builds.
- No third-party scripts — no Google Analytics, no social embeds, no chat widgets. Every script is a performance liability.
Content Architecture
Projects are stored as individual .md files in a /src/content/projects folder. Each file has frontmatter fields for the title, description, tech stack, live URL, GitHub URL, and a featured flag. Astro's Content Collections API validates this schema at build time using Zod — if a field is missing or wrong, the build fails before it ever deploys.
This approach means:
- Adding a new project is just adding a new Markdown file
- No CMS login, no database query, no API key to manage
- The entire site — content and code — lives in one Git repository
What Didn't Go as Planned
The contact form was the most painful part. The original plan was a simple serverless function on Vercel. It worked fine, but adding rate limiting, spam protection, and proper error handling ballooned the scope. In hindsight, using a service like Resend or Formspree for the email delivery and focusing custom code only on the validation logic would have saved several hours.
Dark mode introduced a flash of incorrect theme on first load — a classic problem when reading a localStorage preference in JavaScript after the page renders. The fix was a small inline <script> in the <head> that reads and applies the theme class before the first paint.
Key Takeaways
- Choose boring technology for the parts that don't matter and interesting technology only where it adds real value.
- Measure performance from day one — retrofitting performance onto an existing design is significantly harder.
- Static sites with file-based content are dramatically easier to maintain than anything involving a database for personal projects.
- Your portfolio is never finished — ship something solid, then iterate in public.