Skip to content
All projects

PublishedFeatured

This site: a bilingual portfolio with its own CMS

A Thai/English profile site with an Ant Design admin — drafts and version history, passkey sign-in, a media library on Cloudflare R2, sealed off-site backups, a theme editor and a blog with RSS — on Next.js 16, Prisma 7 and PostgreSQL, deployed to Railway behind Cloudflare.

  • Next.js
  • TypeScript
  • React
  • Prisma
  • PostgreSQL
  • Ant Design
  • Cloudflare R2
  • Railway
  • GitHub Actions

Overview

A personal site should be cheap to keep alive and pleasant to update. This one is a single Next.js app with two faces: an animated public site in Thai and English, and an Ant Design admin where every piece of content is edited, drafted and published. There is no headless CMS and no third-party auth provider — the whole thing is one process, one PostgreSQL database and a handful of scripts.

What is inside

  • Content with a lifecycle. Profile, experience, projects and posts move through draft → published → archived. Editing something that is already live creates unpublished changes that only go public on "Publish changes", and every publish is kept as a version that can be restored.
  • Two languages, one record. English lives in the base row, Thai in a translation row, and every field falls back per field, so a half-translated project still renders.
  • Passkeys first. The owner signs in with a passkey (WebAuthn); the password is only the bootstrap. Changing the password signs every other device out.
  • A media library, not URL fields. Files upload straight to Cloudflare R2 with presigned URLs, are addressed by their checksum (upload the same file twice, get the same object) and know where they are used, so a picture cannot be deleted from under a page.
  • A theme editor. Four colours per scheme are expanded into ~85 design tokens in OKLCH, checked for contrast, with a live preview of the real public page.
  • Posts. Markdown articles with tags, a generated share card per post and an RSS feed per language.

How it runs

Railway hosts the app and the database behind Cloudflare. GitHub Actions runs typecheck, lint, ~550 tests and a production build on every push, applies pending migrations before Railway builds, and dumps the database nightly into a private bucket, sealed with a key CI does not hold. Contact-form messages are stored first and emailed afterwards, so a mail outage never loses a message.

Diagram sourceflowchart
flowchart TD
  people["Visitors and the owner"] --> cf["Cloudflare"]
  cf --> app["Next.js app on Railway"]
  app --> db[("PostgreSQL")]
  app --> r2[("Cloudflare R2: media")]
  app -.->|after the message is stored| mail["Email"]
  push["git push"] --> ci["GitHub Actions: checks and build"]
  ci -->|migrations first| db
  ci -->|then Railway builds| app
  nightly["Nightly workflow"] -.->|sealed dump| bucket[("Private bucket")]

What I learned

The expensive lessons were operational, not front-end: a static build that reads the database will break the moment a migration adds a column the build needs, so migrations now run in CI first; and a backup nobody has restored is a hypothesis, so the restore is a scripted drill, not a hope.

Status
Published
Published
Mar 1, 2024
Stack
Next.js · TypeScript · React · Prisma · PostgreSQL · Ant Design · Cloudflare R2 · Railway · GitHub Actions