Every technical leader eventually has to build a reputation outside their company. The portfolio is the forcing function that reveals whether you actually know your positioning — or just think you do. It went about as well as you'd expect.

I review tech stacks for a living. I make architectural decisions that affect hundreds of engineers. I've debugged performance bottlenecks in systems handling billions of requests. And yet, building a simple personal website nearly humbled me.

Not "nearly." It did.

The first version had a broken logo. In production. For three days.

Building your own portfolio teaches you things a client project never will - mostly because there's no one else to blame.

Why I Didn't Just Use a Template

Everyone asks this. The honest answer isn't "I wanted full control" - it's that I started looking at templates and kept thinking "I'd just change this... and this... and this." At that point you're basically building from scratch anyway but with someone else's bad decisions baked in.

The trap is real: "It'll only take a weekend." Narrator voice: It did not take a weekend.

I decided on Astro - static site generation, zero unnecessary JavaScript, fast by default. In hindsight this seems obvious. At the time it felt like a gamble. Static site generation was having a renaissance but the ecosystem felt fractured. Would Astro stick around? Would the plugins I needed exist?

Spoiler: yes and yes. But I didn't know that when I committed to it.

The Architecture Decision I Got Right (and One I Got Wrong)

Right: keeping it static. No backend, no database, no auth layer. If there's nothing to hack, there's nothing to secure. The portfolio is a file you download. That's it. No midnight pages. No database connections timing out. No infrastructure to maintain.

Wrong: the CSS strategy. I started with a component-scoped approach, then switched to global CSS for performance, then tried a hybrid model to keep some scoping. By the time I settled on a pattern, I had three competing conventions fighting each other in the same codebase.

"I'll clean up the CSS later." Later never came. The mess shipped.

This is the single most costly decision pattern I've seen - not just in my code, but everywhere. The moment you think "I'll refactor this when I have time," you've built a technical debt factory.

The Gotchas Nobody Warns You About

This is where the story gets interesting. These are the six things that made me stop and stare at the screen wondering how they ever made it to production.

1. Lazy loading killed my logos above the fold

Added loading="lazy" to all images for performance. Sensible choice. Except the company logos in the hero section loaded blank on fast connections because the browser hadn't bothered fetching them yet. The human eye can't tell if a logo is slow to load or missing-it just sees a broken page.

Took embarrassingly long to diagnose. The fix was simple: don't lazy-load anything above the fold. Should have started there.

<!-- ❌ Lazy-loads everything — above-the-fold images render blank -->
<img src="/logo.svg" loading="lazy" alt="Company logo" />
<!-- ✅ Eager + high priority for above-the-fold; lazy for everything else -->
<img src="/logo.svg" loading="eager" fetchpriority="high" alt="Company logo" />
<img src="/blog-thumbnail.jpg" loading="lazy" alt="Post thumbnail" />

2. The OG image generator that silently skipped posts

Wrote a script to auto-generate Open Graph images for blog posts. It had a "skip if file exists" guard - totally reasonable. Except I forgot that it would also skip posts where the image was corrupted or half-generated. Deployed a post with a broken social preview. Found out when someone shared it.

The script succeeded. The image didn't exist. No error. No warning.

// ❌ The bug — skips corrupted or half-written files silently
if (fs.existsSync(outputPath)) continue;
// ✅ The fix — validate the output, not just its existence
if (fs.existsSync(outputPath) && isValidImage(outputPath)) continue;

The leadership equivalent: validate outcomes, not activity. "The PR was merged" is an exit code. "The behavior works in production" is output validation.

3. Hardcoded dates in the sitemap script

Built a sitemap generator. Hardcoded the last-modified dates at the top of the script as a lookup object. Every new post required a manual update or the sitemap would report stale dates. Discovered this three posts in when I noticed Google wasn't re-crawling updated pages.

That's not a system. That's a process that fails when you're busy.

// ❌ Hardcoded lookup — breaks silently when you forget to update it
const lastModified: Record<string, string> = {
'my-first-post': '2026-03-01',
'shipping-speed': '2026-03-15',
// every new post: don't forget to add here!
};
// ✅ Read from the filesystem — always accurate, zero maintenance
const lastModified = fs.statSync(filePath).mtime.toISOString();

4. Two sources of truth for blog metadata

One metadata file for the Astro build pipeline (TypeScript/ESM). A separate one for the RSS generator (plain JavaScript/CommonJS). Had to update both manually, in sync, every single time.

I built a process that required discipline to maintain. Discipline is not a system. Discipline is a bug waiting to happen. It happened.

The leadership equivalent: any cross-team process that requires both sides to stay in sync manually is not a process — it's a social contract with an expiry date. You see it in reporting lines, OKR ownership, and sprint ceremonies. Single source of truth isn't just an engineering principle.

5. RelatedPosts blowing up because one post was missing metadata

One post existed as a page file but had no metadata entry. The component that fetched related posts tried to look it up, found nothing, and threw an error. Not on that post's page - on every other post that had overlapping tags.

Took way too long to trace back. The error message was useless. The stack trace led nowhere.

// ❌ Blows up on every page that shares tags with the missing post
const related = allPosts.filter(p =>
p.tags.some(t => post.tags.includes(t))
);
// ✅ Guard first — skip any post with no metadata entry
const related = allPosts
.filter(p => postMeta[p.slug])
.filter(p => p.tags.some(t => post.tags.includes(t)));

6. The particle system on mobile

Built an animated canvas particle system for the hero section. Looked beautiful on desktop. On a mid-range Android phone it dropped to 8 FPS and made the whole page feel sluggish. Added a FPS cap and reduced particle count on mobile. Should have started there instead of desktop-first.

This one taught me: build for the slowest device first, not the fanciest one.

The leadership equivalent: systems designed for your best-performing team members fail when someone goes on leave, joins late, or inherits context without documentation. Optimize for the realistic distribution, not the peak case.

Performance Is a Feature Until It Becomes an Obsession

Set a target: sub-1-second load on 3G. Achieved it. Spent another two weeks shaving bytes. At some point the marginal gain per hour drops to zero. That point comes earlier than you think.

I optimized things that don't matter - hero animation render time. While ignoring things that do - font loading strategy. The Lighthouse score is not the product. The product is the product.

After you hit "good enough," the cost of improvement starts exceeding the value. A human won't notice the difference between a 0.8 second load and a 0.6 second load. But they'll notice if you take six weeks to ship.

Content Is Harder Than Code

Writing about yourself is uncomfortable. Writing about yourself in a way that doesn't sound like a LinkedIn brag post is harder.

I spent more time on the copy than on the code. This surprised me. The code was the easy part. Saying something true about myself that didn't sound performative-that was the hard part.

The first draft of my home page read like a job description. "Experienced leader with expertise in..." No. That's not how humans talk. I had to strip out all the corporate language and rewrite it as a person talking, not a profile performing.

Ended up writing a voice guide just to keep myself consistent. It's probably the most useful document I created during this whole process.

Deployment Is Where Assumptions Go to Die

Local builds pass. Production builds pass. And then you check the live site and something is slightly wrong in a way that's hard to articulate.

The deploy pipeline runs four scripts in sequence before the Astro build. If any of them silently produce bad output, the build succeeds and the output is wrong. The build logs don't tell you what happened. They just tell you it succeeded.

Case in point: the RSS feed was generating fine locally but producing malformed XML in CI because of a line-ending edge case. Validators said it was fine. Feed readers disagreed.

Always validate output, not just exit codes.

Exit codes are liars. They just tell you whether the process crashed. They don't tell you whether the process did what you wanted.

What I'd Do Differently — and What You Should Do Instead

This section is the synthesis the war story above doesn't give you. The lessons below are specific to building a professional portfolio — not just a personal website.

1. Define the positioning before touching the code

A portfolio is not a showcase. It is a positioning document. Before writing a line of code, answer three questions: What role are you targeting? What is the one thing you want visitors to immediately understand about you? What do you want them to do next? I built the structure first and retrofitted the answers later. Every time I had to restructure a page, it was because I had not answered one of those questions clearly enough upfront.

2. Set up analytics and UTM tracking before you publish anything

You cannot improve what you cannot measure. GA4, goal tracking, and UTM parameters on every outbound link should be the first thing you configure — not the last. I added measurement late and lost weeks of attribution data I can never recover. If you are using the portfolio for a job search or consulting funnel, that data is not optional.

Portfolio sessions per week, Mar–May 2026 (Google Analytics). This is what measurement actually shows you — the spike in weeks 9–10 is directly traceable to specific UTM-tagged campaigns. Without GA4 in place from day one, that attribution is gone. Source: GA4 property 529084495.

3. The framework does not matter — stop letting it be the decision

I used Astro. It was a good choice for a content-heavy static site. Next.js, SvelteKit, or a headless CMS would have produced an equivalent result. The framework is infrastructure. The article content, the voice, the positioning, and the conversion path are the product. Spend one hour picking the framework and ten hours on what you are going to say. The ratio most developers use is the inverse of this.

4. Treat content as the primary deliverable, not the afterthought

Nobody hiring a CTO is assessing the quality of the Astro components. They are reading the writing. The articles, the case studies, the way you explain hard decisions — that is what differentiates a portfolio from a CV with a domain name. Budget at least as much time for editorial work as for technical work. For a professional positioning yourself at the CTO level, probably more.

5. Build explicit conversion paths from day one

A portfolio without a clear call to action is a brochure. Every page should have one primary next step: book a call, read the case study, download the CV, send an email. I added conversion CTAs late, and the early traffic — which is often the most valuable — hit pages with no clear direction. Decide your conversion goal before you design anything.

6. Document your conventions as you go

Every time you make a decision that seems obvious, write it down. File naming conventions, component patterns, metadata structure — none of it will be obvious when you return to the project three months later. The CLAUDE.md pattern I use now is the documentation structure I wish I had built from the start.

CLAUDE.md (the document I wish I'd written on day one)
# Portfolio Conventions
## File naming
- Blog pages: src/pages/blog/[slug].astro
- Metadata: src/data/blog-metadata.ts ← single source of truth
- OG images: public/og/[slug].png ← must exist before deploy
## Component rules
- Charts: <Chart type="line" data={...} /> (chart.js wrapper)
- Code blocks: <Code code={...} lang="ts" /> (expressive-code)
- Images above the fold: loading="eager" fetchpriority="high"
## Never
- Lazy-load images in the hero or nav
- Hardcode dates in scripts — use fs.statSync().mtime
- Two metadata files for one concept

Two Months Later

The broken logo is fixed. The CSS is still a little messy. What changed: 40 articles published, 790 sessions in the peak week (May 2026, up from 109 in week one), and six active executive search conversations that started with "I read your site." The analytics chart above shows what measurement actually looks like — the week-nine spike is directly traceable to a specific campaign. Without GA4 from day one, that attribution is gone permanently.

If you are a technical leader thinking about building your own: the technical work is the easy part. The hard part is deciding what you actually want to be known for, then having the discipline to say only that. Every page that tries to say everything says nothing.

The most honest thing a portfolio can show is the gap between what you know and what you thought you knew.