Building a Database-less Blog on Vercel Blob with MDX

How I run a database-less blog on Vercel Blob: posts stored as JSON objects, MDX compiled on the server, and SEO metadata generated for every page.

Anuj Pandey3 min readNext.jsVercel BlobMDXSEOStatic Site

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.

Frequently asked questions

Do I need a database to run this blog?+

No. Every post is a single JSON object in Vercel Blob, with a small set of seed posts baked into the app as a fallback. There is nothing to migrate and nothing to keep running.

How is the content cached?+

Posts are held in the Next.js Data Cache under a tag and served indefinitely. The cache is only invalidated, via revalidateTag, when a post is created, edited or deleted, so reads almost never touch Blob.

Who can edit posts?+

Only the emails on the admin allow-list, after signing in with TinTorch single sign-on. The editor lives on a route that is not linked anywhere on the public site.

Is it good for SEO?+

Yes. Every post ships per-page Open Graph and Twitter tags, canonical URLs, BlogPosting structured data, and FAQPage structured data generated from blocks exactly like this one.

What happens on a fresh deploy when the blob store is empty?+

The app falls back to a small set of seed posts compiled into the bundle, so the blog renders real content on the very first request instead of an empty list.

Why MDX rather than plain Markdown?+

MDX lets a React component sit in the middle of the prose when a diagram, an embed or a small interactive demo says it better than a paragraph can. Everything around it stays ordinary Markdown.

Which Markdown extensions are enabled?+

remark-gfm for tables and strikethrough, and rehype-slug so every heading gets an anchor id that can be linked to directly.

Where does the MDX get compiled?+

On the server. next-mdx-remote/rsc compiles the MDX string pulled from Blob at request time, so none of the compilation pipeline ships to the browser.

How are drafts kept out of the public list?+

Status travels with the post inside the same JSON object, so the public listing filters on it and an unpublished post is only reachable from the admin route.

What does a setup like this cost to run?+

Storage for a few hundred small JSON objects is close to nothing, and because reads are served from the Data Cache the request volume against Blob stays low. There is no database instance billing you between posts.

How do I back the content up?+

A full backup is a list call plus a fetch of every object, which leaves you with a folder of JSON files. Restoring is the same put() call that wrote them in the first place.

When would this stop being the right choice?+

Object storage has no query layer, so filtering and pagination happen in memory after a list call. Somewhere past a few thousand posts, or the first time the content needs real relational queries, a database starts paying for itself.

Building something in Web3 or full-stack?

Hire me