All projects
lavier.tech
Open Source
web

lavier.tech

The site you are reading: Next.js 16 + Go 1.25, all content bilingual in Postgres JSONB, an admin panel behind it, push-based ISR, and a deploy with a health gate and auto-rollback

By the numbers

0

Content languages

0

Admin sections

0 s

Deploy health gate

auto

Rollback on failure

The Problem

What I was solving

Most developer portfolios hardcode their content in JSX. Fixing a typo means an edit, a commit, and a deploy; adding a second language doubles every file and guarantees the translations drift apart within a month. And a personal site is exactly the place where content changes constantly — a new project, a new number in a case study, a new job title.
My Approach

How I built it

Content lives in Postgres, not in the codebase. Every translatable field is a JSONB object with en and ru keys, so a project or a job entry is one row that carries both languages. The Go backend serves it, the Next.js frontend renders it statically, and the admin panel writes it back and triggers on-demand revalidation for exactly the paths that changed. The frontend follows Feature-Sliced Design so entities, features, and widgets stay separable. Deployment mirrors the client projects: GitHub Actions builds both images, pushes to GHCR, dumps the database before every deploy, runs goose migrations, and only marks the release good after a health gate passes.

Tech choices

  • Postgres JSONB for i18nOne row per entity carrying {en, ru} beats parallel tables or parallel JSON files. Adding a language is a key, not a migration, and a half-translated field is visible immediately instead of silently falling back.
  • Next.js 16 + push-based ISRStatic pages with on-demand revalidation give static speed with CMS behavior. The admin panel pushes the exact paths to rebuild, so I never had to pick a revalidate interval and hope it is short enough.
  • Go 1.25 backendA single small binary next to Postgres — fast start, tiny image, and no second Node runtime to keep alive. It also makes the analytics proxy trivial: singleflight collapses concurrent Umami requests into one upstream call.
  • Caddy + health-gated deployCaddy handles TLS automatically and fronts several apps on the same VPS. The health gate is the part that matters: a release is only recorded as good after the containers answer, so rollback is a file read instead of a night of debugging.
Outcome

What came out of it

Content edits ship in seconds without a deploy, and both languages stay in sync because they are literally stored side by side. Eight admin sections cover everything the public site renders. The deploy pipeline has a 120-second health gate and rolls itself back to .last-good-sha on failure, so a bad release does not become downtime. The site also runs the SEO plumbing that clients ask for: sitemap, robots, JSON-LD, Open Graph images generated per project, hreflang pairs, and IndexNow pings on publish.