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 i18n— One 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 ISR— Static 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 backend— A 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 deploy— Caddy 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.