# How I built this site with Claude Code.

**Author:** Ivan Misic  
**Published:** 2026-02-07  
**URL:** https://ivanmisic.net/blog/ai-tools/how-i-built-this-site-with-claude-code

**In plain English**

I built the first version of this site over about three weeks with a custom PHP backend, vanilla JavaScript, custom public CSS, and Claude Code doing most of the typing. The original build used no backend, frontend, or CSS framework; the current admin area later gained a standalone Tailwind build. Claude made implementation fast, but architecture, design, review, security, and documentation stayed with me. Focused sessions, early rules, design tokens, and time to understand the generated code would have made the work cleaner.

I built the first version of this website over about three weeks of actual work. A week during holiday, a handful of weekends, and some late nights in between. I built that version without WordPress, Laravel, React, or Tailwind. The public site still uses PHP, vanilla JavaScript, and custom CSS. The admin area later gained a standalone Tailwind build, so the sections below distinguish the original build from the current repository.

## Why Build From Scratch?

After years of managing products built on frameworks and platforms, I wanted to understand what's actually under the hood, not just the abstraction sitting on top of it.

I wanted to understand AI-assisted development before using it in product work, so I chose a real project with enough complexity to expose its limits.

So I set some rules:

1. **No backend frameworks.** No Laravel, no Symfony. Custom MVC from scratch.
2. **No frontend frameworks.** No React, no Vue, no jQuery.
3. **No CSS frameworks.** No Tailwind, no Bootstrap. Custom design system with tokens.
4. **Build it properly.** Database migrations, service layer, feature flags, deployment pipeline. Not a toy project.
5. **Use Claude Code for implementation.** I'd make all the architecture and design decisions. Claude would write the code.

## The Stack

What powers this site:

| Layer | Technology | Details |
|-------|-----------|---------|
| Backend | PHP 8.2+ | Custom MVC framework with strict typing |
| Frontend | Vanilla ES6+ | Focused scripts without a frontend framework |
| Public CSS | Custom design system | BEM and utility classes |
| Admin CSS | Tailwind | Standalone admin build |
| Database | MySQL | Schema changes tracked with migrations |
| Server | LiteSpeed | With cache integration for production |
| Deployment | Custom pipeline | JSON content sync, PHP build script, Python deploy |

The original build used no npm. Its only build tool was the PHP minifier I wrote. The current admin area now adds a standalone Tailwind CLI.

## Architecture

The request flow is simple:

```
Request → index.php → Bootstrap → Router → Controller → View
                                     ↓
                                  Service → Model → Database
```

<figure>

![Architecture map showing public article requests moving through index.php, the router, BlogController, model reads, cached Markdown rendering, and the article view, with content updates following a separate guarded service path](/images/blog/ai-tools/ivanmisic-current-request-architecture.png)

<figcaption>This is the current repository shape, not a generic MVC diagram. Public controllers can read models directly to assemble a response, while reviewed content updates cross <code>ContentEditingService</code> and <code>BlogPostService</code> before persistence. Markdown rendering is cached, with <code>MarkdownService</code> doing the conversion on a cache miss.</figcaption>

</figure>

I kept the request flow simple, but I was strict about the boundaries between layers.

### Strict Layer Separation

Controllers handle HTTP. That's it. They extract request data, check authentication, and call services or models. They never contain business logic.

Services handle all mutations. Every create, update, and delete goes through a service that validates input, generates slugs, manages cache, and returns a `ServiceResult` object. Controllers never touch models directly for writes.

Models handle normal persistence and define the allowed columns. Application mutations go through services. A few infrastructure services, including migrations and content import, contain documented SQL where that job requires it.

The current create path, trimmed to the hand-off between controller and service, looks like this:

```php
// Admin\BlogController::store()
$result = BlogPostService::create($data);
if ($result->failed()) {
    $this->error($result->errorString());
    return $this->redirect(url('/admin/blog/create'));
}

// BlogPostService::create()
$data = self::normalizeData($data);
$errors = self::validate($data);
if (!empty($errors)) {
    return ServiceResult::failure($errors);
}
```

This pattern made the codebase predictable. When something breaks, I know exactly which layer to look at.

### Feature Flags

The content-related flags currently cover the blog, tools, toolshed, guides, and news. When a flag is disabled, the router shows a "Coming Soon" page for public visitors, but the admin panel still shows everything. This means I can build and populate a new section privately, then flip a switch to launch it.

```ini
FEATURE_BLOG=true
FEATURE_TOOLS=true
FEATURE_GUIDES=true
FEATURE_NEWS=false
```

### Database Migrations

Newer schema changes use migration files with explicit UP and DOWN sections. The runner tracks batches and can roll back the latest one through the CLI or admin UI.

We introduced the migration system a few iterations into the build. Some early migrations predate the current format and need manual reversal, so rollback is a capability, not a guarantee.

### Content Pipeline

Blog posts are written in Markdown, stored in the database, and rendered to HTML at runtime. No pre-rendered cache. The content sync system exports everything to a JSON bundle (slug-based, not ID-based) that can be imported on production. View counts and analytics are never overwritten during sync.

## The Brutalist Design

I went with "Brutalist Bold." Dark backgrounds, 2px borders everywhere, zero border-radius, monospace fonts for labels and metadata, and Electric Lime (#D4FF00) as the accent color.

![Homepage of the original ivanmisic.net design: black canvas, Electric Lime #D4FF00 accent, oversized outlined "IVAN MISIC BUILDS" wordmark, monospace stat tiles for experience, articles, tools, guides](/images/blog/redesign-v1-homepage.png)

> **Heads up.** The site you're reading right now isn't this. The v1 brutalist build was replaced by a Station warm-noir design in April 2026. The full redesign story (four calendar days, one person, end-to-end) is in [Four Calendar Days, One Person, One Full Redesign](/blog/ways-of-working/four-days-one-person-redesign). The rest of this post is the original v1 build story, kept as-is.

Why brutalist? Two reasons.

First, constraints make decisions faster. When border-radius is 0, you never debate "should this be 4px or 8px?" When shadows aren't allowed, you use borders. When you only have one accent color, you don't waste time on color palettes. Every rule I added was one fewer decision to make.

Second, it looks distinctive. Most sites lean on rounded corners and soft gradients, so sharp edges stand out. The site doesn't look like a template because it isn't one.

### The Design Token System

Everything lives in `tokens.css`. Colors, spacing, font sizes, border widths, transitions. Components reference tokens only. Never a hardcoded value.

```css
/* tokens.css */
--color-primary: #D4FF00;
--color-bg: #0a0a0a;
--space-4: 1rem;
--space-8: 2rem;
--border-width: 2px;
--font-mono: 'JetBrains Mono', monospace;
```

The system supports dark/light themes and multiple accent colors (lime, cyan, rose) through CSS custom properties. Switching themes means changing token values, not rewriting components.

The v1 build ran to 48 CSS files and over 15,500 lines. Every component in its own file, utilities loading last so they override components. It was more CSS than most people would write for a personal site, but the token system kept it maintainable. The v2 redesign later consolidated a lot of it.

![Brutalist article layout in action: stark black canvas, sticky table of contents on the left, lime accents on category pills and inline UPDATE keyword, mono-uppercase navigation, hairline rules between sections](/images/blog/redesign-v1-article.png)



## Working With Claude Code

Everyone asks about this part.

### The Main Difference Was Speed

The clear difference was speed. Work I would estimate at a full day sometimes took one or two hours, including database queries, CSS structure, security controls, and deployment scripts. I still reviewed each result.

I could describe what I wanted in plain language and get working, production-quality code back. "Build me a service layer for blog posts with validation, slug generation, and cache invalidation." Done. "Create a migration system that supports rollback by batch." Done.

It was fast and patient, but I still owned the architecture and checked the code.

### The CLAUDE.md File

Early on, I discovered that Claude would "forget" architectural decisions across sessions. It would create a new button style when one already existed, or use a different pattern for error handling than what we'd established.

The fix was a detailed `CLAUDE.md` file at the project root. Design tokens, naming conventions, architectural rules, component inventory. Claude reads it at the start of every session. I also created 15 rules files in `.claude/rules/` covering everything from CSS standards to database conventions. (I dig into this more in [Claude Code and the context window on bigger projects](/blog/ai-tools/claude-code-context-window).)

The rules reduced repeated corrections and made Claude more consistent across sessions. They did not remove the need to review its work.

### Where Claude Struggled

**Over-engineering.** Claude tends to add more abstraction than needed. "Just a simple function" would come back with an abstract base class, two interfaces, and a factory. I spent a lot of time saying "simpler."

**CSS consistency.** Without strict rules, Claude would create duplicate CSS classes. A new card component when `.card-regular` already existed. New button variants when `.btn--primary` was right there. The rules files fixed this, but only after I'd cleaned up several rounds of duplicate styles.

**Design taste.** Claude doesn't have it. The brutalist aesthetic was entirely my vision. Claude executed it well once I explained what I wanted, but it would never have suggested "let's do 2px borders with no border-radius and lime green accents." Every visual decision was mine.

Two more were subtler. The first was context drift. On long sessions with many changes, Claude would sometimes lose track of earlier decisions. Keeping sessions focused on one task at a time helped. So did the rules files.

The second is harder to explain. I came to call them the stupid sessions. Sometimes Claude would just... stop working. Not crash. Not error out. It would keep responding, but the quality would fall off a cliff. Simple tasks it had handled fine an hour ago would produce nonsense. It ignored rules it had been following all session. It made changes that contradicted what it had just done.

My theory: context overload. After 30-45 minutes of complex work, something breaks down. The fix was surprisingly simple. Close everything. Walk away. Come back a few hours later, start a fresh session, and Claude would pick up exactly where it left off like nothing happened. No explanation. No apology. Just back to being competent.

It turned into a rhythm. Work intensely for 30-45 minutes. If Claude starts struggling with things it should know, don't fight it. Don't retry the same prompt five times hoping for a different result. Just stop. Fresh session later. It's frustrating in the moment, but it's faster than arguing with a confused AI for another hour.

### What Worked Well

**Natural language architecture.** I could say "the controller should only handle HTTP, the service should handle business logic" and Claude would implement that separation consistently across every controller and service it created.

**Refactoring at scale.** "Extract all category CRUD into a shared trait." Claude would read the existing implementations, identify the common patterns, create the trait, and update both services. In one pass.

**Security patterns.** Claude often suggested useful controls such as prepared statements, CSRF checks, and rate limits. I still treated those suggestions as code to review, not proof that the feature was secure.

## Why a personal site has this much code

The blog is only one part of the repository. The same codebase runs the tools directory, guides, search, accounts, a custom admin area, private analytics, content sync, and my publishing workflow. It is closer to a small CMS plus a personal operations layer than a simple blog.

### What Came After

The site kept growing after that initial build sprint. A few additions worth mentioning:

**Marketing calendar.** A full calendar view for scheduling social media posts across LinkedIn, Twitter, and Reddit. Drag posts between dates, toggle statuses with a click, link posts to blog articles. Built it in a single session.

**Admin notes system.** As an admin, I can leave notes on any page of the site. Sticky notes attached to URLs, basically. I browse the site, spot something that needs fixing or an idea for improvement, and drop a note right there. Later I export all notes, feed them to Claude Code, and work through them. It's how these very updates to this article happened.

**News aggregation.** An RSS-based system that pulls industry news from sources I curate, clusters related articles, and helps me stay on top of trends relevant to my writing.

Each took an afternoon with Claude Code, but only because I reviewed the output and corrected it as we went.

## What I'd Do Differently

**Set up the design token system from day one.** I started with ad-hoc CSS values and had to retrofit tokens later. Painful. If you're building a custom design system, define your tokens first.

**Shorter sessions.** Long Claude Code sessions led to context drift and inconsistency. Focused sessions (one feature, one PR, done) worked much better.

**Rules files earlier.** I created them after discovering inconsistencies. Should have started with a basic set from the beginning.

**Automate cache busting sooner.** Manual asset versioning was tedious. The build script handles it now, but I should have built that in week one.

**Budget time to understand what Claude builds.** This one caught me off guard. In a typical 30-45 minute session, Claude would build more than I asked for. Not in a bad way. It would see logical next steps and implement them. A service layer would come with cache invalidation I hadn't mentioned. A controller would include rate limiting I hadn't requested. Good additions, but I'd end up spending more time reading and understanding the code than it took Claude to write it.

Then I'd need to document the patterns in my rules files so Claude would stay consistent. Which took even longer.

> The writing was fast. The learning and documenting is what ate the time.

If you're using Claude Code for anything serious, accept that you need to understand what it's doing, how it's doing it, and why. You still own quality, documentation, and the decisions the next session needs to understand.

If you're considering building something with Claude Code, my one-sentence takeaway:

> Claude Code takes the tedious parts off your plate. Deciding what to build is still your job.

If you want a more structured walkthrough, I've put together a [Claude Code Guide](/guides/claude-code-guide) that covers a simpler path to a similar outcome.
