← Projects
Portfolio 2025

Portfolio 2025

A custom publishing platform with per-channel content visibility, scoped full-text search, and AI-assisted content management via an MCP API — built on Next.js, Cloudflare Workers, D1/R2, and an OAuth 2.0 + PKCE auth server.

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)

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