Ghostlite splits every post in two. The metadata — title, slug, status, dates — lives in D1. The body lives in R2, as a single HTML file keyed by the post's ID.
At first glance this looks like extra work. Why not store the body in a TEXT column right next to everything else?
Databases are bad at big blobs
A post body can be tens of kilobytes of HTML. Put that in a row and every query that touches the table drags it along — even a query that only wanted the title. Object storage is built for exactly this shape of data: large, written rarely, read often.
List views stay fast because the rows stay small.
The body is fetched only on the post page, exactly when it is needed.
R2 reads are cheap and cache well at the edge.
The cache falls out for free
Because the body is addressed by a stable key, caching rendered HTML in KV becomes trivial. The cache key folds in the publish timestamp, so an edit busts the cache without a single line of explicit invalidation logic.
The best cache invalidation is the kind you never have to write.
Metadata in a database, content in a bucket — each tool doing the job it is genuinely good at.