Most personal blogs reach for a database far too early. A database means migrations, a connection pool, a hosting bill and a backup story, all for what is, in the end, a handful of articles that change a few times a month. So when I rebuilt the writing section of this site, I set myself one rule: no database. Content lives as plain objects in Vercel Blob, and the entire publishing workflow runs on top of it.
This post is a tour of how that works: the storage, the rendering, the SEO and the editor behind it.
Why Blob instead of a database
A blog post is just text plus a little metadata. Vercel Blob gives me durable, CDN-backed object storage with a one-line put() API. Each article is a single JSON object:
await put(`blog/posts/${slug}.json`, JSON.stringify(post), {
access: 'public',
contentType: 'application/json',
allowOverwrite: true,
})
Listing posts is a list({ prefix: 'blog/posts/' }). Reading one is a fetch() of its public URL. There is no schema to migrate and nothing to keep running. When the blob store is empty, say on a fresh deploy, the app falls back to a small set of seed posts baked into the bundle, so the blog is never blank.
The constraint is the feature
Object storage has no query layer, so every feature has to be expressible as: list the objects, then work in memory. That constraint kept the blog small and fast instead of letting it quietly grow into an application.
MDX for the writing experience
Articles are authored in MDX, so prose and the occasional React component live side by side. Rendering happens on the server with next-mdx-remote/rsc, which compiles the MDX string pulled from Blob at request time:
import { MDXRemote } from 'next-mdx-remote/rsc'
<MDXRemote source={post.content} components={mdxComponents} />
A remark-gfm plugin handles tables and strikethrough, and rehype-slug gives every heading an anchor. Code blocks get syntax highlighting. The result is a reading experience that feels handcrafted but is driven entirely by content.
Caching, so reads almost never touch Blob
Posts are held in the Next.js Data Cache under a tag and served indefinitely. Nothing expires on a timer. The cache is invalidated only when a post is created, edited or deleted, which makes publishing the single moment the site rereads storage.
SEO and Open Graph, baked in
Every post exports a Next.js generateMetadata function, so each article ships with its own title, description, canonical URL, Open Graph card and Twitter summary. Search engines and social previews get exactly what they expect, per page, with no manual head juggling. On top of that, each article emits BlogPosting structured data, and the FAQ blocks in the body become FAQPage structured data.
Editing without a CMS
The admin lives at a quiet route that is not linked anywhere on the site. It is gated behind TinTorch single sign-on, and only a short allow-list of admin emails can get in. Once authenticated, I get a clean black-on-white editor: write MDX on the left, see it rendered live on the right, fill in the SEO fields, and hit publish. Saving writes the JSON object to Blob and the post is live.
That is the whole system, storage, rendering, SEO and editing, with no database in sight. It ships as part of the rest of the site, the same Next.js app that carries my portfolio. The rest of this blog is where I will write about engineering, Web3 and the occasional cricket tangent.