What Building My Portfolio Taught Me About Product Thinking
A portfolio is just a product with one user. Here's what building mine taught me about design decisions, scope, and shipping things that aren't perfect.
I've rebuilt my portfolio four times. Each rebuild taught me something different — not about design or code, but about product thinking. A portfolio is a deceptively hard product to build because the customer is everyone and no one at once. You're building for recruiters, potential clients, collaborators, and the occasional curious stranger. You're also building for yourself, which is often the hardest customer to satisfy.
Here's what I've learned.
Scope Creep Hits Hardest on Personal Projects
Professional projects have constraints: deadlines, stakeholders, budgets. Personal projects have none of these, which sounds liberating until you're redesigning the hero section for the sixth time because no one said stop.
My first portfolio had a 3D globe. It looked incredible and took forever to load. My second had a custom cursor. My third had a loading animation. Each one was solving for the wrong thing — I was optimising for impressive when I should have been optimising for useful.
The fourth one (this one) has none of those things. It has essays, projects, and a way to contact me. That's it.
Constraints aren't limitations. They're the thing that forces clarity.

Next.js Was the Right Call
I evaluated a few options before settling on Next.js with the App Router. The alternatives were Astro (great for content, less flexible for dynamic features) and plain SvelteKit (genuinely considered it, the syntax is beautiful).
Next.js won because of how naturally the App Router handles the mix of static content and dynamic routes. My essays are statically generated from MDX — fast, cacheable, SEO-friendly. My projects page can be dynamic when I need it. The mental model of "server first, client when necessary" maps well to how I think about performance.
// MDX content — generated at build time
export async function generateStaticParams() {
const essays = await getEssaySlugs();
return essays.map((slug) => ({ slug }));
}package main
import (
"fmt"
"strconv"
)
type Node struct {
Text *string
Number *int
Children []Node
}
func ExtractText(node Node) string {
if node.Text != nil {
return *node.Text
}
if node.Number != nil {
return strconv.Itoa(*node.Number)
}
result := ""
for _, child := range node.Children {
result += ExtractText(child)
}
return result
}
func main() {
text := "Hello "
num := 123
world := "world"
tree := Node{
Children: []Node{
{Text: &text},
{Number: &num},
{
Children: []Node{
{Text: &world},
},
},
},
}
fmt.Println(ExtractText(tree))
}#[derive(Debug)]
enum Node {
Text(String),
Number(i32),
Children(Vec<Node>),
}
fn extract_text(node: &Node) -> String {
match node {
Node::Text(s) => s.clone(),
Node::Number(n) => n.to_string(),
Node::Children(children) => {
children.iter().map(extract_text).collect()
}
}
}
fn main() {
let tree = Node::Children(vec![
Node::Text("Hello ".to_string()),
Node::Number(123),
Node::Children(vec![
Node::Text("world".to_string())
])
]);
let result = extract_text(&tree);
println!("{}", result);
}Fumadocs handles the MDX pipeline — parsing frontmatter, generating the source, handling the routing. The thing I'd change if I started again: set up the content schema strictly from day one. I added fields like readTime and featured mid-project and had to backfill every file.
Design Decisions Are Product Decisions
Every visual choice on a portfolio is also a content strategy decision. Some examples:
Dark mode: I ship both. Not because I think everyone uses dark mode (they don't), but because not shipping it signals that I didn't think about it. For a design engineer, that signal matters.
No animations on first load: Every animation has a cost — either performance or attention. I removed the entry animations from the homepage and added them back only where they serve a purpose (hover states, page transitions). The page felt snappier and more confident without them.
Typography: I use Cabinet Grotesk for display and system fonts for body text. The custom display font is a personality choice. The system font is a performance choice. Both are intentional.1
Navigation: Five links maximum. If you need more, you're trying to say too many things at once. A portfolio should have a point of view.

The Hardest Part Wasn't the Code
It was deciding what to leave out. Every project I've worked on isn't on here. Every skill I have isn't listed. That's by design — a portfolio that lists everything says nothing.
The essays section was the last thing I added and the best decision I made. Code shows what you can build. Writing shows how you think. Hiring managers and clients want both.
Shipping Imperfect Things
Version four of this portfolio is not done. It may never be done. But it's live, and being live matters more than being perfect. The act of publishing forces decisions that iteration avoids.2
If you're sitting on a half-built portfolio waiting for the right moment — this is the right moment. Put it up. Fix it later. The people you're building it for would rather see something real than wait for something ideal.
Footnotes
-
Cabinet Grotesk is designed by Fontshare and distributed free for personal and commercial use. It has a slightly geometric quality that works well at large display sizes without feeling cold. ↩
-
Ship it, then improve it. This is advice I give freely and follow reluctantly. The portfolio you see now was pushed live in a state I wasn't fully happy with. Every update since has been made possible only because it was already live and real people were reacting to it. ↩
More essays
View allRust • February 10, 2025
Outcome Engineering: The Discipline of Building What Actually Matters
Most engineers build features. Outcome engineers build results. The distinction sounds small but changes everything about how you approach a problem.
Open Source • January 15, 2025
The Design Engineer's Toolkit: Tools I Actually Use
A honest look at the tools, libraries, and workflows that make up my daily design engineering practice — what works, what doesn't, and why.
Typescript • December 20, 2024
What Building Mobile Apps Taught Me About Constraints
Mobile development forces decisions that web development lets you defer. Working under those constraints changed how I approach every product I build.