The blog you're reading right now was built in a single conversation with Claude Code, Anthropic's CLI, in about 30 minutes. No all-nighter, no purchased template, no WordPress. One working session in the terminal. Here's exactly how it went — real code, real commands, and what almost went wrong.
Why add a blog to a portfolio?
My portfolio web-developpeur.com does its job: showcasing my background and projects. But a static site has its limits from an SEO standpoint. Google loves fresh content, pages that answer real questions, sites that evolve.
The blog's goal: build a semantic cluster around the portfolio.
- Articles on the technologies I use (Golang, PHP, Vue.js, DevOps)
- Concrete lessons learned from real projects
- Content I'd want to write anyway
Each article = a real indexable URL, with its own meta and Open Graph tags. No database, no CMS, no maintenance.
Why not WordPress?
A legitimate instinct. But for this use case, it's using a sledgehammer to crack a nut. My constraints:
- No database to manage on the server
- No plugins to update (attack surface)
- Clean integration with the existing site (Apache + PHP)
- Maintainable solo with Claude Code in two minutes
The solution: PHP files for articles, a JSON manifest for the index, and a shared template. Each article is a real page with its own URL, SEO tags, and syntax highlighting. No unnecessary abstraction layer.
The complete architecture
Here's the final project structure:
blog/
├── index.php # Listing page with JS search + filters
├── posts.json # JSON manifest: metadata for all articles
├── template.php # blog_header() and blog_footer() functions
├── .htaccess # Clean URLs: /blog/my-slug → posts/my-slug.php
├── assets/
│ └── blog.css # Styles: nav, cards, article, code blocks
└── posts/
└── my-article.php # One PHP file per article
The posts.json file
This is the heart of the system. Each article is registered here with its metadata.
It's the file the index page loads via fetch() to power
real-time search and category filters.
[
{
"slug": "creer-un-blog-avec-claude-code",
"title": "Créer un blog sans CMS avec Claude Code",
"date": "2026-02-22",
"category": "Retour d'expérience",
"tags": ["claude-code", "ia", "php", "no-cms", "seo"],
"excerpt": "Comment j'ai construit ce blog en 30 minutes..."
}
]
To add an article: create the PHP file + add one line to this JSON. That's it.
The template.php
Two PHP functions that wrap each article: blog_header() generates
the complete <head> with meta tags, Open Graph, Twitter Card,
and loads Bootstrap, Font Awesome, Prism.js (CDN). blog_footer()
closes the page and loads scripts.
<?php
// In each article:
require_once '../template.php';
blog_header(
'Article title',
'Meta description for Google and Open Graph',
'https://www.web-developpeur.com/blog/my-slug'
);
?>
<article class="blog-article">
<h1>My title</h1>
<div class="article-content">
<p>Content...</p>
<!-- Automatic syntax highlighting via Prism.js -->
<pre><code class="language-go">
func main() {
fmt.Println("Hello, blog!")
}
</code></pre>
</div>
</article>
<?php blog_footer([]); ?>
The .htaccess for clean URLs
Without rewriting, URLs would be /blog/posts/my-slug.php. Not great.
Two rules in the root .htaccess are enough:
# In the site's root .htaccess
RewriteRule ^blog/?$ blog/index.php [L]
RewriteRule ^blog/([a-z0-9-]+)$ blog/posts/$1.php [L]
Result: /blog/ points to blog/index.php,
and /blog/creer-un-blog-avec-claude-code points to
blog/posts/creer-un-blog-avec-claude-code.php.
Client-side search
No server request for search. The index page loads posts.json
once, and filtering happens in pure JS:
fetch('posts.json')
.then(res => res.json())
.then(posts => {
// Sort by descending date
allPosts = posts.sort((a, b) => new Date(b.date) - new Date(a.date));
render(allPosts);
});
function filterPosts() {
return allPosts.filter(post => {
const matchCat = activeCategory === 'Tous' || post.category === activeCategory;
if (!matchCat) return false;
if (!searchTerm) return true;
const term = searchTerm.toLowerCase();
return post.title.toLowerCase().includes(term)
|| post.excerpt.toLowerCase().includes(term)
|| post.tags.some(tag => tag.toLowerCase().includes(term));
});
}
Claude Code: how it actually works
Claude Code is a CLI that runs in the terminal. It has access to your filesystem, can read, write, modify files, and execute bash commands. It's not a chat where you copy-paste code: it's an agent that works directly on the codebase, like a developer with access to your machine.
To install it:
npm install -g @anthropic-ai/claude-code
claude # Launch the CLI in the terminal
My workflow: tech lead + agent
I play the tech lead role. I describe the architecture and make decisions. Claude executes. For heavy tasks (writing multiple files in parallel), the Opus model delegates to Sonnet agents that work simultaneously.
For this blog, I started by describing the complete architecture in one message: which files to create, each one's role, design constraints (same colors as the CV, Bootstrap 3, Prism.js for highlighting), existing files to modify. Claude drafted a detailed plan, I approved it, and it created everything in one pass.
Total duration: ~30 minutes. Breakdown:
- ~10 min — describing the plan + approval
- ~10 min — creating all files (Sonnet agents in parallel)
- ~10 min — design fixes (see next section)
For testing during development, I used the built-in PHP server:
# At the CV project root
php -S localhost:8000
# Then in the browser:
# http://localhost:8000/blog/
# http://localhost:8000/blog/posts/creer-un-blog-avec-claude-code.php
Note: clean URLs (
/blog/my-slugwithout.php) only work with Apache and mod_rewrite active. Locally with the built-in PHP server, you need to pass a router file that simulates the rewrites:php -S localhost:8000 router.php.
What almost went wrong: desynchronized CSS classes
The first visual render was broken. Navigation as a bulleted list, unstyled buttons, tags crammed together. Inspecting the source, the problem was obvious:
/* Generated CSS — selectors that match nothing */
.blog-listing .post-card { ... }
.blog-nav .navbar-brand { ... }
<!-- Generated HTML in parallel — different classes -->
<article class="blog-card">...</article>
<a class="blog-nav-brand">Odilon Hugonnot</a>
Two Sonnet agents had written the CSS and HTML in parallel. Result: class names silently diverging. No PHP error, no JS error — just CSS that applies to nothing.
The fix illustrated something important: with an AI tool, visual feedback is more efficient than description. I shared a screenshot. Claude compared the CSS classes against the HTML classes and rewrote the CSS to match. Two minutes. Without the screenshot, we could have gone in circles for a long time.
Conclusion
Alone, without an AI tool, this blog would have taken me half a day. Not because it's complex, but because of the usual friction: finding the right files, writing the CSS from scratch, testing the Apache rewrites, debugging PHP includes.
With Claude Code: 30 minutes.
It's not magic. It requires knowing precisely what you want, describing it technically, and reviewing what gets produced. But for a senior developer who knows their stack, it's a genuine productivity multiplier.
The blog is live. It will host real lessons learned from concrete projects. That's exactly what it was built for.