keateswebsite
Portfolio website - Next.js and Payload CMS๐๏ธ Last updated June 12, 2026
michaelkeates.co.uk
Personal portfolio and blog, live at www.michaelkeates.co.uk โ a fully self-hosted, content-managed rebuild of my original Next.js site, keeping its glassmorphism look while making every part of it editable from a CMS.
Tech stack
| Layer | Choice |
|---|---|
| Framework | Next.js 16 (App Router, React Server Components) |
| CMS | Payload CMS 3 (Postgres adapter, blocks, globals, native dashboard widgets) |
| UI | Chakra UI v3 + Tailwind 4, framer-motion fade-ups, glassmorphism design system |
| 3D | three.js โ an interactive avatar model on the home page |
| Database | PostgreSQL 16 |
| Cache | Valkey 8 (open-source Redis fork) via a Next.js cache handler |
| Runtime | Docker Compose on Proxmox VMs โ separate dev and production environments |
| Git hosting | Self-hosted Gitea |
| Edge | Cloudflare Tunnel (no open inbound ports) |
Highlights
- Everything is content-managed. Pages are composed from reusable Payload blocks โ profile header, experience timeline (LinkedIn-style multi-role support), certifications, icon grids, layout grids, video/X embeds โ so new sections need no code. Site title, favicon, background colours, navigation, search and social buttons are all CMS settings too.
- Interactive 3D avatar with a seasonal wardrobe: the CMS schedules alternative models by date range (a Halloween ghost with a "BOO" animation, Santa, shirt and cap variants), all generated procedurally with three.js scripts in this repo.
- Theme sync: the site's light/dark toggle and the Payload admin share one cookie, so flipping either side updates both.
- Live ops dashboard inside the Payload admin: content stats, Valkey cache metrics with a live performance graph, and Cloudflare analytics โ 7-day totals, a 30-day traffic chart, a world bubble-map of requests by country, and a top-countries list, all fed by Cloudflare's GraphQL API.
- Portfolio straight from git: the Portfolio page lists repositories live from my Gitea instance, and each project page renders the repo's README server-side โ this very document is rendered there.
- Production hardening: immutable Docker image builds, named volumes, nightly database + media backups with retention, and a Cloudflare Tunnel so the origin exposes nothing to the internet.
Architecture
Cloudflare (DNS + Tunnel)
โ
โผ
Production VM โโ docker compose: Next.js (standalone build) ยท Postgres ยท Valkey
โฒ โ
โ git pull main + image rebuild โ content authored here
โ โผ
Gitea (self-hosted) โโโ push dev / merge main โโ Dev VM (hot reload, schema push)
Code flows dev โ Gitea โ production; content flows the other way (production is the source of truth, dev refreshes from its backups). Schema changes ride along with code via Payload's Drizzle schema push.
Development
cp .env.example .env # set secrets
docker compose up -d # dev stack: next dev with hot reload
Production runs the same repo through docker-compose.prod.yml (multi-stage standalone build) and deploys with a single script: pull main, rebuild the image, restart the stack.
Full setup and operations documentation lives in docs/SETUP.md.