$ cat ~/projects/lancangkuning.mdx

LancangKuning.com

lancangkuning.com

Rebuilt a legacy Laravel 5 news portal on a Go + Nuxt.js + Redis stack, cutting page load from ~1 minute to under 1 second.

  • Go
  • Nuxt.js
  • MySQL
  • Redis

Context

LancangKuning.com was a regional Indonesian news portal that had accumulated years of organic growth on a Laravel 5 stack. By the time I joined, the home page was taking 45–60 seconds to render under load, and the editorial team couldn't publish a breaking news story without watching the entire site fall over.

What I built

The rebuild was a near-total backend replacement, keeping the existing content:

  • Go HTTP server with a thin, request-scoped middleware chain (auth, request ID, cache lookup, fallback to handler). The routing table is small — maybe a dozen handlers — but every one of them is cache-aware.
  • Normalized MySQL schema — the legacy database was denormalized to a degree that made every article list query a 7-table JOIN with a LIKE '%keyword%' on a TEXT column. The new schema has proper article/category/tag join tables and full-text indexes where they make sense.
  • Redis caching layered in two tiers: page-level (60s TTL on article pages) and fragment-level (longer TTL on the sidebar and footer). The invalidation story is "purge by article ID on publish" — no global flushes.
  • Nuxt.js frontend doing SSR so the first paint is server-rendered HTML, with hydration for the interactive bits.

Outcome

Page load went from 45–60 seconds to under 1 second. The editorial team could publish during traffic spikes without coordinating with engineering. The bigger win was operational: the new stack was small enough that one engineer could hold the whole system in their head, which made on-call dramatically less painful.

What I'd do differently: I'd build the cache invalidation primitives first, then the cache layer, then the handlers. We did it handler-by-handler and the first version of the invalidation logic was a mess of "publish this, purge that" inline. Centralizing it in a small set of event-driven purges would have been cleaner.