
Portfolio 2025
A full-stack portfolio site rebuilt annually. The 2025 edition runs on Next.js 16 + Cloudflare Workers + D1, with a rich admin CMS, FTS5 search, OAuth 2.0 server, and an MCP API for AI-driven content management.
Problem
Most portfolio sites are static, hard to maintain, and quickly become stale. The goal here was to run everything — public site, admin CMS, REST API, URL shortener, and OAuth server — from a single codebase deployed entirely on Cloudflare's edge network: zero cold starts, low latency, no third-party CMS dependency.
Tech Stack
Framework: Next.js 16 (App Router, RSC, React 19) via OpenNext (@opennextjs/cloudflare)
Runtime: Cloudflare Workers — Web-standard APIs only (fetch / crypto / Request), no Node built-ins
Database: Cloudflare D1 (SQLite at the edge) + Drizzle ORM; FTS5 full-text search with snippet highlighting and LIKE fallback
Storage: Cloudflare R2 for media; client-side compression before upload
Editor: Tiptap 3 (rich text with colour, links, images, task lists, YouTube embeds)
Styling: Tailwind CSS 4 + shadcn/ui + OKLCh colour space, flicker-free dark mode
Validation: Zod 4 + @t3-oss/env-nextjs
Testing: Vitest + better-sqlite3 (in-memory TDD for the data layer)
Toolchain: Biome, Wrangler, pnpm
Outcome
A fully functional portfolio and content platform, live at txz.cool.
Public site
Four content sections — Experiences, Projects, Photography, Blog — each with list, detail, and tag filtering views
Draggable Polaroid photo stack on the homepage: physics-based random rotation, click-to-expand, SSR-safe with no hydration mismatch
⌘K command palette powered by FTS5 — grouped results, highlighted snippets, adaptive debounce (600ms / 400ms / 200ms by query length)
URL shortener at
/pf/[shortCode]with referral source tracking
Admin CMS
Full content lifecycle: draft → publish → unpublish, with scheduled future publishing and one-click index rebuild
Draft autosave to localStorage — edits survive page refresh
OAuth 2.0 + PKCE server: complete authorisation flow for third-party integrations
Bearer token REST API — the same API used to manage this entry via MCP
Data model
Single
contentstable with atypediscriminator and adataJSON column for type-specific fields — simplifies migrations and keeps queries flatTag filtering via D1's
json_each()— no join tables