Astro + Cloudflare Pages: A $0/Month Blog That Deploys in 45 Seconds
My blog costs nothing to host and deploys automatically when I push to git. Here’s the exact setup.
Why Astro?
I evaluated a dozen static site generators. Astro won for three reasons:
1. Content Collections with Type Safety
Astro doesn’t just render markdown. It validates it:
// src/content/config.ts
import { defineCollection, z } from 'astro:content';
const posts = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
date: z.string(),
part: z.number(),
readTime: z.string(),
author: z.string().default('Myron Koch'),
description: z.string().optional(),
tags: z.array(z.string()).default([]),
image: z.string().optional(),
substackUrl: z.string().optional(),
}),
});
export const collections = { posts };
If I forget a required field or use the wrong type, the build fails. No more “oops, that post has no date” in production.
2. Zero JavaScript by Default
Astro ships zero JavaScript unless you explicitly add it. Pages load instantly. Lighthouse scores stay perfect.
3. First-Class Markdown Support
Code blocks, tables, footnotes - everything just works. Plus I can add rehype plugins for extra features:
// astro.config.mjs
markdown: {
rehypePlugins: [
rehypeSlug,
[rehypeAutolinkHeadings, { behavior: 'wrap' }]
]
}
Now every heading gets an anchor link automatically.
The Project Structure
blog-site/
├── src/
│ ├── content/
│ │ ├── config.ts # Schema definitions
│ │ └── posts/ # All markdown files
│ ├── layouts/
│ │ └── BaseLayout.astro # Shared layout
│ ├── pages/
│ │ ├── index.astro # Homepage
│ │ ├── about.astro # About page
│ │ └── posts/
│ │ └── [...slug].astro # Dynamic post routes
│ └── styles/
│ └── global.css # Tailwind imports
├── public/
│ └── images/ # Static assets
├── astro.config.mjs # Astro configuration
└── package.json
Content lives in src/content/posts/. Each file is a markdown file with YAML frontmatter:
---
title: "Your Post Title"
date: "2026-01-19"
part: 7
readTime: "10 min"
description: "What this post is about"
tags: ["tag1", "tag2"]
---
# Your Post Title
Content goes here...
The Frontmatter Schema
Every post needs this frontmatter:
| Field | Type | Required | Description |
|---|---|---|---|
title | string | Yes | Post title |
date | string | Yes | YYYY-MM-DD format |
part | number | Yes | Narrative arc section (1-4) |
readTime | string | Yes | Estimated read time |
description | string | No | SEO/preview description |
tags | string[] | No | Categorization tags |
image | string | No | Cover image path |
substackUrl | string | No | Link to Substack version |
author | string | No | Defaults to “Myron Koch” |
The part field groups posts into narrative arcs:
- Part 1: The Chaos Era
- Part 2: Discovering Patterns
- Part 3: The Automation Breakthrough
- Part 4: Advanced Topics
The homepage automatically organizes posts by part.
Sitemap Generation
This is critical for AI search. AutoRAG needs a sitemap to know what to index.
// astro.config.mjs
import sitemap from '@astrojs/sitemap';
export default defineConfig({
site: 'https://operationalsemantics.dev',
integrations: [sitemap()],
// ...
});
That’s it. Every build generates a fresh sitemap at /sitemap-index.xml. When AutoRAG crawls, it finds every post automatically.
Cloudflare Pages Setup
Step 1: Connect Your Repository
- Go to Cloudflare Dashboard → Pages
- Click “Create a project”
- Connect your GitHub/GitLab repository
- Select the repository
Step 2: Configure Build Settings
| Setting | Value |
|---|---|
| Framework preset | Astro |
| Build command | npm run build |
| Build output directory | dist |
| Root directory | blog-site (if in a subdirectory) |
Step 3: Deploy
Click “Save and Deploy”. First build takes ~2 minutes. Subsequent builds: ~45 seconds.
Step 4: Custom Domain (Optional)
- Go to your Pages project → Custom domains
- Add your domain
- Cloudflare handles SSL automatically
The Git Push → Deploy Pipeline
Once connected, the flow is:
git add .
git commit -m "Add new post"
git push origin master
Cloudflare detects the push and:
- Pulls the latest code
- Runs
npm install - Runs
npm run build - Deploys to their edge network
- Invalidates the CDN cache
Your changes are live globally in under a minute.
No FTP. No manual uploads. No “works on my machine.”
Build Script
The package.json build script does more than just Astro:
{
"scripts": {
"dev": "astro dev",
"build": "astro build && npx pagefind --site dist",
"preview": "astro preview"
}
}
After Astro builds the site, Pagefind runs to create the search index. The search index ships with the static site - no server required.
Local Development
cd blog-site
npm install
npm run dev
Opens at http://localhost:4321. Hot reload on file changes.
To preview the production build:
npm run build
npm run preview
This catches build errors before pushing.
The Cost Breakdown
| Service | Monthly Cost |
|---|---|
| Cloudflare Pages hosting | $0 |
| Cloudflare CDN | $0 |
| Custom domain (annual) | ~$10/year |
| SSL certificate | $0 (included) |
| Total | ~$0.83/month |
Compare to:
- Vercel Pro: $20/month
- Netlify Pro: $19/month
- Traditional hosting: $10-50/month
For a static blog with moderate traffic, Cloudflare’s free tier is more than enough.
Common Gotchas
1. Trailing Slashes
Astro and Cloudflare can disagree about trailing slashes. Set it explicitly:
// astro.config.mjs
export default defineConfig({
trailingSlash: 'never',
// ...
});
2. Build Output Directory
If your Astro project is in a subdirectory, set the root directory in Cloudflare Pages settings. Otherwise builds fail silently.
3. Node Version
Cloudflare Pages uses Node 18 by default. If you need a different version, add a .nvmrc file:
20
4. Environment Variables
If your build needs env vars (API keys, etc.), add them in Cloudflare Pages → Settings → Environment variables. They’re available during build but not at runtime (it’s static).
Why This Matters for AI
The sitemap isn’t just for Google. It’s the entry point for AI indexing.
When I set up AutoRAG (covered in the next post), I point it at the sitemap. It crawls every URL, chunks the content, generates embeddings, and stores them in a vector database.
New post? Sitemap updates automatically. AutoRAG reindexes. The chatbot knows about the new content within minutes.
The static site is the source of truth. Everything else derives from it.
Verification
After deploying, verify everything works:
1. Check the live site
https://yourdomain.com
2. Check the sitemap
https://yourdomain.com/sitemap-index.xml
3. Check Cloudflare Pages dashboard
- Build logs should show success
- Deployment should show “Active”
4. Test a new deploy Make a small change, push, watch it go live.
Summary
The stack so far:
| Layer | Technology | Status |
|---|---|---|
| Content | Markdown + Frontmatter | ✓ |
| Validation | Zod schemas | ✓ |
| Build | Astro | ✓ |
| Search | Pagefind | ✓ |
| Hosting | Cloudflare Pages | ✓ |
| CDN | Cloudflare (automatic) | ✓ |
| SSL | Cloudflare (automatic) | ✓ |
| Deploy | Git push | ✓ |
Ongoing maintenance: Zero. Monthly cost: Zero.
Next up: The Chatbot Architecture - How the blog answers questions about itself.