Web Development

Building a Modern Blog with Astro and Cloudflare Pages (Two Years In)

How I publish a portfolio of Astro sites in 2026, why I migrated RankingHacks off WordPress, and the unusual editing workflow (Claude Code, git, no IDE) that ties it all together.

Andreas De Rosi
8 min read
#Astro #Cloudflare Pages #Web Publishing #Claude Code #Static Sites
Astro and Markdown logos with code editor background

The first version of this post was a tutorial. That was 2025 and I was still poking at Astro to see whether it could handle a real content operation. Two years later I’ve actually done it: migrated one WordPress site to Astro, launched two new sites on it, and settled into an editing workflow that would have sounded absurd when I originally hit publish. This is the updated version. It’s less about how to build a blog with Astro and more about what has actually worked when you run one for a while.

What I’ve moved, and what I haven’t

I run six main websites plus a handful of internal tools. Not all of them run on the same stack, and I don’t think they should:

  • andreasderosi.com (this site): Astro + Cloudflare Pages. Built fresh on this stack in 2025.
  • RankingHacks.com: Astro + Cloudflare Pages. Migrated off WordPress in spring 2026 after a full redesign around a warm-paper editorial system with green as the signal color.
  • PhotoWorkout.com: still WordPress. Deeply invested in custom Gutenberg blocks, an Amazon affiliate integration with dozens of custom shortcodes, and a large existing library of articles. Migrating this to Astro would be a several-month project for no clear ROI.
  • Mappr.co: still WordPress. Uses the Interactive Geo Maps plugin heavily, plus thousands of custom map posts. The plugin ecosystem is what I’m buying here.
  • SampleShots.com and AirportRoutes.com: Django. These are database-backed apps first and blogs second, so the stack has to be a real web framework.
  • Internal tools and micro-sites (like the andero.co landing page and various map embeds): Astro or vanilla HTML on Cloudflare Pages.

The takeaway: it isn’t “Astro for everything.” It’s “the right stack for the shape of the content.” Static, editorial content that ships as files? Astro is excellent. Product surfaces with heavy application logic or a plugin ecosystem you actively depend on? Something else.

My editing workflow: Claude Code only

Here’s the part that surprises people. I don’t open a local IDE to edit these sites. I don’t even keep the repos cloned on my laptop. All content and code edits, across every Astro site I run, go through Claude Code: Anthropic’s terminal-native AI coding assistant.

The practical loop looks like this. I write a Telegram message to one of my agent bots (yes, I run a small fleet of them, but that’s another post) with something like: “Write a blog post about attending IFA Berlin. Include exhibitor shortlist, creator hub, ~1200 words.” Claude Code, running on a small Hetzner server, reads the repo, drafts the post as a Markdown file with correct frontmatter, generates a featured image, commits, pushes to GitHub. GitHub Actions builds the Astro site with Wrangler. Cloudflare Pages deploys. Cache gets purged. I read the result and either ship it or ask for revisions in the same chat.

Two things make this workable, and both are worth thinking about if you’re considering a similar setup:

The repo is the CMS. Astro’s content collections mean every blog post is a file: Markdown or MDX in src/content/blog/, with a Zod-typed frontmatter schema. An AI can read those files, understand the pattern from existing posts, write new ones that match the voice, and check them into git. WordPress can’t do this cleanly because posts live in the database as HTML fragments plus a stew of shortcodes, custom fields, and block metadata. Not impossible to edit via REST API, but nowhere near as fluent.

Deploys are declarative. A single git push to main triggers the entire pipeline: Astro build, Wrangler deploy, cache purge, done. No manual staging step. No SSH into a server. No “did you remember to bump the version?” Astro plus Cloudflare Pages plus GitHub Actions is a full DevOps setup for a static site that fits in about 40 lines of YAML.

The result is that I can ship a blog post from a coffee shop with just a phone. I’ve done it. It works.

The stack, precisely

For the Astro sites specifically, the stack is:

  • Astro 5 (moving to 6 when it stabilizes) for the framework
  • Content collections with Zod schemas for typed frontmatter and build-time validation
  • Tailwind CSS with custom design tokens per site (RankingHacks uses a warm paper palette; this site is more of a standard blue/gray)
  • MDX for posts that need custom components (callouts, code blocks, highlight boxes)
  • GitHub as the source of truth (private repos for the business sites, public for a couple of internal tools)
  • Cloudflare Pages for hosting, deployed via cloudflare/wrangler-action@v4 in GitHub Actions
  • Plausible for privacy-friendly analytics
  • Microsoft Clarity on some sites for session-level behavior data
  • Claude Code as the editor and deploy driver

No WordPress admin. No headless CMS. No Netlify Forms. The repo is the whole system.

Why Astro + Cloudflare Pages specifically

I could have picked Next.js on Vercel, Nuxt on Netlify, or a static-site generator like Eleventy on any of them. A few things pulled me to Astro plus Cloudflare Pages:

  • Zero JavaScript by default. Astro renders your components to HTML at build time and ships no JS unless you explicitly ask for it. Great for content sites where the “app” surface is basically links and text. Core Web Vitals stay green with almost no effort.
  • Content collections catch schema errors at build. Bad frontmatter fails the build instead of 500’ing in production. This is a bigger deal than it sounds when you have multiple people (or agents) writing posts.
  • Cloudflare Pages pricing. The free tier covers small sites entirely. At scale, Cloudflare’s per-request pricing beats Vercel and Netlify substantially, and the edge is genuinely global with no cold starts.
  • No lock-in. Astro output is static HTML plus assets. You can move it to any static host in an afternoon. Try that with a Vercel-first codebase that leans on their runtime features.

Where WordPress still wins

I keep four sites on WordPress and I don’t apologize for it. WordPress remains the right choice when:

  • You have an existing content library measured in the thousands of posts, plus a mature editorial workflow that would take real effort to reproduce.
  • Your monetization depends on plugins (affiliate link management, ad optimization, SEO features) that don’t have clean equivalents in the static world.
  • Non-technical people need to publish, and asking them to learn git or Markdown would slow the operation down.
  • You have complex Gutenberg blocks or ACF fields with tight editorial coupling.

The Astro-versus-WordPress debate is often framed as “static is faster and cheaper, WordPress is legacy.” That’s too simple. The right question is: what does your content actually look like, and where does most of the labor go?

Setup basics

If you’re building an Astro blog fresh, the essentials haven’t changed much since the original version of this post. The content collection schema:

import { defineCollection, z } from 'astro:content';

const blog = defineCollection({
  type: 'content',
  schema: ({ image }) => z.object({
    title: z.string(),
    description: z.string(),
    publishDate: z.date(),
    category: z.string(),
    tags: z.array(z.string()).optional(),
    author: z.string().default('Your Name'),
    featuredImage: image().optional(),
    featuredImageAlt: z.string().optional(),
  }),
});

export const collections = { blog };

A dynamic route to render posts:

---
import { getCollection } from 'astro:content';
import Layout from '../../layouts/Layout.astro';

export async function getStaticPaths() {
  const posts = await getCollection('blog');
  return posts.map((post) => ({
    params: { slug: post.slug },
    props: post,
  }));
}

const post = Astro.props;
const { Content } = await post.render();
---

<Layout title={post.data.title}>
  <main class="max-w-4xl mx-auto px-4 py-16">
    <article class="prose prose-lg mx-auto">
      <Content />
    </article>
  </main>
</Layout>

That’s a working blog in under 100 lines. The framework details aren’t the hard part. The hard part is deciding whether the tradeoff (git workflow, no admin panel, no plugin ecosystem) fits how you actually publish.

What’s next

The best stack decision I’ve made in the last few years is accepting that not all sites need the same stack. What made Astro really click for me was pairing it with git-native AI editing, which turns the “no admin panel” tradeoff from a downside into an upside. When your CMS is a repo, and the editor is an assistant that reads the repo, the whole surface gets simpler.

Next up on this stack: an umbrella holding site for the portfolio (early days), and some app-shell content sites tied to internal data pipelines. If you’re thinking about a similar migration and want to compare notes, the About page has my email.