Building Portfolio With Astro
I’ve been meaning to build a proper portfolio for years. As a software developer working with React, React Native, Flutter and other technologies, I had projects and ideas to show but nowhere to show them. I also wanted a place to write about tech stuff I was learning. And honestly, I wanted an excuse to try something new.
Most developer portfolios I see fall into two categories: either plain HTML pages that look like they’re from 2010, or overengineered apps that take five seconds to load. I wanted something in between.
The First Attempts
Back in 2021, my first version was hosted on GitHub Pages. Static HTML and CSS. It worked, but every time I wanted to change something, update a project, I had to edit raw HTML files. After a few weeks, I stopped updating it entirely.
The following year, I did what most developers were doing and rebuilt it with Next.js. Everyone was using Next.js. It was the default choice. The site looked better, had a proper blog system, and I felt like a real developer using a real framework.
But something felt off. I was shipping around 200kb of JavaScript to the browser, even though my portfolio was mostly static content. The blog posts don’t need JavaScript. The about page doesn’t need JavaScript. The homepage is just text and images. The only interactive parts were a contact form and a game I built for fun.
Finding Astro
I stuck with the Next.js version for a couple of years. It worked. But I kept seeing Astro mentioned on Twitter and Reddit. The framework had hit version 1.0 back in August 2022, and by 2024, companies like Google, Microsoft, and The Guardian were using it in production.
The pitch was simple: ship zero JavaScript by default, add it only where you need it.
In 2024, I decided to rebuild the portfolio one more time. Third time’s the charm, I hoped.
Why Astro Fit
The core idea of Astro matched what my portfolio actually needed. Most of my content is static. Blog posts are Markdown files that don’t change after I write them. The about page is just text and images. The project showcase is static.
But I also have a few things that need to be interactive. There’s a contact form with validation. A 3D game built with Three.js. These need JavaScript. The Spotify widget showing what I’m currently listening to is server-rendered on each request, so no JavaScript needed there.
Astro calls this “islands architecture.” Your page is static HTML by default. Interactive components are islands of JavaScript in that static sea. The framework decides when to load that JavaScript:
<!-- Three.js game is heavy. Only load JS when it enters the viewport. -->
<Game client:visible />
<!-- Form HTML renders immediately. React hydrates when browser is idle. -->
<ContactForm client:idle />
<!-- No directive means no JavaScript. Just static HTML. -->
<Header />
The client:visible directive uses IntersectionObserver internally. The game’s JavaScript doesn’t load until you scroll down to that section. The contact form renders as static HTML on the server, then React attaches event handlers during browser idle time.
The Stack
Here’s what the project looks like:
- Astro as the framework
- React for interactive components (contact form, game)
- Content Collections for blog posts with type-safe frontmatter
- Tailwind CSS for styling
- Cloudinary for image optimization
- AstroDB with Turso for tracking post views
- Vercel for hosting
The project structure follows a fairly standard pattern. Components are organized by complexity. Blog posts live in a content folder as MDX files. API routes handle things like the Spotify integration and contact form submission.
Fonts
One easy win was font subsetting. I’m using Roboto Flex, a variable font that gives me the full weight range (400-700) in a single file. The full font from Google is about 1.4MB. After subsetting to ASCII characters only, it’s around 200KB.
I used glyphhanger to strip out everything except US ASCII:
glyphhanger --subset=robotoflex-regular.woff2 --format=woff2 --US_ASCII
200KB is still larger than a static font, but that’s the trade-off for variable fonts. Instead of loading separate files for regular and bold weights, one file handles everything.
One Interesting Problem
I wanted to show what song I’m currently playing on Spotify, with a 30-second preview. Spotify’s API has a preview_url field for exactly this. Then in late 2024, they deprecated it. No more previews through the API.
But the embed player still works. If you look at the HTML that Spotify returns for an embedded track, the preview URL is buried in there as JSON. So now I fetch the embed page and extract it:
const embedUrl = `https://open.spotify.com/embed/track/${trackId}`;
const html = await fetch(embedUrl).then((r) => r.text());
const match = html.match(/"audioPreview":\s*{\s*"url":\s*"([^"]+)"/);
This will probably break when Spotify changes their embed structure. I have an iTunes fallback for when that happens. Not pretty, but it works.
The Results
After finishing the rebuild, I ran Lighthouse out of curiosity. All categories scored 100. Performance, accessibility, best practices, SEO. First paint at 0.3 seconds. Zero layout shift.
I wasn’t trying to optimize for these scores. I didn’t do anything clever. Images go through Cloudinary. Static assets get long cache headers. But none of that required special effort. Astro’s defaults are just sensible.
The biggest JavaScript on the site is Three.js for the game at around 600KB, but it only loads on the game page. The analytics (Mixpanel) is 87KB and loads on every page. That still bugs me. It’s probably overkill for a portfolio. I’ll swap it out for something lighter at some point.
What I Learned
The main lesson wasn’t technical. It was about matching tools to problems. Next.js is a great framework for applications that need server-side rendering, API routes, and dynamic content. My portfolio needed almost none of that.
Astro worked because it treats JavaScript as opt-in rather than opt-out. For a site that’s 90% static content with a few interactive pieces, that’s exactly right.
I’m not saying everyone should use Astro. If you’re building a dashboard or a web app with lots of client-side state, something like Next.js or Remix makes more sense. But for portfolios, blogs, marketing sites? I’d start with Astro now.