Chinghuihui's Blog
Dev

How I Built This Blog: A Caching-First Architecture

A blog looks dynamic, but the content only changes when I publish. Here's how that one idea shaped a fast, cache-first build with Next.js and headless Ghost.

Ching Hui

Hi, I'm chinghuihui ☀️

I wanted this blog to feel instant — fast to load, instant to click around — without paying for it in complexity or infrastructure. This post is a short tour of the architecture and, more importantly, why it's shaped this way.

          users     users
             └───┬────┘
                 ▼
       ┌──────────────────┐
       │ CDN (Cloudflare) │
       └────────┬─────────┘
                ▼  HTML / RSC
       ┌──────────────────┐
       │     Next.js      │
       │  · /blog         │
       │  · /blog/{slug}  │
       │  · API Route     │
       └────────┬─────────┘
                ▼  cacheHandler()
       ┌──────────────────┐
       │      Redis       │
       └──────────────────┘

 
  Ghost (headless CMS)
    ├─ data    ──► Next.js
    └─ webhook ──► API Route
          → revalidateTag()
          → purge CDN

The one idea everything follows from

A blog looks dynamic. You've got a latest-articles feed, a category switcher, articles filtered by topic. It's tempting to reach for server rendering on every request.

But step back and the content is actually the same for everyone, and only changes when I publish something. There's nothing truly per-request about it. Once you see that, the whole design falls out of a single principle:

Treat the content as static. Cache it aggressively. Invalidate it precisely.

Everything below is just that principle applied to each piece.

The stack

  • Next.js (App Router) — the rendering and caching layer
  • Ghost (headless) — the CMS, used purely as a content API
  • Cloudflare — CDN and edge image optimization
  • Base UI + Tailwind CSS — accessible component primitives, styled with design tokens

Pages are static, and that's the whole trick

Every page — the blog index and each article — is statically rendered and served through Incremental Static Regeneration (ISR). The rendered HTML lives in a cache, and Cloudflare sits in front of it.

In practice that means a normal visit never touches my server. The page comes straight from the edge, typically in a few dozen milliseconds. The origin only does work when content actually changes.

The second payoff is navigation. Because pages are static, the framework prefetches the next page's data as soon as a link enters the viewport. By the time you click an article, the data is already there, so the transition is essentially instant — no spinner, no dead pause while a server scrambles to assemble the page.

"Filter by category" without giving up static

The index has a category switcher that swaps the articles below it. That sounds dynamic, but the set of categories is small and known — so instead of rendering on demand, I render every category's articles into the page up front and toggle them on the client.

I use a Base UI Tabs component for this, with all tab panels kept mounted. Switching categories is pure client-side state: zero network requests, instant. And because every panel is in the HTML, search engines see all of it — I get the snappy interaction and the crawlable content, without the page ever leaving the "static and cacheable" side of the line.

Precise updates, not a guessing-game TTL

The catch with caching is staleness. The lazy fix is a short time-to-live and hoping nobody notices the gap. I didn't want that.

Instead, Ghost fires a webhook whenever I publish or edit. That hits a small endpoint which does two things:

  1. Invalidates exactly the affected pages with tag-based revalidation (an edited post busts its own page; a new post busts the lists).
  2. Purges the matching pages from Cloudflare's edge.

So updates are immediate and surgical. Nothing waits on a timer, and unrelated pages stay warm in cache.

What this buys me

Putting it together, the advantages are concrete:

  • Fast first load — pages are served from the edge, not generated per request.
  • Instant navigation — the next page is prefetched before you click.
  • Immediate, precise updates — publish, and only what changed regenerates.
  • Cheap to run — most traffic never reaches the origin; the server is mostly idle.
  • Accessible and consistent UI — Base UI handles behavior and a11y, Tailwind handles the look.

None of these pieces are exotic on their own. The value is in the single thread running through them: decide what's actually static, push it as close to the user as possible, and update it the moment — and only the moment — it changes.

That's the whole blog.