What I Learned Rebuilding My Portfolio Site Three Times
After three complete rewrites in as many years, I've landed on principles that actually matter for developer portfolios: accessibility, performance, and maintainability over aesthetic trends.

The Rewrite Cycle
I've rebuilt my portfolio site three times since 2021. Not because I enjoy throwing away work, but because each iteration taught me something the previous one missed. The first was overengineered React with SSR. The second was a Gatsby experiment that became unmaintainable. The current version is Next.js with MDX, and I think I've finally stopped fighting myself.
The pattern I see across these rewrites isn't about choosing the right framework. It's about understanding what a portfolio actually needs to do: load fast, read clearly, and stay out of the way of the content.
Performance Is Non-Negotiable
My first portfolio had a 4MB initial bundle. I justified it with "rich interactions" and "smooth animations." The reality was that most visitors bounced before the hero section finished loading.
The current site scores 98-100 on Lighthouse consistently. Here's what actually moved the needle:
// next.config.js
const nextConfig = {
images: {
formats: ['image/avif', 'image/webp'],
deviceSizes: [640, 750, 828, 1080, 1200],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
},
compiler: {
removeConsole: process.env.NODE_ENV === 'production',
},
};
Next.js Image component handles responsive images automatically, but you still need to configure the sizes appropriately. I wasted weeks optimizing JavaScript before realizing my hero images were 3840px wide and served to mobile devices.
Font loading deserves its own callout. Self-hosting fonts with next/font eliminated the external request to Google Fonts and gave me full control over loading strategy:
import { Inter } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
display: 'swap',
variable: '--font-inter',
});
The display: 'swap' prevents invisible text during font load. Simple change, massive improvement to perceived performance.
Accessibility Isn't an Afterthought
I used to think accessibility meant adding alt tags and calling it done. Then I ran my site through axe DevTools and discovered 47 violations. Focus states were invisible. Color contrast failed WCAG AA. Semantic HTML was nonexistent.
The biggest accessibility win was stopping my abuse of div elements:
// Before
<div className="card" onClick={handleClick}>
<div className="card-title">Project Name</div>
<div className="card-description">Description here</div>
</div>
// After
<article className="card">
<h3>
<a href="/projects/name">Project Name</a>
</h3>
<p>Description here</p>
</article>
This change alone fixed keyboard navigation, gave screen readers proper structure, and eliminated the need for click handlers on non-interactive elements.
Skip links are another feature I never see on developer portfolios but should be universal:
<a href="#main-content" className="skip-link">
Skip to main content
</a>
.skip-link {
position: absolute;
left: -10000px;
top: auto;
width: 1px;
height: 1px;
overflow: hidden;
}
.skip-link:focus {
position: static;
width: auto;
height: auto;
}
Keyboard users can jump straight to content. It's three lines of HTML and CSS that respect your visitors' time.
Content Management Without a CMS
I almost installed Contentful for blog posts. Then I realized I was adding complexity to solve a problem I didn't have. I write in Markdown. Why transform that into API calls and database queries?
MDX gives me Markdown with the option to embed React components when needed:
## Regular Markdown Works
But I can also embed components:
<CodeComparison
before={beforeCode}
after={afterCode}
language="typescript"
/>
Then continue with Markdown.
The file system becomes the database. Each post is a file in content/posts/. Metadata goes in frontmatter. No build step complexity, no API routes, no caching strategies.
import { readdir, readFile } from 'fs/promises';
import matter from 'gray-matter';
export async function getPosts() {
const files = await readdir('content/posts');
const posts = await Promise.all(
files.map(async (file) => {
const content = await readFile(`content/posts/${file}`, 'utf-8');
const { data } = matter(content);
return data;
})
);
return posts.sort((a, b) =>
new Date(b.date).getTime() - new Date(a.date).getTime()
);
}
This runs at build time. The entire blog becomes static HTML. No runtime overhead.
What Actually Matters
After three rewrites, these are the principles I'd start with today:
-
Ship less JavaScript. Every component you add is bytes your visitors download. Default to static HTML.
-
Semantic HTML first. Use the right elements. They come with accessibility and SEO built in.
-
Optimize images aggressively. They're usually 80% of your page weight. Modern formats and responsive images aren't optional.
-
Test with real constraints. Throttle your network. Use a keyboard. Turn off JavaScript. Your site should degrade gracefully.
-
Content is the product. Everything else—the framework, the animations, the design system—exists to make your content readable.
The site I have now loads in under a second, scores 100 on accessibility audits, and I can update it by editing a Markdown file. It took three tries to get here, but the simplicity was worth it.