How I shipped this blog in a couple of hours using Claude Code — Astro, Keystatic, Cloudflare for free.
The stack, the prompts that mattered, and how I actually work with Claude Code — directing rather than coding.

A quick intro about this post.
First post on the blog — a Russian-nesting-doll situation: this post is about how I shipped the blog you’re reading. The next one is the first actual technical writeup, about building the KEEP pre-Kickstarter landing page over a weekend with Claude Code and Nano Banana.
Why starting a blog now.
I’ve been operating consumer hardware businesses for ten years, mostly without a public-facing personal channel. Lovebox shipped 300,000+ units across paid acquisition, retail, and PR — there was plenty of marketing. I just never made the time to build a personal brand on top of that.
What changed is that I actually want to do this now. Building in public gives me energy I didn’t realize I needed: being in direct contact with the people who use what I make, and with other creators trying to do the same thing. Ten years into this, it also feels like the right moment to start giving something back — writing down what I’ve learned, sharing what’s actually working in 2026, the failures alongside the wins.
AI does make it noticeably cheaper and faster to run a blog like this, the writing included — but that’s the enabler, not the reason. The reason is wanting to be in the conversation.
I also have a thesis now (AI bridging the gap between creativity and the market) and a small set of products to talk about. Writing in public connects them all.
The trigger.
Monday afternoon, I posted on LinkedIn that I was going to start building in public. By the time I closed the tab I realised I’d committed to something I hadn’t actually set up. The LinkedIn post links to a LinkedIn profile, but the long-form writeups I want to do — the field-guide kind — don’t live well on LinkedIn. I needed a blog as the anchor. And every hour the gap between announcing it and having one widened.
The stack — and why I didn’t shop around.
The architecture comes from How I built this blog with Astro, Keystatic and Claude Code, a post someone in the Claude Code community had shared a few days earlier. It described — in architecture, not full code — exactly the shape I wanted: Astro 5, MDX, Keystatic, Cloudflare. I made small adjustments, but the spine is theirs. Worth a read if you’re building something similar.
The pieces:
- Astro 5 — file-based routing, content collections, MDX support out of the box. Static output by default.
- Tailwind v4 via the Vite plugin — same brand tokens already declared in my KEEP repo, copied over.
- MDX for posts. The piece I want to spend a beat on — more below.
- Keystatic as the CMS, in local mode only. File-based, no database, no auth, no hosted service.
- Cloudflare Pages for hosting. Free tier, auto-deploys on push to
main.
Why Astro 5 over the alternatives.
A short version of why this stack and not Next.js / Notion-as-CMS / Substack / Ghost:
- Content-first, not app-first. Next.js is designed for apps that happen to have a blog attached. Astro is designed for content sites that occasionally have an app embedded. For a writing-focused blog, the second framing is closer to what I’m doing.
- Static output by default. Astro ships zero JavaScript to the browser unless I explicitly opt in. The blog you’re reading loads in under 200ms — no React runtime to hydrate, no client-side router, no bundle of widgets.
- File-based, not database-based. Every post is an
.mdxfile insrc/content/posts/. Git is the version control. The filesystem is the database. No external service to keep alive, no migration to run, no vendor to depend on. - No lock-in. The day I want to move this blog to a different framework, the
.mdxfiles come with me as plain text. Try migrating away from Substack or Notion.
Why MDX, specifically.
Content format mattered more than I expected. MDX — Markdown + JSX — is what makes the authoring experience pleasant instead of painful.
The frontmatter at the top of any post is just YAML — title, deck, date, draft flag:
--- title: "How I shipped this blog in a couple of hours using Claude Code." deck: "The stack, the prompts that mattered, and how I actually work with Claude Code." date: 2026-05-18 readingTime: "~7 min read" draft: false ---
The body is identical to how I’d write a README on GitHub — ** for bold, [text](url) for links, - for list items:
## Why pre-Kickstarter pages matter. Kickstarter mostly pushes projects that get backers in the first 24 hours. **Launch cold and you die.** The pre-launch page exists to prevent exactly that — it collects emails before launch day so day one isn't day one.
The interesting part is when I want something the default Markdown vocabulary can’t express. The highlighted prompt blocks you’ve been reading throughout these two posts — dark background, yellow highlights on key phrases, a Tip 01 chip in the corner — are a custom Astro component called <Prompt>. From inside an MDX file, I use it like a tag:
<Prompt html={`<mark>https://en.lovebox.love/products/keep-beta-test-version</mark><span class="tip-tag">Tip 01</span>
Build a pre-Kickstarter landing page for KEEP. <mark>First, study how the best pre-Kickstarter pages convert</mark><span class="tip-tag">Tip 02</span>.`} />
That’s the actual MDX source from the KEEP landing-page post. The component itself is ~12 lines in src/components/Prompt.astro. After that I can drop a styled block into any post without reaching for HTML or copy-pasting CSS. Same goes for the stack-card in Technical notes below — a small chunk of inline JSX in the MDX file.
The blank page is friendly. No CMS to log into, no toolbar to navigate, no “what’s the keyboard shortcut for a callout box” friction. Just type. Keystatic is wired up if I ever want a GUI (it lives at /keystatic in dev), but I haven’t used it for either of the two posts live here — the .mdx file is the source of truth and editing the file is faster.
The build, prompt by prompt.
Two main prompts to Claude Code, plus one for the scheduling detail. The first scaffolded the project. The second pulled KEEP’s visual identity over. The third added a small feature I realized I wanted halfway through.
Prompt 1 — scaffold the blog from the tutorial reference.
Reference: https://allthingstech.ch/blog/how-i-built-this-blog-with-astro-keystatic-and-claude-code/ Build a personal blog at ~/Documents/GitHub/blog-jean-gregoire. Same stack as my keep-landing-page repo: Astro 5 + Tailwind v4 + MDX. Add Keystatic as the CMS, local mode only — admin at /keystatic, 404 in production. Deploy target: Cloudflare Pages. Before writing any code, study the tutorial's architecture and propose a file layout. Create a DESIGN.md file derived from my keep-landing-page DESIGN.md, and a CLAUDE.md framing your role as a brand-aware writing partner — referenced from ~/Documents/GitHub/life-manager-data/context/me/personal-brand.md. Initialize with one MDX post I'll paste in, plus a short about-me page.
Three things in that prompt I’d point at as load-bearing:
- The tutorial reference at the top. Not as a step-by-step to follow — as a starting architecture so Claude doesn’t have to invent one from scratch.
- The “same stack as my keep-landing-page repo” line. That single phrase pulled in Claude’s familiarity with the other project — file layouts, import conventions, the way I write Astro components. Familiarity is the most under-rated time-saver in an AI build.
- The DESIGN.md instruction. Brand tokens written before the UI. Every later iteration had a file to check against, which is how the blog ended up visually consistent with KEEP without me policing every CSS commit.
The scaffold was quick — maybe twenty minutes from npm create to a working build at localhost:4321 with the about page and a placeholder post live. Most of the session was the long tail of iteration that came after.
Prompt 2 — pull KEEP’s visual identity over.
Out of the box, Astro gives you a perfectly competent editorial blog. That wasn’t what I wanted — I wanted the pixel-arcade flavor of KEEP, so the blog feels like another room in the same house. I pointed Claude at the KEEP landing-page repo and gave it one prompt:
Iterate the design toward the KEEP landing page's pixel-arcade flavor. The current look is too generic-editorial. Reference repo: ~/Documents/GitHub/keep-landing-page — match the masthead, palette, and visual accents. Take screenshots with Playwright MCP between iterations and verify the layout yourself before pinging me.
The highlighted line is the single biggest unlock for visual iteration with Claude Code. Once Playwright MCP is wired up, Claude can navigate to localhost, screenshot at multiple viewports, and review its own work before claiming a change is done. Pixel-alignment issues that used to take three rounds of “no, the arrow is at the bottom of the line” now resolve in one round, because Claude catches them itself.
There were still moments where I had to point at a screenshot and say “this triangle is misaligned.” Visual feedback from a human is still the fastest signal for the last 20% of polish. But for the bulk of it, the loop runs without me.
Prompt 3 — a small detail: future-dated posts.
About halfway through, I realized I’d want to write posts ahead of time and have them go live on a chosen date — without me hitting publish. Standard blog feature, but I couldn’t think of a clean way to do it with a pure-static stack, so I asked:
I want to date posts in the future and have them auto-publish on that date. Add the simplest possible scheduling that works with the current static-build setup.
Claude came back with two pieces. A filter on the content collection:
const showFuturePosts = import.meta.env.DEV || import.meta.env.PUBLIC_PREVIEW === "true";
const now = new Date();
const posts = await getCollection("posts", ({ data }) => {
if (data.draft) return false;
if (!showFuturePosts && data.date > now) return false;
return true;
});
That alone hides future posts in production. But — and Claude flagged this immediately without me asking — the filter runs at build time, not at request time. Static site. So without something to retrigger the build, a post dated May 20 stays invisible forever on a production deployed May 18.
The second piece — a five-line GitHub Actions cron at .github/workflows/daily-rebuild.yml:
name: Daily rebuild for scheduled posts
on:
schedule:
- cron: "1 0 * * *"
workflow_dispatch:
jobs:
trigger-rebuild:
runs-on: ubuntu-latest
steps:
- run: curl -fsS -X POST "${{ secrets.CLOUDFLARE_DEPLOY_HOOK }}"
Every night at 00:01 UTC the cron POSTs to a Cloudflare Pages Deploy Hook. Pages rebuilds. The filter re-evaluates with the new “today”. Any post that just crossed its date appears.
(One manual step Claude can’t do: create the Deploy Hook in Cloudflare Pages settings and paste the URL into a GitHub Actions secret called CLOUDFLARE_DEPLOY_HOOK. Two minutes of clicking. Documented in the repo README so future-me doesn’t have to remember.)
The post that comes after this one — the KEEP landing-page writeup — is set up that way. It’s dated May 20, invisible on the May-18 production build that ships this intro post. On May 20, the nightly rebuild picks it up and it goes live.
The iteration arc.
Honest split: the scaffold was 20% of the work. The other 80% was iteration. Per session:
- Voice corrections paragraph by paragraph. Several rounds on the homepage bio alone.
- Migrating the KEEP post from HTML draft to MDX, with the custom
<Prompt>component for highlighted prompt blocks. - Design tweaks to pull KEEP’s visual identity over — ribbon, dividers, typography mix.
- Mobile layout fixes — masthead wordmark wrapping, hero arrow misalignment.
- Triangle alignment saga — Press Start 2P doesn’t ship a ▶ glyph, so the fallback font kept breaking the baseline. Ended up inline SVG.
How both posts on this blog got written.
Neither post started as “let me sit down and write a blog post.” Both fell out of work I was already doing, by two different routes.
Post 1 — this post, the blog intro.
Written in the same Claude Code session that built the blog. Every prompt, iteration, UI bug, voice correction — all already in the session memory. Zero notes taken during the build, and I didn’t need any.
Bonus: the draft for Post 2 (the KEEP writeup) had been sitting in my notes for a week, already voice-corrected over ~twenty rounds. So I didn’t have to teach Claude how I write — the example was right there. Match this. Claude did.
One prompt:
Based on our whole conversation, write a blog post about how I created this blog in a couple of hours and got it published for free on Cloudflare. Same style as the KEEP landing-page draft sitting in my notes. Be honest about what was hard.
First draft came back in one go. ~Ten rounds of voice correction after that — “don’t blame the cost too much”, “be honest about how many visual rounds it really took”, “credit the author of the tutorial I borrowed the stack from.” Faster than Post 2 because the substance was already accurate AND the voice was already calibrated — tuning, not building.
Post 2 — 5 tips for a pre-Kickstarter landing page.
Started as a YouTube video — the first episode of a channel I’m launching, Consumer Hardware is Easy. Screen-recording walkthrough of building the KEEP pre-Kickstarter page over a weekend with Claude Code + Nano Banana. After the shoot I had a transcript, and figured video and post should ship together — video as talking-head sketch, post as the field guide that backs it up.
Three inputs to Claude: the YouTube transcript, the path to the keep-landing-page repo, and a one-sentence brief — “turn this into a long-form post in the style of a hacker-news field guide, for consumer-tech creators.” First draft came back as an HTML file — the blog didn’t exist yet. Then ~twenty rounds of voice iteration plus direct edits — often faster to rewrite a paragraph by hand than to describe what was wrong: “less arrogant”, “too literary”, “drop the manifesto opening”, “use ‘I’ not ‘we’”, “tighten the prompts block.” Substance from the video. Voice from the rejections.
The HTML draft sat in my notes for a few days. When I scaffolded the blog, the migration to MDX took minutes — most of the HTML mapped to Markdown directly, the custom prompt blocks became the <Prompt> component, done.
Two patterns, if you want to copy them.
Pattern 1 — for build-in-public posts: write in the same Claude Code session as the work. The session memory is the brief. You’re not authoring a post from scratch — you’re asking Claude to narrate work it just helped you do, then telling it where to tighten. Voice work still yours. Blank-page problem gone.
Caveat: a sprawling session gets token-expensive — every new prompt re-processes the full history. Better practice (which I’ll switch to next time): at the end of the build, ask Claude for a structured summary plus the path to the session transcript. Commit the summary. Open a fresh session for the post, with the summary as the brief and the transcript path as a fallback if Claude needs a specific detail. Same context, no token tax.
Pattern 2 — for posts that back a video: feed Claude the transcript and the repo it’s about. The transcript does two jobs at once. It carries the story — you already figured out the arc when you recorded it. And it carries your voice — sentence rhythm, asides, where you push. Better voice reference than any style guide you’d write. Match the way I talk in this transcript. Claude can.
Then point at the codebase the video was about. The transcript is fifteen minutes of camera; the repo is the long tail that didn’t make the cut — prompts in full, file paths, gotchas skipped for pacing. Long-form needs both.
Technical notes.
For the curious, if you want to copy the setup:
► The stack
- Site generator
- Astro 5, static output, no adapter.
- Styling
- Tailwind v4 via Vite plugin. Brand tokens shared with my KEEP repo via
DESIGN.md. - Content
- MDX in
src/content/posts/. Future-dated posts gated byimport.meta.env.DEVorPUBLIC_PREVIEW=true. - CMS
- Keystatic, local mode only — admin at
/keystaticin dev, 404 in production. No database, no auth, no infra cost. - Hosting
- Cloudflare Pages, free tier. Auto-builds on push to
main. Custom domain wired to jeangregoire.com. - Scheduled posts
- A GitHub Actions cron (
.github/workflows/daily-rebuild.yml) POSTs to a Cloudflare Pages Deploy Hook every night at 00:01 UTC. - SEO
- Sitemap, OG, Twitter Card — all wired up by Claude in one shot.
- Brand discipline
DESIGN.mdat the root with YAML brand tokens, derived from the KEEP one.CLAUDE.mdnext to it, following the agents.md convention.- Visual QA
- Playwright via the Playwright MCP — Claude takes screenshots between iterations and reviews them itself.
Total time on the actual build: about three hours of focused work, split across Monday afternoon and Tuesday morning. KEEP post migrated, blog live, this post dated to the day after.
Marginal spend: $0. Cloudflare free tier covers everything. The AI stack I run for this and every other project is about $300/month.
What’s next?
The first real technical post — 5 tips for building a pre-Kickstarter landing page with Claude Code in a couple of hours — goes live two days from now, on May 20. It’s about how I built the KEEP pre-Kickstarter landing page over a weekend — the actual prompts, the iteration arc, the small tricks that made it work.
After that, the topic ideas are on the homepage. All consumer-hardware-specific: iterating on hardware design with Nano Banana before the mechanical engineer touches CAD, turning shady NFC datasheets into working iOS and Android modules, briefing a PCB engineer with Claude Code, designing product packaging without a packaging studio.
If one of those would be useful to you, or there’s something I haven’t listed, drop me a line at me@jeangregoire.com. I read every email.