← Projects
Portfolio 2025

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.

LiveNextjsCloudflareTypescriptMCPReact

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 contents table with a type discriminator and a data JSON column for type-specific fields — simplifies migrations and keeps queries flat

  • Tag filtering via D1's json_each() — no join tables