Daniel Majer

Building a Blog with Redis and MDX

@dnomjr|May 14, 2026 (17d ago)5 views

This blog has two interesting pieces worth writing about - MDX as the content format and Redis as the storage for the view counter. Let's look at what they are and how they're used here.

What is MDX

MDX is markdown with the ability to use React components inline. You write regular ## headings, **bold** and lists, but when you need something interactive, you can import a component and drop it in as <MyComponent />.

In this project every post is a single .mdx file in content/blog/. No frontmatter - the metadata (title, date, id) lives separately in app/posts.json.

// app/blog/[slug]/page.tsx
const { default: Content } = await import(`@/content/blog/${slug}.mdx`);

That's the whole trick - Next.js dynamically imports the MDX file based on the slug from the URL and renders it as a React component.

What is Redis

Redis is an in-memory key-value database. It keeps data in RAM, so reads and writes are extremely fast. It's great for things like caching, counters, rate limiting, session stores - anywhere you don't need a relational database and want responses in milliseconds.

This blog uses Upstash Redis - a serverless variant that speaks over HTTPS REST instead of the classic TCP protocol. That lets it run in edge runtimes where long-lived socket connections aren't possible.

How it all fits together

The connection is initialized in app/redis.ts:

const redis =
  process.env.SKIP_VIEWS === "1" || !process.env.KV_REST_API_TOKEN
    ? null
    : new Redis({
        url: process.env.KV_REST_API_URL!,
        token: process.env.KV_REST_API_TOKEN,
      });

If there's no token, or if SKIP_VIEWS=1 is set (handy during local development), the client is null and every call gracefully falls back - the counter simply shows 0.

The counter itself lives under a single hash key views, where the field is the post id:

// increment when a post is opened
const views = await redis.hincrby("views", id, 1);

// read a single value
const views = (await redis.hget<number>("views", id)) ?? 0;

// read all counts at once for the listing
const allViews = await redis.hgetall<Views>("views");

hincrby is atomic, so concurrent requests don't lose updates. And a single hgetall is enough for the blog index to load counts for every post in one round-trip.

The view flow

  1. User opens a post → the server renders the MDX through app/blog/[slug]/page.tsx.
  2. The client fires GET /api/view?id=<slug>&incr=1 (route in app/api/view/route.ts).
  3. The API calls redis.hincrby("views", id, 1) and returns the new value.
  4. On the blog index, getPosts() loads all views at once and pairs them with the metadata from posts.json.

No schema, no migrations - just a hash, two commands, and it's done. For this use case that's exactly what you want.

The inspiration for this whole setup comes from Guillermo Rauch (founder of Vercel), who uses a very similar pattern on his personal blog - MDX for the writing, Upstash Redis for the view counts. It's a clean, minimal approach that's hard to improve on, so I just leaned into it.