How We Built This: Deploying an AI-Maintained Blog in a Day
From a blank conversation about tech stacks to a live FREE website deployment — the full story of how halfface.ai went from idea to production in a day, with every decision made alongside an AI.
This blog is about building with AI. So it’s only right that the first real post is about how the blog itself was built — with AI, from scratch, in a single day.
Not a tutorial in the traditional sense. More of an honest account of what that process actually looks like: the initial questions, the decisions, the things that broke, and the moment where I asked an AI to write the post explaining how the AI helped build the blog that you’re currently reading.
It started with a conversation
The first thing I did wasn’t write any code. It was ask questions.
Specifically: what should the tech stack be for a content-first blog that I intend to maintain primarily through AI agents? Not can an AI write a Next.js app — obviously it can. But what stack would make that the most reliable over time? What would age well? What would break least when the AI generating my components has slightly different conventions one session versus the next?
The conversation covered four main candidates for the framework layer: Next.js, SvelteKit, Eleventy, and Astro. Each of them can build a blog. But the constraint wasn’t can it build a blog — it was what does an AI agent produce most consistently when it’s asked to add a component three months from now.
Next.js was ruled out quickly. It’s a great framework, but it’s also a complex one. Cloudflare Workers support via next-on-pages has real friction. And complexity means more failure modes when the agent working on it doesn’t have full context of decisions made six months ago.
SvelteKit fell off for a simpler reason: it’s less represented in the training data of the models I’d be using. That’s not a knock on Svelte — it’s just a practical observation about where AI assistance is most reliable today.
Eleventy was interesting but has no component model. The site would inevitably get repetitive raw HTML as it grew, which creates the exact drift problem I was trying to avoid.
Astro won for three reasons. First, its Content Collections API maps almost perfectly to the way I already write: notes in Obsidian, markdown files with frontmatter. Second, it ships zero JavaScript by default, which means excellent Core Web Vitals without having to think about it. Third, it has an official @astrojs/cloudflare adapter, which was a hard requirement.
The stack decisions, one by one
Once the framework was settled, the other decisions fell into place quickly.
Styling: Tailwind CSS. The reasoning here was almost entirely about AI consistency. Tailwind utility classes are extremely well-represented in model training data. When I ask an AI to style a component, it produces coherent Tailwind output reliably. If I had chosen a custom CSS approach or CSS Modules, I’d be managing naming conventions across sessions with no guarantee of consistency. Tailwind eliminates that problem.
Content format: plain Markdown. Not MDX. The publishing pipeline I had in mind was: write in Obsidian → AI polishes and formats → push to GitHub. Markdown is what Obsidian writes natively. MDX would require managing component imports in posts, which adds complexity for no real benefit at launch. MDX is available per-post if a tutorial genuinely needs an interactive element, but it’s not the default.
CMS: the git repository itself. Evaluated whether I needed a headless CMS — Contentful, Sanity, something with an admin UI. Decided no. Git push is already the deploy trigger. Markdown files in src/content/blog/ are directly editable by AI agents without any API authentication overhead. The draft: true frontmatter field handles staging. Obsidian is the writing UI. Adding a CMS would have introduced another service, another credential, and another thing to break.
Hosting: Cloudflare Workers. This was a pre-existing decision. Cloudflare has unified their Workers and Pages products, and the combination is genuinely excellent for a static site: global CDN, generous free tier, automatic deploys from GitHub.
Newsletter: embeddable form, decision deferred. Beehiiv and Kit are both good options. Rather than pick one before the site had any content, the component was scaffolded as a placeholder — NewsletterForm.astro exists as a slot that an embed script can drop into.
The build: nine phases in one session
Once the decisions were documented (in Architecture Decision Records, committed to the vault), the actual build began. The checklist had nine phases:
- Initialize the Astro project and install dependencies
- Set up the Content Collections schema
- Build the components (BaseHead, Header, Footer, PostCard, TagFilter, NewsletterForm)
- Build the layouts (BaseLayout, PostLayout)
- Build the pages (index, blog/[slug], tags/[tag], about, rss.xml)
- Wire up the SEO and AEO layer (JSON-LD schema generators, sitemap, canonical URLs)
- Add public static files (favicon, robots.txt, llms.txt)
- Write the code-repo CLAUDE.md — agent instructions for future sessions
- Build, verify, push, confirm deployment
The parts that went smoothly: almost all of it. Astro is genuinely pleasant to scaffold. The component structure is clean, the Content Collections API is well-designed, and the Cloudflare adapter worked on the first try.
The part that didn’t go smoothly: the Cloudflare Workers deployment.
The _worker.js problem
Here’s the specific thing that broke and why it matters.
When @astrojs/cloudflare builds an Astro site, it generates a dist/_worker.js/ directory. This directory contains the Worker entrypoint — the server-side code that Cloudflare runs. But Cloudflare’s tooling (wrangler) also tries to upload the contents of dist/ as static assets. And wrangler refuses to upload _worker.js/ as a static asset, because it recognizes it as a Worker bundle.
The fix required two things. First, a public/.assetsignore file containing _worker.js, which tells Cloudflare not to treat that directory as a static asset. Second, the wrangler.jsonc config needed explicit entries: main: "./dist/_worker.js/index.js" pointing at the Worker entrypoint, and assets.binding: "ASSETS" wiring up the static asset binding.
Once both were in place, it deployed cleanly. But it took a full debugging cycle to get there — examining the build output, reading wrangler error messages, reasoning through what the adapter was actually generating versus what wrangler expected to see.
This is the kind of thing that isn’t in any tutorial, because it’s at the intersection of two tools that both changed recently. Cloudflare merged Workers and Pages. Astro released v5 with a rewritten adapter. The combination produces a deployment quirk that you only hit if you’re using both in their current form.
The SEO layer, because it matters from day one
One thing I was firm about: the full SEO and AEO stack goes in at launch, not later.
It’s easy to tell yourself you’ll add structured data “once you have content.” But structured data is infrastructure. Adding it later means retroactively auditing every page. Better to wire it in correctly once, so every post published after that automatically gets it right.
What that means concretely: BaseHead.astro outputs canonical URLs, Open Graph tags, Twitter Card meta, and a JSON-LD hook on every page. PostLayout.astro generates a BlogPosting JSON-LD object for every post automatically — pulling author, date, headline, description, and URL from the frontmatter. The homepage gets a WebSite JSON-LD block.
There’s also a /llms.txt file at the root. This is part of the emerging llms-txt standard — a plain-text file that describes the site structure and content for AI crawlers. It’s the equivalent of robots.txt, but for language models.
The goal is for this site to be discoverable and usable not just by search engines and human readers, but by AI agents. That felt appropriate given what the site is about.
The first post
After the site was live, there was a placeholder post — a minimal “Hello World” that served its purpose during scaffolding.
Replacing it was the next task. And here’s where it gets meta.
The request to write the first real post was: write a blog about how we deployed the website, all the way from my initial questions about the technologies to use, to the first dummy draft. Include the discussion, and writing of this blog as well.
So what you’re reading was written by the same AI that helped make the decisions it’s describing, using the documentation of those decisions as source material, in a session that itself became part of the content.
That’s not a gimmick. It’s the model.
The point of this site is not that AI does the work while a human watches. It’s that the human and the AI work together in a way where the human’s judgment and the AI’s execution compound on each other. The decisions about the stack were mine — the AI helped me think through the tradeoffs. The build was the AI’s — I reviewed it. This post is the AI’s words — shaped by my intent and grounded in my notes.
I don’t know exactly where that line falls in every case. But I think being honest about the process is more interesting than pretending otherwise.
What this setup enables
The site as it stands is a clean foundation. A post goes from idea to live URL in a predictable sequence: write in Obsidian, hand to an AI agent for formatting and frontmatter, review, push, auto-deploy. The whole pipeline is documented and repeatable.
Future things I want to add: a proper newsletter integration, a content API endpoint that would make the site usable as an MCP data source, and eventually search once there’s enough content to warrant it. Dark mode is deferred. Comments are deferred. The goal was a foundation that works and doesn’t require constant maintenance — not a feature-complete product on day one.
The bet on Astro was a bet that simple and predictable ages better than clever and powerful. A year from now, when an AI agent that wasn’t in the original session needs to add a component or fix a layout, the codebase should be legible enough to work from. That’s the standard I was optimizing for.
So far: one day of work, a live site, a documented stack, and a first post. That’s a reasonable start.