← Back to portfolio

CASE STUDY / 02

Personal Portfolio

A content-driven Next.js portfolio with a password-protected admin area, so projects and public copy are published as commits without editing code.

Next.jsReactTypeScriptZodTailwind CSSGitHub APIVercel

Screenshots

01 image

01 / OVERVIEW

This portfolio is a content-driven Next.js application that presents the projects, technologies, and technical direction I am working on, and lets me publish all of it without editing code.

Every visible string on the site is a JSON document in `content/`, validated by Zod at build time. Nothing is hardcoded inside a component, so a change to the copy is a change to data rather than a redeploy of logic.

02 / PROBLEM

Work-in-progress projects change faster than presentation code does. A portfolio that keeps its content inside components needs a code change and a redeploy for every correction to a sentence, which quietly discourages writing copy that was never tested against a reader.

There is a second problem specific to a developer in training. The honest version of a portfolio shows unfinished work, current direction, and the reasoning behind decisions. That is a large amount of prose to keep current, and maintaining it by hand is the reason projects stop being documented at all.

03 / SOLUTION

The site separates content from presentation. Four documents in `content/` hold all public copy, validated by Zod so a malformed value fails the build rather than shipping. Presentational components read those documents through cached loaders and keep the same shapes regardless of where the data came from.

A password-protected admin area writes those same documents as commits to this repository through the GitHub API, and that commit triggers the Vercel build which publishes the change. This provides a normal editing surface without introducing a database, a separate CMS, or another service to maintain. Unsubmitted form input is preserved locally, so a refresh or a rejected save does not lose work in progress.

04 / ARCHITECTURE

Next.js App Router, with the public pages prerendered at build time and the admin routes rendered on demand behind a session check. Public pages read the cached content loaders. The admin reads and writes through Server Actions, each of which validates the session itself because a Server Action is a public HTTP endpoint and its id is not a secret.

Content is described by Zod schemas in `lib/`, and the TypeScript types are derived from those schemas so the two cannot drift apart. Writes go through the GitHub Git Data API so that a project and its images land in a single commit. Styling is one hand written stylesheet in `app/globals.css`, using CSS custom properties for the design tokens.

05 / TECHNICAL DECISIONS

  • Keep content in JSON documents validated by Zod rather than in typed modules, so publishing is a data change and an invalid value fails the build instead of rendering.
  • Write as a commit through the GitHub API rather than to a database, which keeps the repository the source of truth and adds no service to run or pay for.
  • Publish a project and its images in one commit, so a case study can never reference an image that was never written.
  • Prerender the public pages and read NEXT_PUBLIC_* values at build time, so the public site works with no environment variables at all.
  • Render owner-written prose by splitting on blank lines rather than emitting raw HTML, which would silently discard the structure typed into the editor.

06 / FEATURES

  • Public portfolio sections rendered from validated JSON content
  • Case study pages with per-section cards, images, and a capped reading measure
  • Password-protected admin area for projects and every piece of public copy
  • Publishing as a repository commit, which triggers the production deploy
  • Client-side image resize and WebP conversion before upload
  • Preserved form drafts across refreshes, failed saves, and navigation
  • Accessible mobile navigation, sitemap, and social metadata