Building My Portfolio Site with Claude Code
I was skeptical about AI coding tools. Then I rebuilt my portfolio with one. Here's what actually worked, what didn't, and why I'm not "vibe coding".
I didn't believe the hype
I've been a fullstack TypeScript engineer for nearly a decade and I've watched every wave of "this will change everything" tooling come and go. When AI coding assistants started gaining traction I gave them a fair shot. Copilot, Cursor, ChatGPT, various others. The experience was consistently frustrating.
The loop went something like this: ask the AI to do a thing. It does something adjacent to the thing. Correct it. It overcorrects and breaks something else. It apologises. You ask it again, more firmly. It produces something that looks right but has more issues you catch in review. By the time you've guided it to a working solution you've spent more time than if you'd just written it yourself.
I've always focussed on building great developer tooling to speed up development. I didn't need to burn GPU watts to scaffold my crud as that was done for me. My functions already were code-generated with correct arguments so I didn't need glorified autocomplete. I already had UI libraries with automatic theming so I didn't need a tool to bash out sloppy components. I get night terrors so I didn't need more hallucinations.
It's safe to say: I wasn't impressed. The promise was improved productivity but the reality was babysitting an idiot.
Finding a workflow that works
Despite the frustration I kept experimenting as new tools were launched. I gave them a fair shot by testing them on small projects. I noticed that some tools were better than others, some tools were becoming better, and knew that even if it was rubbish now I could see the trend line approaching 'good enough' eventually.
I've had a few ideas (which I'll be writing about once they're deployed) for tools and projects which I'd been putting off because they would require a period of time for deep focus and deep wells of motivation. I've been hearing so much about Claude Code that after failing miserably with some other tools to produce code anywhere near what I actually wanted I thought I'd give it a go on these ideas. The difference was night and day. It was still pretty bad, but if you spent enough time defining requirements and implementation details, setting up standards, and building tooling it could actually deliver readable maintainable code.
Maybe the hype has some small basis in reality? I thought I'd better experiment further.
I've recently gone freelance and desperately needed to rebuild my portfolio site (the old one was a CRA hosted on Firebase which was functional but horrendously outdated). I needed a professional portfolio with a detailed breakdown on my big projects and space for writing about things I'm interested in to show my thought process.
I decided to take Claude for a proper test drive to see how it handles frontend development (my previous experiments were all mostly backend work). This was a full rebuild from scratch. Next.js 16 (App Router), Tailwind CSS v4, shadcn/ui (with Base UI), MDX-driven content, SEO, and self hosted on my own VPS.
I'm not "vibe" coding
I think the conversation around AI-assisted development has (some would argue justifiably) become pretty toxic. I've been a big skeptic until recently so I get it. These silicon valley bros are creating a giant bubble based on hype and hopium that is forcing useless AI into every facet of our society. I totally get why people are pushing back.
I've also seen all the hilarious results of so called 'vibe coding'. GarrysList being something like 300k lines for a basic blog, the Huntarr app dumping the entire .env via an unsecured endpoint, reports of AI generated code having over double the amount of vulnerabilities, and the degrading uptime of github.
There are grifters promising some kind of nirvana where you can just outline a spec, walk away from your laptop for 2 days, burn a billion tokens, and launch a unicorn business. People just asking AI's to 'implement security please', 'make my app better', 'parse this HTML in javascript', etc who are just asking the models to hallucinate some slop that eventually somehow compiles and pretend they are coding.
I get the appeal of removing the barriers to entry for normal non-techy people, but vibe coding is crazy right now. The models just aren't good enough.
I'm not 'vibe coding' or 'prompt engineering'. I'm not begging the model to pretend it's a senior dev.
What I'm doing is closer to how a senior engineer works with a very eager junior developer. One who has decent Google-fu and can write code extremely fast but who needs constant direction and thorough code review.
I architect the solution. I decide the file structure, the patterns, the dependencies, the tooling, the definition of done. I tell it what to build and how. Then I review everything it produces, line by line, and either approve it, ask for changes, or rewrite sections myself.
The AI doesn't make architectural decisions. It doesn't choose libraries. It doesn't decide how components are structured. It doesn't define 'done'. It doesn't choose tradeoffs.
That's my job.
Its job is to take clear direction and produce code that I then verify.
Setting the rules
Even with me in the 'driver's seat' the current models still need a lot of handholding and guidance. Luckily the tooling for doing this is getting better and it's now possible to structure a codebase and enforce rules so that the models produce "not-bad" results first time.
The first thing I did was write a CLAUDE.md file. This is a markdown file in the project root that acts as a constitution for the AI that gets loaded into context at the start of each prompt.
I did some research and the consensus appears to have settled on "keep it minimal and concise". You're burning tokens when it loads this file in for every prompt so you want it to remember the essentials and not get distracted.
Mine includes rules like:
- No
anytypes. Try proper types, generics, and type guards first. Ask before usingany. - Always use the typed route helper or shared config. Never hardcode route paths in components or magic numbers.
- Verify changes. After non-trivial changes, run lint, type-check, and tests. All must pass before considering work done.
- Feature branches only. No direct commits to main. No commits, merges, or pushes without explicit permission.
- Always use the shadcn CLI. Never hand-write shadcn components.
These guardrails exist because otherwise the model descended into sloppiness. The models don't remember most of the 'don't do that again' prompts but many issues can be solved with tooling rather than configuration. It's only situations like 'no any types' that really need to be run whenever it generates code.
These models are lazy, they'll use an escape hatch instead of doing things properly unless you force them into line.
The typed route helper is a good example of this. At first it was putting magic strings everywhere and pointing at routes that didn't exist. To fix this every link in the app goes through a toHref() function that maps route names to paths. This means the AI can't scatter magic strings across the codebase.
If it tries to hardcode /projects/something instead of using the route helper, it's breaking a rule and I catch it in review. I could also strongly type my routes and parameters to ensure tsc throws any errors if it hallucinates a route.
// Every navigation goes through this - no hardcoded paths anywhere
export function toHref(route: AppRoute): Route {
switch (route.name) {
case 'home':
return appPaths.home;
case 'projectDetail':
return `/projects/${route.slug}` as Route;
// ...
}
}I've been building lots of little tools like this to enforce correctness and standards. The best part is these are the types of tools I'd build for any team project. By enforcing correctness you skip lots of bug classes and reduce the cognitive overhead for both humans and agents.
The working loop
The biggest thing I learned is that the quality of AI-assisted development is almost entirely determined by your process and partly by the quality of the tool.
Here's the loop that works for me:
1. Discuss the approach. Before any code is written I talk through the problem with the AI. I write a detailed outline of the feature and requirements then I ask it to grill me relentlessly until we reached a shared "understanding" of the requirements. Sometimes it even raises issues or things that I hadn't thought of myself which is pretty nifty. Often here is where you discover it was planning to slop it up, however you can catch it during the interrogation process and force it down the correct path.
2. Make a plan. Plan mode is the only thing that keeps me using AI. They just simply aren't good enough right now without a solid plan. However, with a solid plan, they can be pretty damn good. I take the learnings and understanding from the discussion and ask it to write up a solid implementation plan based on all my guidance and feedback. We've already clarified what I want, how it should be structured, what patterns to follow, what should be in v0 and v1, etc so it can generally write a pretty detailed plan up. I'll then correct any issues or clarify any gaps to make sure it's the right plan. If it's complex I'll tell it to break the big task down into smaller chunks.
3. Build. Here's where we follow the plan. I usually let the agent work in phases so I can review and course correct as it goes. They are pretty fast at writing code, much faster than me, but need that planning phase to actually excel. With a good plan the output is usually about 80% right on the first pass.
4. Review and edit. I go through everything. Does it work? Did it follow the plan? Did it write clean code? Did it add anything I didn't ask for? Did it do an unsecure bare minimum implementation? That last one is the most common. Ignoring config getters and just rawdogging unvalidated process.env everywhere.
This loop sounds simple but sticking to it rigorously is what makes AI-assisted development actually productive rather than chaotic.
You spend a lot of time upfront defining a PRD and planning the solution. Then the AI blitzes through the plan and implements it. Then you do a thorough code review and ask it to make revisions. It lets you the human operator be in control of the quality of the codebase and it's just using the same approach we've been using as developers already.
Tight - loose - tight feedback loops, clear direction, high standards, and great tooling works whether it's a junior dev or an agent.
Better than Google
This wasn't a particularly complicated site but on this site and on my other side projects I've found it incredibly helpful to bounce implementation ideas around as part of the discovery/planning phase.
Sometimes it's just small things like "you are rawdogging process.env, would zod work as a runtime env validator on next?" with an immediate response of "Yes, here's how I'd implement it" which is more time efficient than me looking up the docs to check.
We ended up using Next.js's instrumentation.ts hook to validate all environment variables the moment the server starts. If anything is missing or wrong the app crashes immediately with a clear error message. Fail fast, fail loud, and make the agent aware that it's been naughty. It's also the tool I would have picked and it's great to see it instantly make the right call.
// instrumentation.ts - validates env vars at server startup
export async function register() {
const { env } = await import('@/lib/env');
env(); // throws with a formatted error if anything is wrong
}I also wanted to have a great markdown experience and haven't had chance to play with rendering markdown for a few years. I asked it to do some research on what tools are out there, what the tradeoffs are between them, links to the docs, and eventually settled on next-mdx-remote and gray-matter so I could have a nice tidy /content folder with projects and blogs all defined in mdx.
Proofreading pal
I'm not the best writer in the world. I tend to overexplain and overwrite my thoughts. For my project listings I just put my headphones on and wrote everything I could think about for my projects. It was detailed but even I got a little tired trying to proof read them.
Normally I'd send my copy off to a friend to review but I thought "parsing language is what these LLMs are good at, let's give it a go" and made a plan to proof read and check the copy of my project mdx files. It's several thousand words of sometimes dense technical writing.
Rather than asking the AI to "just fix everything" (which would have been a disaster) I asked it to go through them one by one, most recent to oldest. For each file it would present the issues it found - spelling, grammar, capitalisation, verbose sections that could be tightened. Then I would approve, reject, or modify each suggestion individually.
Some corrections were obvious typos or capitalisation errors. Others were judgment calls about my writing voice. It was actually pretty good at spotting verbosity and suggesting a few tighter alternatives to help me modify the paragraphs. The result was writing that was cleaner but still sounded like me.
Proofreading your own writing is painful and slow. Knowing it would catch the errors and would highlight verbosity let me get into a flow state and just write the content.
Where oversight was essential
Even with all the guardrails and rules the agent still got things wrong. Often in ways that would have caused real problems if I hadn't been reviewing everything.
I've mentioned the broken routes and unsafe (and silent!) env issues but it also often failed to compile or would try to gaslight me into thinking images were indeed rendering. I also had to set up some extra eslint rules to enforce whitespace because it seemed incapable of letting the code breathe.
It got the Next.js Image component's sizes prop wrong, trying to use Tailwind breakpoint names in what is actually a raw HTML attribute expecting CSS media query syntax for OG images. This is the kind of subtle mistake that looks right at a glance but breaks in production.
The git discipline was non-negotiable. The AI never commits, never pushes, never merges. It was trying to commit a bunch of broken code that didn't work with messages full of emojis and em-dashes. I'll decide when it's a good point to stop and I'll write a clean commit message without all the nonsense.
Finally when it said a feature was 'done' and wanted to move on I'd have to force it to refactor. Its code is pretty close to being right but it's often subtly wrong. Nested try {} catch {} blocks, horrible function names, duplicated code that should be in shared utils, crazy regex for simple string parsing, etc. At each relevant pausing point make sure to review what it's written and make it clean up after itself. Eventually it 'learns' the patterns of the project and has some memory about corrections so it gets better over time, but it's a bit of a journey to get there.
Was it worth it?
Yes. I wouldn't say I'm a convert but I'm not as cynical as I used to be.
Despite the hype the AI didn't make me 10x faster. But if I can get a modest 2x performance boost whilst spending more time doing the interesting thinking and focussing on architecting a solution I call that a win.
It didn't write the hard parts for me. It didn't do the thinking. It wasn't just "vibes". What it did was make it easier to make a plan and then quickly iterate on that plan until it met my definition of done. It didn't complain about refactors, it didn't argue with me that the sloppy pattern it had adopted was 'good enough', it just got on with it.
I love coding and find great joy in solving interesting problems, but most of the time it's just implementing an architecture using standard boilerplate. This tool for this job let me focus on the interesting architecture, design decisions, setting coding standards, writing the actual content, etc. And that's fine by me. That's the work I enjoy and the work that actually determines whether a project is good.
If you're considering using AI coding tools, my advice is: don't believe the hype. I do think that we're getting to the point where it can be a net positive for productivity though so be cautiously optimistic.
Set the rules upfront, make a plan, decide which iterative loop works for you, and never let it go rogue. The tool is only as good as the process around it.
What next
I've been experimenting with a few projects on the side of building this portfolio so I'll continue to try and improve the tooling and methodologies until I can confidently say that using Claude is part of my workflow. If I can deliver the same (or possibly better) quality of code with the same quality of architecture and thought for my clients and startup projects twice as fast it would be mad not to use this tool.
I'm working on a 'starter template' for your classic fullstack TypeScript apps which if built right should theoretically make it quite easy for models to build off of a solid foundation whilst meeting my architectural, style, and quality standards.
I'm also working on my first npm package which will be a plugin for TypeORM to finally offer robust RLS support. Using Claude to do deep dive research into dozens of existing solutions for the problem I'm solving has been genuinely very useful. I've been after this plugin for years and it's cool to have this tool to get me excited to work on it. I'll need to keep a tight leash on it and it needs to meet my design intent, but if we get around to publishing it will be an awesome addition to the ecosystem.
I've also got some very fiddly work on my startup which I'm hoping Claude can help with and have some leads on some interesting client greenfield projects which I'll be able to offer competitive quotes for whilst maintaining product quality.
Enjoyed this post?
I'm available for freelance work. If you like how I think, let's talk about what I could build for you.
Get in Touch