AI News Hub Logo

AI News Hub

Adding comments to a PHP blog without a database

DEV Community
Odilon HUGONNOT

The first article on this blog explained how it was built in 30 minutes with Claude Code. Naturally, a blog needs comments. Same constraints: no database, no external dependencies, no Disqus tracking visitors. Just PHP + JSON files. Built in one session with Claude Code — the interesting part wasn't the code, it was the security audit that followed. A comment system without a database seems trivial. It almost is. But "almost" hides a few classic pitfalls — some of them introduced directly by the speed of AI agents. The final result fits in ~300 lines total. What follows is the journey, not just the destination. Comments stored in JSON files (1 per article) Immediate publication — no moderation queue for a personal blog Anti-spam without captcha (zero friction for humans) GDPR: no email, no full IP stored Design integrated with the existing blog (same CSS system) Zero external dependencies Three places in the codebase: blog/ ├── comments/ │ └── {slug}.json # 1 file per article, created on first comment ├── posts/ │ └── comment-handler.php # Single POST endpoint └── template.php # Modified blog_footer(): comments + form The JSON storage format per article looks like this: [ { "id": "a3f2b1c4", "author": "Jean Dupont", "date": "2026-02-22T14:32:10+01:00", "content": "Great article, thanks.", "ip_hash": "9f86d081884c7d65" } ] Each field has a reason. id: a sha256(uniqid()) truncated to 8 characters — unique enough for a personal blog, no need for a full UUID v4. ip_hash: hash of only the first two octets of the IP (see next section). content: stored raw, escaped on display with htmlspecialchars() — never store HTML. Why honeypot over captcha: zero friction, no external service, stops the vast majority of bots. A hidden field in the form that humans don't see and don't fill in. Bots fill it every time: .hp-field { display: none !important; } Important detail: rejection returns a normal redirect to the article page, with the #comments anchor. No 403, no error message — nothing that confirms the filter exists. The bot receives a 302, as if it had succeeded. Rate limiting: max 3 comments per IP prefix per 10-minute window, checked by scanning the existing JSON. The IP hash calculation handles both IPv4 and IPv6: if (str_contains($ip, ':')) { // IPv6: take the first 3 groups (network /48) $groups = explode(':', $ip, 4); $ipPrefix = implode(':', array_slice($groups, 0, 3)); } else { // IPv4: take the first 2 octets $octets = explode('.', $ip, 3); $ipPrefix = ($octets[0] ?? '0') . '.' . ($octets[1] ?? '0'); } $ipHash = substr(hash('sha256', $ipPrefix), 0, 16); Only the network prefix is hashed — not the full IP. Impossible to recover the individual address, but possible to detect abuse from the same network. This is the right balance for GDPR: protection without over-collection. comment-handler.php is the sole endpoint that receives submissions. The validation chain in order: GET method → redirect to /blog/ (no direct access) Honeypot not empty → silent redirect (fake confirmation) CSRF token — timing-safe comparison with hash_equals() Slug validated by regex [a-z0-9-]+ (path traversal prevention) Corresponding article file exists Author: 2–50 characters, content: 10–2000 characters Rate limit: 'janvier', 2 => 'février', 3 => 'mars', 4 => 'avril', 5 => 'mai', 6 => 'juin', 7 => 'juillet', 8 => 'août', 9 => 'septembre', 10 => 'octobre', 11 => 'novembre', 12 => 'décembre' ]; $date = new DateTime($comment['date']); $formatted = $date->format('j') . ' ' . $months[(int)$date->format('n')] . ' ' . $date->format('Y'); Editorial note: 3 of these 5 problems were introduced by the initial parallel implementation (Sonnet agents working simultaneously on different parts of the code). The audit found all of them. The takeaway: AI-assisted code requires the same rigor of review as human-written code. Maybe more, because it arrives fast and looks correct. The complete system is ~120 lines of PHP for the handler, ~60 lines of additions in the template, ~130 lines of CSS. No database, no npm install, no build step. Deployed by copying files. The real value wasn't in writing the code — any PHP developer can write this in an afternoon. It was in the iterative security review: implement quickly, then audit methodically by trying to break things. With Claude Code, both fit into the same session — write, then immediately switch to adversarial mode. For a personal blog, that's the right level of engineering. No more.