back to posts
#40 Part 7 2026-01-11 12 min

Astro + Cloudflare Pages: A $0/Month Blog That Deploys in 45 Seconds

Setting up a blazing-fast static blog with type-safe content, automatic sitemaps, and zero hosting costs

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:

FieldTypeRequiredDescription
titlestringYesPost title
datestringYesYYYY-MM-DD format
partnumberYesNarrative arc section (1-4)
readTimestringYesEstimated read time
descriptionstringNoSEO/preview description
tagsstring[]NoCategorization tags
imagestringNoCover image path
substackUrlstringNoLink to Substack version
authorstringNoDefaults to “Myron Koch”

The part field groups posts into narrative arcs:

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

  1. Go to Cloudflare Dashboard → Pages
  2. Click “Create a project”
  3. Connect your GitHub/GitLab repository
  4. Select the repository

Step 2: Configure Build Settings

SettingValue
Framework presetAstro
Build commandnpm run build
Build output directorydist
Root directoryblog-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)

  1. Go to your Pages project → Custom domains
  2. Add your domain
  3. 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:

  1. Pulls the latest code
  2. Runs npm install
  3. Runs npm run build
  4. Deploys to their edge network
  5. 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

ServiceMonthly Cost
Cloudflare Pages hosting$0
Cloudflare CDN$0
Custom domain (annual)~$10/year
SSL certificate$0 (included)
Total~$0.83/month

Compare to:

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

4. Test a new deploy Make a small change, push, watch it go live.


Summary

The stack so far:

LayerTechnologyStatus
ContentMarkdown + Frontmatter
ValidationZod schemas
BuildAstro
SearchPagefind
HostingCloudflare Pages
CDNCloudflare (automatic)
SSLCloudflare (automatic)
DeployGit push

Ongoing maintenance: Zero. Monthly cost: Zero.


Next up: The Chatbot Architecture - How the blog answers questions about itself.