On this page
Knowledge

Building This Blog with Next.js 16 and Supabase

The blog you are reading runs on the same stack as the rest of this portfolio: Next.js 16 App Router, React 19, and a Supabase Postgres backend. This post walks through how the pieces fit together.

Content model

Posts live in two tables: a locale-neutral blog_posts row plus one blog_post_translations row per language. Markdown is stored raw and rendered at request time, so both English and Vietnamese versions share the same slug, category and cover image.

Why bilingual rows instead of JSON columns

Typed columns keep queries honest. A missing Vietnamese translation simply hides the post from the Vietnamese listing instead of rendering half-localized content.

Reading path

A single repository module owns every public query. It maps database rows into view models, computes reading time from the markdown body, and never leaks raw rows into components.

const listing = await getPublishedPosts(locale, page);

Listings are wrapped in a tagged cache so admin edits revalidate everything through one tag.

Takeaways

  • Model translations as rows, not documents
  • Render markdown on the server, ship HTML to the client
  • One cached accessor per read path keeps invalidation simple

Related posts

Reviews

Review: AI Coding Assistants in Daily Work

Three months of using AI assistants for reviews, boilerplate and debugging — where they genuinely help and where they still need a human.

1 min read