Appearance

System follows your phone’s setting. Default.

Data layer
Date
Read
11 min
Views

izzy.build costs $0 a month to run. Here's how it's built, and why it isn't a CMS.

Sep 25, 2026 · 11 min read ·

Two briefs, one day of design in Claude Design, one day of build with Claude Code. Static pages on Cloudflare, git as the CMS, and a small API for views, reactions and moderated comments.

01 · The symptom
“A personal site shouldn't need a database, a plugin updater, and a monthly bill.”

Most personal websites carry a database, a plugin updater, a hosting plan, and a login page someone in another country is trying to guess. For a site whose job is to publish writing and collect a few comments, that’s a lot of moving parts to own.

izzy.build has none of them. It runs for $0 a month, serves pages as fast as a phone can draw them, and still has view counts, reactions, moderated comments, a newsletter signup and a contact form. It took two briefs, one day of design in Claude Design, and one working session of build with Claude Code. This post is the whole build, the decisions behind it, and the parts that broke.

Design brief tokens, copy, rules Claude Design one stage at a time I review approve or send back HTML handoff screens, states, OG Dev brief stack, GDPR, targets Claude Code plan, build, test GitHub promptmetrics/izzy.build Cloudflare live at izzy.build DESIGN · SEP 24 BUILD · SEP 25 next stage only after approval Stages: 1 style sheet · 2 post · 3 home · 4 articles · 5 builds, about, contact · 6 OG image handoff + dev brief
Design brief tokens, copy, rules Claude Design 6 stages, one at a time I review each stage approve or send back HTML handoff screens, states, OG + Dev brief stack, GDPR, targets Claude Code plan, build, test GitHub promptmetrics/izzy.build Cloudflare live at izzy.build
Two briefs, two agents. Claude Design turned the design brief into approved screens one gated stage at a time. Claude Code turned the handoff plus the dev brief into the site, pushed to GitHub and deployed to Cloudflare.
02 · The brief

What the site had to do

The job is narrow. When someone hears my name, the site should prove in 30 seconds that I know the gap between connecting an MCP and getting real work out of it, then send them to the right place: the next post, the newsletter, or PromptMetrics.

That turned into a short list of requirements:

  • Writing first. Posts are the product. Each one follows the same four-part shape: the symptom, why the stock tool can’t do it, the fix, what it saved.
  • Engagement. View counts, three reactions, and comments. Comments are moderated: nothing publishes until I approve it.
  • Germany. The site runs from Berlin and collects names and emails, so GDPR applies from day one. No cookies, no stored IP addresses, no data sent anywhere it doesn’t need to go.
  • Fast on a phone. Targets from the dev brief: Lighthouse 95+ on mobile, largest contentful paint under 1.5 seconds, layout shift under 0.05, under 30 KB of JavaScript on a post.
  • No maintenance. Nothing to patch, nothing to restart, nothing that dies the first month I’m busy.
03 · Design

Design first, in Claude Design

I didn’t start with code. I wrote a design brief and gave it to Claude Design: the audience, the brand direction, every color as a named token in light and dark, the typography candidates, the page list, the post template, every engagement component, and real sample copy. No lorem ipsum anywhere.

The part that mattered most was the build order. The brief told Claude Design to work in six stages and stop after each one until I approved it: the style sheet first, then the post page on mobile before desktop, then home, articles, the builds, about and contact pages, and finally the social image template. It also set rules I’d give any designer. Use the tokens exactly, and if you need a new color, propose it and wait. Show light and dark side by side on every screen. Keep the accent blue to links and one primary button. Draw diagrams as simple line drawings in ink and slate. No stock photos, no robots, no glowing brains. Design the empty, loading, pending and error states, not just the happy path.

Stopping after each stage is what kept it honest. A style sheet I didn’t like would have leaked into every screen after it. Catching it at stage one cost minutes. I rejected two of three hero options and one of two type pairings on the way, and every rejection happened before anything downstream depended on it.

What came out was a handoff Claude Code could build from without guessing: a token file, a component stylesheet built only from those tokens, six pages at both 375 and 1280 pixels, four boards of loading, empty, error and success states, and five social image templates. The site’s stylesheet still uses those two files verbatim. When I want to change a color, I change it in one place.

04 · No CMS

Why not a CMS

The obvious options all fail the brief in a different way.

WordPress gives you an admin panel, and with it a database to secure, a PHP runtime, plugins that each need updating, and a login page that attracts automated attacks from day one. That’s a part-time job for a site that publishes every two weeks.

Hosted builders like Webflow or Squarespace remove the maintenance and replace it with rent, and the rent isn’t the real cost. The exit is. I wrote about that in I stopped renting my website: templates, content and analytics fused to the platform, and a rebuild when you leave.

A headless CMS fixes the portability problem and adds another vendor, another account, and another API between my writing and my site.

So izzy.build has no CMS. Git is the CMS. Every post is a markdown file in the repository, with a short block of settings at the top: title, date, rung, the symptom quote. A schema checks those settings at build time and refuses to publish a post with a missing or misspelled field. Version history comes free. So does backup.

Two ways to write feed the same files. On the laptop, I write or Claude Code writes, and a push publishes. On the phone, Pages CMS gives me a form-based editor at izzy.build/admin that commits to the same repository. No database behind either one.

The other reason is agents. A coding agent can read, write and check a markdown file in a repository perfectly well. It can’t reliably click through a WordPress admin panel, and it shouldn’t have to. Content in git is content an agent can maintain.

05 · Architecture

The architecture

The key split: almost everything is a static file, and only a small API runs code.

Reader's browser phone or desktop CLOUDFLARE OUTSIDE CLOUDFLARE Static pages free, no traffic cap Worker: /api/* 100k free requests/day pages, CSS, fonts /api/* only D1 EU region Turnstile bot check Cron daily cleanup Access admin, Izzy only Web Analytics cookieless, no banner Workers Builds builds every push GitHub posts are markdown Claude Code or Pages CMS writes and commits Loops newsletter, double opt-in Resend contact + auto-reply Slack comment alerts
Reader's browser phone or desktop Static pages free, no cap Worker /api/* 100k free/day D1 EU region Turnstile bot check Access admin, Izzy only Cron daily cleanup Workers Builds every push GitHub markdown posts Claude Code or Pages CMS Called by the Worker, outside Cloudflare Loops · Resend · Slack
Pages, styles and fonts are prebuilt static files that Cloudflare serves free with no traffic cap. Only /api/* runs on a Worker, which talks to D1 in the EU and, when needed, to Loops, Resend and Slack. Publishing flows up from GitHub through Workers Builds.

Astro builds every page at deploy time into plain HTML. Cloudflare serves those files from its edge, which means the reader’s phone gets a finished page from a data center near them, with nothing to compute. The styles are inlined into each page, the fonts are self-hosted, and a post ships about 5 KB of compressed JavaScript.

The only code that runs on demand is /api/*: record a view, return the counts, toggle a reaction, save a comment, add a newsletter signup, send a contact message. It runs as a Cloudflare Worker next to a D1 database, a small SQLite database created in Cloudflare’s EU region so the data stays in Europe. The Worker calls out to three services only when it has to: Loops for the newsletter, Resend for contact emails, Slack to tell me a comment is waiting.

Two choices I’d defend. First, Cloudflare over Vercel: Vercel’s free Hobby plan is for non-commercial use, and this site routes readers to a paid service. Second, D1 over Supabase: one vendor instead of two, and the database lives on the same platform as the code that reads it.

06 · Reading

Reading a post

The rule for every post page: the article never waits for the API.

READER CLOUDFLARE EDGE PAGE SCRIPT WORKER API D1 Opens a post on a phone Prebuilt HTML CSS inlined Reads at once nothing waits Loads after the article GET /api/stats one call Views, reactions comments Fills counts space reserved 10 s or 50% scroll Hashes visitor no raw IP One view per visitor, per day If the API is slow or down, the article is already on screen. Counts drop out quietly; reactions show a retry. Layout shift stays at 0.
01 · Reader Opens a post · on a phone 02 · Cloudflare edge Prebuilt HTML · CSS inlined 03 · Reader Reads at once · nothing waits 04 · Page script Loads after · the article 05 · Worker API GET /api/stats · one call 06 · D1 Views, reactions · comments 07 · Page script Fills counts · space reserved 08 · Page script 10 s or 50% · scroll 09 · Worker API Hashes visitor · no raw IP 10 · D1 One view · per visitor, per day A slow API never blocks the article. Counts drop out quietly; layout shift stays 0.
The article arrives as prebuilt HTML and is readable immediately. The page script asks the API for views, reactions and comments afterwards, and fills them into space the layout already reserved. A view only counts after 10 seconds or half a scroll.

The page arrives complete. The reader starts reading. Only then does a small script make one call for the view count, reactions and the first comments, and it drops them into space the layout already reserved, so nothing on the page jumps. If the API is slow or down, the article is already on screen: the view count quietly disappears and the reactions show a retry.

A view counts after 10 seconds on the page or half a scroll, whichever comes first, and known bots don’t count at all. That’s closer to “someone read this” than “someone’s browser loaded this.”

07 · Engagement

Engagement without cookies

Counting views and reactions normally means a cookie, and a cookie in Germany means a consent banner. izzy.build has neither.

Instead, the Worker combines the visitor’s IP address and browser details with a secret that changes every day, and stores only the resulting fingerprint. The raw IP address is never stored. Because the secret changes daily, fingerprints from different days can’t be linked. That’s enough to count one view per person per day and let someone undo their own reaction, and not enough to follow anyone around.

Comments get more protection, because they’re the one place strangers can put text on my site.

READER WORKER D1 SLACK IZZY Posts a comment consent ticked Turnstile + rules 3/hour, plain text Saved pending email never public Sees own comment only in their browser Alert with a link to the queue Reviews queue behind Access Approved or rejected Comment is public name, text, date Erase on request all rows for an email Nothing publishes without approval. Only name, text and date ever leave the API. The email stays in D1 until the commenter asks to delete it.
01 · Reader Posts a comment · consent ticked 02 · Worker Turnstile + rules · 3/hour, plain text 03 · D1 Saved pending · email never public 04 · Reader Sees own comment · only in their browser 05 · Slack Alert with a link · to the queue 06 · Izzy Reviews queue · behind Access 07 · D1 Approved · or rejected 08 · Reader Comment is public · name, text, date 09 · Izzy Erase on request · all rows for an email Nothing publishes without approval. Email never leaves the API.
Every comment passes a bot check and a rate limit, then waits as pending. Slack tells me it's there. I approve or reject it on a moderation page behind Cloudflare Access, and only then does it become public. A deletion request removes every comment stored under one email.

A Turnstile check blocks bots without a puzzle for real people. The Worker allows three comments per visitor per hour, strips anything that looks like HTML, and saves the comment as pending. The commenter sees their own pending comment, stored only in their browser, and nobody else sees it. Slack sends me a link to the moderation page, which sits behind Cloudflare Access: a one-time code to my email, and nobody else gets past the login screen. The page also checks the login itself, so a mistake in the Access settings can’t expose the queue. From the same page, one form deletes everything stored under a commenter’s email, which is how I answer a GDPR deletion request.

08 · Publishing

Publishing

Publishing is a push to GitHub. Everything after that is automatic.

IZZY EDITOR GITHUB WORKERS BUILDS IZZY.BUILD Writes a post laptop or phone Pages CMS or Claude Code Commit on main history = backup Checks posts schema + HTML gate Build stops error names the file Astro build pages, OG, RSS Live in ~1 min last good version A broken post never reaches readers. A missing field, a wrong rung or raw HTML stops the build, and the live site keeps its last good version.
01 · Izzy Writes a post · laptop or phone 02 · Editor Pages CMS or · Claude Code 03 · GitHub Commit on main · history = backup 04 · Workers Builds Checks posts · schema + HTML gate 05 · Izzy Build stops · error names the file 06 · Workers Builds Astro build · pages, OG, RSS 07 · izzy.build Live in ~1 min · last good version A broken post never reaches readers. Live site keeps its last good version.
A post saved from the laptop or the phone lands on GitHub. Workers Builds checks every post, builds the site and deploys it in about a minute. A post with a mistake stops the build with an error that names the file, and the live site keeps its last good version.

The check at the start of each build is the part that lets me hand writing to an agent or edit from a phone without worrying. A missing field, a rung that doesn’t exist, a diagram file that isn’t there, or raw HTML pasted into a post each stops the build with an error that names the file. The live site keeps running its last good version. A broken post never reaches readers.

Each deploy also regenerates the RSS feed, the sitemap, and a social preview image for every post, drawn from the same template Claude Design produced.

09 · Cost

What it costs, and how it scales

The running cost at my traffic is $0 a month. The only bill is the domain. Here’s what each piece allows for free, from the vendors’ pricing pages today:

PieceFree allowanceWhat happens beyond it
Static pages on CloudflareUnlimited requests, not counted against anythingNothing. A viral post costs the same as a quiet one
Worker (/api/*)100,000 requests a dayWorkers Paid, $5 a month for 10 million requests
D1 database5 million rows read and 100,000 written a day, 5 GBPaid usage pricing
Workers Builds3,000 build minutes a monthA deploy takes about a minute
Loops (newsletter)1,000 subscribers, 4,000 emails a monthPaid plan
Resend (contact form)3,000 emails a month, 100 a dayPaid plan
Turnstile, Web Analytics, AccessFree (Access up to 50 users)I need one user

The scaling story is in the first row. Reading is the expensive part of any popular site, and here reading costs nothing and has no cap. What scales with traffic is the engagement API, and only because each post page makes one or two small calls. If a post went properly viral, the risk isn’t a bill. It’s that view counts would stop counting for the rest of the day while every article kept loading, and $5 a month removes that ceiling.

10 · Speed

The numbers

I ran Lighthouse against the live site today, on the heaviest page: the WHOOP post, with three diagrams.

PerformanceAccessibilityBest practicesSEO
Mobile99100100100
Desktop100100100100

Layout shift is 0 and blocking time is 0 ms on both. The whole page weighs 163 KB. Largest contentful paint is 0.5 seconds on desktop and 1.8 seconds on a simulated mid-range phone.

That last number misses my own target. The brief said under 1.5 seconds on mobile, and the site lands at 1.8 on the live network. Two changes got it this far: inlining the styles into each page, which removed a separate request the browser had to wait for, and self-hosting the fonts, which also keeps visitors’ IP addresses away from Google. The remaining gap is the fonts loading on a slow connection, and the fix would show a fallback font for a moment on first visit. I’d rather keep the look. The site is also too new for Chrome’s real-user data, so these are lab numbers and nobody should read rankings into them.

11 · What broke

What broke

It wasn’t smooth, and the breaks are more useful than the wins.

The framework changed under me. Astro had just shipped version 7 with a new default markdown engine, and the plugins that turn a post into the numbered-section layout didn’t run on it. The fix was to switch back to the older engine explicitly. Reading the current docs instead of trusting memory caught it in minutes.

A safety check that only warned. The rule that rejects raw HTML in a post logged an error, and the build carried on and published the broken page anyway. I only found it because I tested the failure on purpose. Now a separate check runs before every build and stops it.

Three minutes offline. While connecting the izzy.build domain, the temporary address went down. The deploy tool reads a copy of the configuration made at build time, not the file I’d just edited, so my fixes didn’t apply until I rebuilt. The lesson is now written into the repository’s notes for the next agent that works on it.

Two quiet URL bugs. Canonical links pointed search engines at .html addresses instead of clean ones, and the RSS feed added trailing slashes the pages don’t use. Neither shows up in a browser. Both would have split search signals across two addresses for the same page. A scan of the built output caught them before search engines did.

Every one of these crossed layers: framework, build tooling, DNS, SEO. That used to mean four specialists and a ticket queue. Here, one session held the whole picture, and my job stayed the same as always: define what good means, review what comes back, and refuse to ship what I can’t defend.

12 · Steal this

Steal this

If you’re rebuilding a personal or small-company site this year, this is the pattern:

  1. Design before code, in gated stages. Write the brief, name every token, and make the design tool stop for approval after each stage.
  2. Git is the CMS. Markdown files, a schema that refuses bad posts, and a phone editor if you need one.
  3. Static by default. Prebuild every page and serve it from the edge for free. Only the interactive parts run code.
  4. The article never waits. Load engagement after the content, and reserve the space so nothing moves.
  5. Privacy by construction. No cookies, no stored IPs, moderation behind a real login, deletion in one step.

If you’ve built yours differently, or you tried this and hit a wall I didn’t, tell me. That’s the more useful story.

13 · What it saved
$0
per month to run, at my traffic
99–100
Lighthouse, every category, mobile and desktop
Reactions
Subscribe

Get the next MCP-gap post in your inbox. Every two weeks, no fluff.

More on the Data layer rung
Comments

Comments

Loading comments…

    Leave a comment

    Not shown publicly.
    Work with
    Done-for-you buildsWork with PromptMetrics →