People assume a blog with full-text search, per-post social cards, RSS, and a hundred-ish pages needs a server somewhere. This one has none. The entire running cost is the domain registration — roughly the price of two coffees a year — and everything else is $0. Not “free tier that becomes $40/month when you get traffic” free. Actually free, by construction.

The trick isn’t a discount. It’s an architectural decision: nothing happens at request time. If no code runs when you load a page, there’s nothing to bill.

The pipeline

The whole system is one directed graph, and every arrow fires at build time:

Everything fires at build time, Cloudflare Pages serves the result Markdown in git one file per URL git push Astro build schema-checked HTML + CSS Pagefind index OG cards sharp sitemap RSS Cloudflare Pages static edge · $0

nothing happens at request time

Posts are Markdown files in src/content/blog/ — one file per URL. There is no CMS and no database; the git history is the edit history, and a git revert is my rollback story. Astro reads the collection at build time, type-checks the frontmatter against a schema, and emits plain HTML.

Hosting: Cloudflare Pages

Cloudflare Pages watches the GitHub repo. Every push to main triggers a build (astro build, about a minute) and atomically swaps the deployment. The free plan serves static assets with unmetered bandwidth from their edge — the same CDN story I dug into in the HTTP caching contract, except here every response is cacheable forever because nothing is dynamic. TLS is included; the handshake terminates at their edge, not on any machine I pay for.

The limits that matter on the free plan: 500 builds per month (I use maybe 30) and a 25 MiB per-file cap (largest file here is a font subset, ~50 KiB). Traffic spikes are Cloudflare’s problem, which is exactly where I want that problem to live.

Search without a server

Full-text search is the feature everyone assumes needs a backend. It doesn’t. Pagefind runs after the build, crawls dist/, and writes a chunked, compressed index as static files:

json
"build": "astro build && pagefind --site dist"

The browser lazy-loads only the index fragments a query touches — a search costs a few tens of kilobytes, not a round-trip to a search service. The index is scoped with data-pagefind-body so navigation chrome doesn’t pollute results. Total infrastructure: zero.

Social cards, fonts, CI — all build-time

OG images. Every post gets a 1200×630 social card at /og/<slug>.png, rendered during the build by an Astro endpoint that feeds an SVG template through sharp. When a post’s title changes, the next build regenerates its card. No image service, no on-demand rendering λ.

Fonts. The three typefaces are self-hosted woff2 files with @font-face and preload hints — no Google Fonts request, no third party in the critical path, and one less DNS lookup (a cost I’ve measured before).

CI. GitHub Actions runs tsc --noEmit and a full build on every push. Free for public repos. It’s the reviewer that never gets tired: a broken frontmatter field or a dead import fails the check before Cloudflare ever sees the commit.

The bill

ComponentProviderCost
Hosting + CDN + TLSCloudflare Pages$0
SearchPagefind (static)$0
Social cardssharp at build time$0
CIGitHub Actions$0
Analyticsnone (deliberately)$0
Domainregistrar~$10/yr

The domain is the only line item, and it’s the one thing that can’t be static.

What this architecture costs you instead

Honesty section: you pay in constraints, not dollars. No comments without a third-party embed. No personalization, ever. Content changes require a build (~90 seconds from push to live), so this is the wrong shape for anything real-time. And build-time work scales with content — at thousands of posts, the OG-image step would dominate and I’d need to cache between builds.

For a technical blog, none of those bite. The failure mode of a $6 VPS is a 3 AM page; the failure mode of this stack is that I wait a minute for a deploy.

Takeaways

  • Move all computation to build time. If nothing runs at request time, hosting is free and outages need Cloudflare-sized incompetence.
  • Full-text search does not require a server — Pagefind ships a static, lazily-loaded index that costs kilobytes per query.
  • Social cards, fonts, sitemaps, RSS: all derivable from content at build time. Derive them; don’t serve them.
  • Git as CMS gives you review, rollback, and history for free — and lets CI reject broken content before it deploys.
  • The next post in this series covers where the rest of my infrastructure lives: the homelab.