How I built my site with no frameworks and Claude Code (PHP, vanilla JavaScript)

imisic1 pts0 comments

How I Built This Site With Claude Code | Ivan Misic | Ivan Misic

Skip to main content

station · ivanmisic.net

log in

IM

IVAN MIŠIĆ

product · tech · ai

Search archive<br>⌘K

★ latest ship

The Claude Code tools I'm sharing

№37 &middot; 8 min &middot; read →

▸ start here

Claude Code Guide

beginner guide &middot; zero to a live site →

log in

§01<br>AI & Tools

↵ back to ai & tools

On This Page

Why Build From Scratch?

The Stack

Architecture

The Brutalist Design

Working With Claude Code

The Numbers

What I'd Do Differently

What's Next

Contents

01<br>Why Build From Scratch?<br>13%

02<br>The Stack<br>25%

03<br>Architecture<br>38%

04<br>The Brutalist Design<br>50%

05<br>Working With Claude Code<br>63%

06<br>The Numbers<br>75%

07<br>What I'd Do Differently<br>88%

08<br>What's Next<br>100%

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:

No backend frameworks. No Laravel, no Symfony. Custom MVC from scratch.

No frontend frameworks. No React, no Vue, no jQuery.

No CSS frameworks. No Tailwind, no Bootstrap. Custom design system with tokens.

Build it properly. Database migrations, service layer, feature flags, deployment pipeline. Not a toy project.

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<br>Technology<br>Details

Backend<br>PHP 8.2+<br>Custom MVC framework with strict typing

Frontend<br>Vanilla ES6+<br>~3,700 lines across 10 focused scripts

CSS<br>Custom design system<br>26 files, ~10,700 lines, BEM + utility classes

Database<br>MySQL<br>40 migrations tracking schema changes

Server<br>LiteSpeed<br>With cache integration for production

Deployment<br>Custom pipeline<br>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<br>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)<br>$result = BlogPostService::create($data);<br>if ($result->failed()) {<br>return $this->error($result->errorString());

// Service (business logic)<br>public static function create(array $data): ServiceResult {<br>$data = self::normalizeData($data);<br>$errors = self::validate($data);<br>if (!empty($errors)) return ServiceResult::failure($errors);<br>// slug generation, cache invalidation, reading time calc...<br>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<br>FEATURE_TOOLS=true<br>FEATURE_GUIDES=true<br>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...

code claude build from built design

Related Articles