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:
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:
"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
| Component | Provider | Cost |
|---|---|---|
| Hosting + CDN + TLS | Cloudflare Pages | $0 |
| Search | Pagefind (static) | $0 |
| Social cards | sharp at build time | $0 |
| CI | GitHub Actions | $0 |
| Analytics | none (deliberately) | $0 |
| Domain | registrar | ~$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.