Skip to main content

Building a Portfolio That Doesn't Suck

8 min read

At one point, this portfolio looked finished. The grid was in place, the type felt right, and the dark mode toggle worked. Then I sat down to write the first real post and realized the problem immediately: the site looked polished, but it did not sound like me yet.

What followed was less of a redesign and more of an edit. I kept the parts that felt true to me, cut the parts that felt performative, and rebuilt the site around one idea: if someone spends a few minutes reading, the experience should feel calm, clear, and intentional.

A lot of that aesthetic direction came from Paco Coursey's site. It is the clearest example I know of a portfolio that stays out of its own way: quiet type, a simple grid, and motion that only shows up when something changes. I borrowed that restraint more than any single layout detail.

Next.js App Router (and why I stopped debating it)

I picked Next.js for the site shell: routing, layouts, static generation, and fast page delivery. For writings, I wanted long-form pages that still pull in custom visuals when a paragraph alone is not enough.

Pages and layouts: App Router

The UI shell (home, writings index, article pages) runs on the App Router. I like it for portfolio work because it keeps the structure obvious. Routes live where you expect, layouts stay close to the pages they affect, and static generation gives me performance without extra ceremony.

Posts: TSX pages in the repo

Writings are not MDX files in a content folder. They are TSX pages with shared article chrome and custom components only where they genuinely help. Each post is one route under app/writing/, committed like code. That matters to me because writing and editing stay simple. If I want to fix a sentence, add a visual, or tighten a section, I do it in the repo and ship it with the rest of the site.

Typography is the navigation

I spent more time on fonts than on most components. Satoshi carries headlines and body copy. JetBrains Mono handles code and small UI labels. The combination does something practical: it creates hierarchy you can feel while scanning without switching families every other line.

Every tweak was in service of readability: relaxed line height for body text, medium weight only on titles, and code styles that support the paragraph instead of hijacking it.

The two voices of the site

Satoshi

Headlines that stay calm at small sizes

One family carries the whole reading experience. Medium weight for titles, regular for body copy, and tight tracking only where the eye needs a landmark.

JetBrains Mono

Code, labels, and small UI details: callout-info

Color with purpose (not decoration)

This version of the site is intentionally neutral. Light and dark modes share the same structure: grayscale surfaces, soft borders, and readable contrast. I did not want a theme that competed with the writing.

The small callout accents (warm story, cool info, green tip) handle emphasis inside articles. One rule kept everything honest: if something did not need attention, it did not get an accent.

Neutral palette with quiet accents

The site stays mostly grayscale so typography and spacing do the work. Callout accents only appear where a block needs a little extra context, not as decoration across every surface. Values follow your current theme — click a row to copy.

Backgroundbackground
Foregroundforeground
Mutedmuted
Muted foregroundmuted-foreground
Borderborder
Story accentcallout-story
Info accentcallout-info
Tip accentcallout-tip

Animation as feedback, not spectacle

Most motion on this site is reactive, not decorative:

  • A staggered entrance on the home page, once per full load.
  • Hover states on the grid confirm “yes, this is interactive.”
  • Color shifts on links and list rows that stay under 200ms.

Writings and articles load instantly — no entrance animation there. When animation has a narrow job, you stop noticing it. The interface starts to feel responsive, not staged.

Technical details that changed performance (for real)

Code blocks stay lightweight — no syntax highlighter bundle on pages that do not need one. The home entrance runs once when the document loads and resets on a full browser reload, so client navigations to writings never replay it.

Where content fits into the build (draft to page)

This is the path your reader is taking, even though they only see text and sections:

From draft to published post

1. One page file

Write the post as a route

Each article is a TSX page under app/writing/. The prose lives next to the components it uses, committed like any other UI work.

2. Shared shell

Wrap it in WritingArticle

Title, date, reading time, and nav come from a shared layout component so every post inherits the same typography and spacing.

3. Earned interactivity

Add visuals only when they teach

Most of the page stays plain paragraphs. Custom components show up when a diagram or comparison explains something faster than another block of text.

In practice, it means your editing workflow stays simple: write in a page file, commit changes, and Next renders the content with consistent typography and component styling.

What I'd do differently next time

If I were starting over, I'd set up the writing components earlier. By the end, I had plenty of reusable pieces (callouts, diagrams, article chrome), but I paid for that later with extra glue code.

I'd also profile interaction performance earlier, before motion patterns harden into habits. Shipping teaches you what actually matters; profiling teaches you sooner.

Ship it, then polish the decisions

The fastest way to lose momentum is to keep chasing a perfect first version. I shipped, watched how the reading experience behaved, and then tightened details that had real impact: what to emphasize, what to simplify, and how to keep everything fast.