Building a Portfolio Site from Scratch: From Concept to Code in 2 Weeks
Two months ago, my portfolio lived on Framer. It looked fine. But I felt constrained. Framer is great for designers — drag, drop, done. But it's also a black box. You don't own the code, you can't extend it, and you're locked into Framer's paradigms. I wanted to rebuild in code. Plain HTML, plain CSS, my own system. No frameworks, no bloat, just intentionality. This is how I did it, and why the constraints became the strongest part.
The decision: why rebuild?
Control
Framer is a tool. I wanted to own the system. Every color, every animation, every interaction should be mine.
Philosophy
I believe in minimalism — not sparse design, but intentional design. Every pixel should serve a purpose. Framer pushes you toward flashy. I wanted subtle.
Scalability
Framer works for portfolios. But I wanted to add features: case study chat with AI, dynamic components, API integrations. Can't do that in Framer. So I started from zero.
Part 1: The design system (Figma)
Before I wrote a line of code, I designed in Figma. Not to create pixel-perfect mockups — to create constraints.
I locked in the typography: H1 at 32px weight 500, body at 15px weight 400 with line-height 1.8, labels at 11px uppercase with letter-spacing. And a hard rule — font weight never above 500. No 600, no 700. Just 400 and 500.
Why? Constraints force clarity. Without a bold weight, you can't rely on boldness. You use size, color, spacing instead. The hierarchy becomes more intentional.
The color palette is eight values: background, text primary, text secondary, text tertiary, border — in both light and dark variants. No gradients, no shadows, no decorative accent colors. Every color exists for a semantic reason. The green accent appears only for interactive states and badges. It's earned.
For spacing, every value is a multiple of 8: 8, 12, 16, 24, 32, 48, 64. No arbitrary numbers. This creates visual rhythm without thinking.
Max content width: 680px centered. At this width, a line of body text has roughly 80 characters — optimal for reading. Wider, and your eye travels too far. Everything lives in a container with this max-width. Mobile scales down. Desktop respects the constraint.
The system stayed intentionally small. Enough range to create hierarchy and rhythm, not enough to hide weak decisions behind decoration.
Palette
Typography
Spacing
Part 2: The architecture (Astro)
I chose Astro for one reason: it gives me static generation with the option to add dynamic features. Generate static HTML (fast, cheap to host), add interactive components where needed, and send no JavaScript to the client unless you ask for it. This aligned with my philosophy: ship only what's necessary.
Key structural decision: one layout file, one global CSS file. No scoped styles, no CSS-in-JS. Just plain CSS with semantic class names. With one CSS file, you're forced to think systematically. No buried styles, no duplicate logic. Everything is visible.
I built components for reusable patterns, not pages. Nav, Footer, ProjectCard, Badge — each single-purpose. The CaseStudy component is the key one: instead of building five separate case study pages with repeated structure, one component takes data and renders consistently. Around 60 lines per page instead of 400. This is the power of reusable components.
The stack was small on purpose. Each layer had one job, and anything that didn't justify itself got removed.
Astro
Static-first output, with dynamic pieces only where interaction actually mattered.
Plain CSS
One global file, CSS variables everywhere, no utility churn and no hidden structure.
Reusable patterns
Nav, Footer, Badge, ProjectCard, and CaseStudy built once and reused deliberately.
Vercel
Cheap static hosting for most pages, serverless only where the AI chat needed it.
Part 3: CSS as a system
I could've used Tailwind. I chose not to. Tailwind is a utility framework — fast to write but it creates invisible structure. The CSS is hidden inside compiled output. You can't see it, reason about it, or tweak it.
I wanted the opposite: visible structure. Plain CSS with custom properties. Every color comes from a variable. Change the variable and it cascades everywhere. For dark mode, instead of maintaining two CSS files, I just flip the variables with a single setAttribute call. No flash of white on load, no CSS bloat.
The hard rules I set: no shadows, no gradients, no decorative effects, max two font weights, CSS variables everywhere. These aren't limitations — they're forcing functions. They made me think harder about every design decision. The result is more cohesive than if I had infinite options.
Part 4: Case studies as the centrepiece
Case studies follow a strict section order: Hero, Problem, Research, Strategy, Solution, Collaboration, Results, Reflection. Every case study uses this sequence. The consistency is intentional — it trains readers. They know what to expect, so they focus on the content, not the structure.
For layout, I use a two-column grid inside each section: a 120px label column on the left and a fluid content column on the right. This creates visual rhythm. The labels become a scanning guide. Readers can skim by reading labels, or dive deep into content.
Metric cards are prominent — 32px numbers, green for positive deltas. Results are the payoff. They deserve space. I don't bury them in a paragraph. They're the first thing you see when you reach that section.
Use one case study structure everywhere
Every case study follows the same sequence and layout logic instead of reinventing the storytelling each time.
Consistency lets readers spend their attention on the substance of the work, not on learning a new page pattern every time.
The site feels authored rather than assembled. Reuse improves maintainability and also improves reading rhythm.
Give metrics their own visual weight
Key numbers live in dedicated components and section patterns instead of being buried inside body copy.
Results are the argument. If they are visually secondary, the whole case study loses sharpness.
The pages communicate impact fast, even when someone only scans for a few seconds.
Lessons learned
Constraints are liberating
Limiting myself to 8 colors, one font, and no shadows forced better decisions and produced a more coherent result.
Systematisation scales
The CaseStudy component alone saved over 1,200 lines of duplicate code. Reuse is not just tidy, it compounds.
Simplicity is hard
It is easy to decorate a page. It is much harder to make it persuasive and calm without visual crutches.
CSS variables are underrated
They make the design system explicit. Every token stays visible, auditable, and adaptable without adding abstraction debt.
What I'd do differently
Build components earlier, not after. Starting with reusable components would've saved time and enforced the system from the start.
Add analytics from day one. I don't know what people click on or where they drop off. That data would've informed design decisions I was making blind.
Use TypeScript everywhere. I coded defensively, but types would've caught bugs in data flow before runtime.
This portfolio is a statement: clarity, constraints, systems, less is more. Every decision reflects how I think, not just what I can build.
View the portfolio → Back to insights