Why this site has its own CMS (and what that cost)
A portfolio could be a static page. Mine is a small CMS on Next.js and PostgreSQL — here is what that bought me, and the two lessons I paid for.

A personal site is the one project where nobody can tell you what to build. That freedom is exactly why most of them end up as a static page that never changes: the moment updating it means touching code, it stops being updated. I wanted the opposite — a site I would keep editing — so I built it as a small CMS instead.
What "its own CMS" means here
There is one Next.js app. Visitors get an animated, bilingual public site rendered ahead of time and refreshed on demand. I get an Ant Design admin where every piece of content — profile, experience, projects, and now posts — is edited in place, in English and Thai side by side.
Three decisions shaped everything else:
Content has a lifecycle. Nothing goes live because I saved a form. Editing something that is already public creates unpublished changes; the public page keeps showing the old version until I press Publish changes, and every publish is kept as a version I can restore. It sounds like ceremony for a one-person site, but it is what makes editing on a phone at midnight safe.
Files are a library, not URL fields. Uploads go straight to Cloudflare R2 through presigned URLs, are addressed by their checksum, and know where they are used. Uploading the same picture twice gives the same object, and the library refuses to delete a file a page still shows. Alt text is written once, per language, and follows the file everywhere.
The owner signs in with a passkey. The password exists only to register the first passkey. Changing it signs every other device out, which is the whole recovery story for a single-owner site.
What it cost
The front-end work was the fun part. The expensive lessons were operational, and both came from the same place: the site is rendered at build time from the database.
The first: a build that reads the database breaks the moment a migration adds a table the build needs — the migration was scheduled to run after the build. It failed three times before I read the error properly. The fix was not clever: CI now applies migrations first, and the platform waits for CI before it builds. A trap you can fall into twice deserves a fence, not a note.
The second: a backup nobody has restored is a hypothesis. The nightly dump is sealed with a key CI does not hold, which is reassuring right up to the day you need it. So the restore is a scripted drill against a throwaway database, and it has actually been run. Same for the contact form — a message is stored first and emailed afterwards, because a mail provider will have a bad day eventually.
Was it worth it?
For the site alone, probably not. As a place to practise the boring parts — migrations under pressure, backups you trust, verification in a real browser before shipping — it has paid for itself several times. And the theme editor, the appearance system that expands four colours into eighty-odd tokens with a live preview, is the kind of thing you only build when the site is yours.
The next things on the list are site settings (so the title of this page is not a constant in the code) and a proper audit screen. This post is the first one written through the editor it describes.