station · ivanmisic.net / v2.02 20 yrs shipping · latest essay Apr 26 · log in
§01 AI & Tools ↵ back to ai & tools
Three Weeks, Zero Frameworks

How I Built This Site With Claude Code

The full story of building ivanmisic.net from scratch. No frameworks, no templates. Just PHP, vanilla JS, a custom CSS design system, and Claude Code.
Published
Updated
Reading
19 min · 2,597 words
On This Page

I built this website from scratch over about three weeks of actual work. A week during holiday, a handful of weekends, and some late nights in between. No WordPress. No Laravel. No React. No Tailwind. Just PHP, vanilla JavaScript, a custom CSS design system, and Claude Code as my development partner.

This post is the full story. What I built, how I built it, what worked, what didn't, and what I'd change.

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 the abstraction. The thing itself.

But there was another reason. AI-assisted development is everywhere now, and I'm a strong believer that you need to understand something before you can manage it or benefit from it in your work. You need to know its strengths and weaknesses firsthand. Building a real project with Claude Code was about learning how AI works in practice. Writing code, yes, but also planning, designing, strategizing, and problem-solving.

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+ ~3,700 lines across 10 focused scripts
CSS Custom design system 26 files, ~10,700 lines, BEM + utility classes
Database MySQL 40 migrations tracking schema changes
Server LiteSpeed With cache integration for production
Deployment Custom pipeline JSON content sync, PHP build script, Python deploy

No npm. No Composer packages for the frontend. No build tools besides a PHP minifier I wrote myself.

Architecture

The request flow is simple:

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

But the interesting parts are the constraints I enforced.

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 persistence. Every model extends BaseModel, defines a $fillable whitelist for allowed columns, and uses prepared statements exclusively. No raw SQL anywhere outside of models.

// Controller (thin - HTTP concerns only)
$result = BlogPostService::create($data);
if ($result->failed()) {
    return $this->error($result->errorString());
}

// Service (business logic)
public static function create(array $data): ServiceResult {
    $data = self::normalizeData($data);
    $errors = self::validate($data);
    if (!empty($errors)) return ServiceResult::failure($errors);
    // slug generation, cache invalidation, reading time calc...
    return ServiceResult::success($entity);
}

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

Feature Flags

Every content type has a feature flag in .env. Blog, tools, guides, journeys, API specs. 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.

FEATURE_BLOG=true
FEATURE_TOOLS=true
FEATURE_GUIDES=true
FEATURE_NEWS=false        # Building this next

Database Migrations

Every schema change has a migration file with an UP and DOWN section. I can run them forward, roll back the last batch, and track what's been applied. Works via CLI or admin UI.

40 migrations and counting. We introduced the migration system a few iterations in, so the actual number of schema changes is higher than the tracked ones. But from the initial blog schema through OAuth tables, analytics, and content sync, every change since has been tracked and reversible.

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

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. 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. In a world of rounded corners and soft gradients, 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.

/* 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.

48 CSS files. 15,500+ lines. Every component in its own file. Utilities load last so they override components. It's more CSS than most people would write for a personal site, but the token system means it's maintainable.

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

Working With Claude Code

This is the part everyone asks about. So what actually happened?

What Blew My Mind: The Speed

The speed changed how I think about building things. Tasks I'd estimate taking a full day were done in an hour or two. And not just boilerplate. Claude handled complex database queries with proper indexing, CSS architecture decisions, security patterns (CSRF tokens, prepared statements, rate limiting), and deployment pipeline design.

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 felt like pair programming with a very fast, very patient senior developer who never needs a coffee break.

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 Making Claude Code Work on Bigger Projects.)

This persistent memory was a turning point. Once the rules existed, Claude stopped inventing new patterns and started following the established ones.

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.

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 stupid sessions. This one is harder to explain. 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 would ignore rules it had been following all session. It would make 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.

This became 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 was better at security than I expected. Column whitelisting, prepared statements, CSRF validation, rate limiting. It would proactively add security measures I hadn't asked for.

The Numbers

Three weeks of focused work produced the first version. The numbers below are where the site stands today, after the v2 redesign and a year of iteration on top of that initial sprint:

Metric Count
PHP files 204
Controllers 39
Models 23
Services 43
Views 78
CSS files 26
JS files 10
Database migrations 40
Lines of PHP ~48,700
Lines of CSS ~10,700
Lines of JS ~3,700
Claude rules files 15

Why a personal site has this much code

A reasonable read of the table above is "why does a blog have ~48,700 lines of PHP?" The answer is that the blog is one feature out of around a dozen. Here's what those numbers actually power.

Public side. Blog with categories, tags, related posts, and full-text search. Tools directory with ratings and click tracking. Step-by-step guides with modules and content gating. RSS-driven news aggregation with article clustering. About / Now / Contact pages. User accounts with OAuth (Google, GitHub, LinkedIn) and magic-link login. SEO infrastructure: dynamic sitemap, robots.txt, structured data, canonical handling.

Admin, the iceberg. This is where most of the code lives, and it's bigger than the public site. 21 admin controllers covering:

  • Dashboard with server-side analytics (no GA, no third-party tracking)
  • Blog editor, media manager, category and tag management
  • Tools manager with click-tracking review
  • Guides editor with module-level authoring
  • News management (sources, briefings, manual curation)
  • Marketing calendar for scheduling LinkedIn, Twitter and Reddit posts, with status tracking and links back to source articles
  • Sticky-note system for leaving notes attached to any URL on the site
  • Contacts and user management
  • Redirects, migrations runner, content sync (export/import bundles between local and production)
  • Maintenance actions

Infrastructure underneath. Custom MVC with strict typing. Service layer with thin controllers. Migrations system with rollback by batch. Rate limiting on auth and external API calls. CSRF on every state-changing route. A CLI content workflow (pull/diff/push) so AI tools can edit DB content safely without losing analytics or skipping cache invalidation. Build pipeline split into public and admin. Python deploy pipeline. 15 review skill rules that gate every meaningful chunk of work before it merges.

So the ~48,700 PHP lines are doing more than running a blog. They're running a small CMS plus a personal ops layer. Claude Code made building all of that possible in the time I had.

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 of these was a "Claude Code afternoon." Describe what I want, review what comes back, iterate until it's right. The pattern never gets old.

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, but the learning and documenting was the real time investment.

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're not just a director. You're also the quality inspector, the technical writer, and the institutional memory.

What's Next

The site is live and I'm writing content. Blog posts about product management and digital transformation. The tools directory is growing. More guides are in progress.

I plan to keep building in public and updating this post as the site evolves. New content types (journeys, case studies) are in the pipeline, gated behind feature flags and waiting for content.

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

Claude Code doesn't replace knowing what you want to build. It replaces the tedious parts of getting there.

If you want a more structured walkthrough, I've put together a Claude Code Guide that covers a simpler path to a similar outcome.