Portfolio CMS

A headless Payload CMS powering my portfolio site, extended with a command-line pipeline that scans GitHub repos and live sites, drafts write-ups with Claude, captures screenshots with Playwright, and publishes via API — turning a multi-hour manual workflow into minutes.

Screenshot of a Payload CMS admin dashboard in dark mode. The Collections section displays five collection cards arranged in a grid: Users, Media, Projects, Technologies, and Tags. Each card has a plus icon for adding new entries. The top navigation bar shows the Payload logo, a Dashboard breadcrumb, and a user avatar in the top right corner.
Client

Andrew Braun Portfolio

Status

Live

Introduction

A headless content management system that powers my portfolio site — it stores the projects, the technologies behind them, and the images, and hands them to the front end over an API. The interesting part isn't the CMS itself but the automation around it: a command-line pipeline that scans my GitHub account and deployed sites, drafts each project's write-up with an AI model, screenshots the live pages, and then publishes the whole thing into the CMS so that adding a new portfolio entry takes minutes instead of an evening.

The Challenge

Building a personal portfolio CMS that not only stores content but automates the entire project ingestion workflow — from scanning GitHub repositories and live sites to generating write-ups, capturing screenshots, and publishing everything via API — while remaining safe to re-run without overwriting manual edits.

The Outcome

Adding a new portfolio project went from an evening of manual case-study writing to minutes of CLI commands and review, with all write-up generation, screenshot capture, and structured field extraction fully automated.

Tech Stack

Frontend

  • Next.js
  • React

Language

  • GraphQL
  • TypeScript
  • SQL
  • Node.js

Backend

  • Payload CMS
  • Cloudflare R2
  • AWS SDK
  • REST API
  • Amazon S3

Database

  • PostgreSQL

DevOps

  • Docker
  • GitHub Actions
  • pnpm
  • Husky
  • lint-staged
  • BuildKit
  • GitHub Container Registry
  • GHCR
  • Dokploy

Tooling

  • Claude
  • Anthropic
  • Playwright
  • Vitest
  • ESLint
  • Prettier
  • Chromium
  • SHA-256
  • Payload CMS v3 running inside a Next.js 16 App Router app — the admin panel, REST API, and GraphQL endpoints all live in the same deployment via route groups ((payload) and (frontend))
  • PostgreSQL through @payloadcms/db-postgres, with schema changes tracked as committed SQL migrations rather than auto-push
  • TypeScript in strict mode end to end, with collection types auto-generated by Payload into payload-types.ts and consumed by both the app and the CLI
  • React 19 for the admin customisations — a custom field component injected into the Projects edit view
  • Cloudflare R2 for media, wired up with @payloadcms/storage-s3 and the AWS SDK S3 client
  • Claude (Anthropic) for technology extraction, write-up generation, and vision-based image alt text, behind a thin typed client in src/app/lib/ai/claude
  • Playwright does double duty: end-to-end tests and headless Chromium screenshot capture for the ingest pipeline
  • Vitest for unit and integration tests (~20 suites, most of them covering the ingest pipeline's pure logic)
  • Docker multi-stage build producing a standalone Next.js server, pushed to GHCR by GitHub Actions and deployed to Dokploy on my own infrastructure
  • pnpm, ESLint, Prettier, Husky and lint-staged for the day-to-day workflow

Key Implementation Details

AI technology extraction in the admin panel

  • A custom React component (ExtractTechnologiesButton) is mounted into Payload's beforeDocumentControls slot, so extraction is a single click while editing a project
  • The button calls a Next.js server action / route handler pair that hands the project's markdown to Claude, then reconciles the result against the Technologies collection — creating missing records with a category from a fixed enum (frontend, backend, database, devops, CMS, language, tool…) and linking them to the project
  • Results are validated before writing: URLs, required fields, and category values are checked, and failures are reported back to the editor rather than silently dropped
  • On one project this catalogued 23 technologies with no manual data entry

A multi-stage ingest CLI

  • pnpm ingest <command> runs a bespoke TypeScript CLI through payload run, so every command has full access to the Payload local API and generated types
  • Stages are separate and composable — discover (GitHub scan via the gh CLI plus a URL list), analyze (repo metadata, file tree, stack-revealing config files, plus a live-site probe for title, meta, framework fingerprints and nav links — no clone required), notes, writeup, shots, sheet, publish, remote, status
  • The generating stages deliberately produce files, not database rows: everything lands in a gitignored work directory and each project gets an ENTER-ME.md checklist listing every field value, the write-up to paste, and each screenshot's alt text — so the whole thing degrades gracefully to a manual copy-paste workflow if anything upstream fails

Resumable, idempotent pipeline state

  • A hand-editable manifest.json is the source of truth; discover only ever appends newly-seen repos and URLs so curated edits survive re-runs
  • Each stage records a timestamp plus a SHA-256 fingerprint of its inputs, and skips when the artifact exists and the fingerprint still matches — with a dependency table so changing, say, liveUrl invalidates analysis, screenshots and write-up, while changing display ordering invalidates nothing
  • The manifest is fully validated before any command writes: slug format, HTTP(S) URLs, contained screenshot basenames, duplicate detection — malformed input exits nonzero instead of half-writing
  • Screenshot capture is transactional: everything is rendered into a temporary sibling directory and only promoted once every target succeeded, every file is a nonempty PNG of the expected dimensions, and exactly one capture is flagged as hero

Structured write-ups split into CMS fields

  • writeup prompts Claude twice against the same context briefing — once with the summary instructions to produce the technical description_markdown, once with the case-study instructions to produce structured JSON (summary, client_name, business_challenge, contribution_highlights, outcomes, status) plus an explicit needsReview list for anything the model was unsure about
  • publish splits the generated markdown on heading text (loose matching, not position) into intro_markdown, tech_stack_markdown, implementation_markdown and outcome_markdown so the front end can lay each section out independently, while the whole document is retained for extraction
  • Unmatched headings and missing sections are reported rather than causing failure — a project that doesn't fit the four-section mould simply publishes with fewer section fields
  • Case-study generation failures write a stub sidecar with every field flagged for review, so the write-up stage still succeeds

Authenticated remote publishing

  • publish --remote pushes straight to the live instance over its REST API — uploading screenshots, creating or updating the project, and triggering technology extraction — authenticated with an API key from a dedicated third-party-access collection using Payload's useAPIKey auth
  • Updates are deliberately partial: omitted fields preserve whatever is already in the CMS, concrete values replace, and an explicit null clears nullable fields, so hand-edits in the admin panel aren't clobbered by a re-publish
  • Projects arrive hidden by default unless --visible is passed, so nothing appears on the live site before review
  • A companion remote command exposes the same transport for one-off ping/list/get/create/update/delete/upload operations

Migration discipline and reproducible deploys

  • A Husky pre-commit hook runs ESLint, Prettier and payload migrate:create, so a schema change can't be committed without its migration
  • pnpm build is payload migrate && next build, meaning production migrations are applied as part of the image build rather than as a manual step
  • The Docker build uses BuildKit secret mounts for DATABASE_URI and PAYLOAD_SECRET — required for the build-time migration — so credentials never land in image history, and the build fails fast if they're absent
  • Deployment is fully automated: GitHub Actions builds and tags the image, pushes to GHCR, then triggers a Dokploy redeploy via its API

What I Contributed

  • Architected a multi-stage CLI ingest pipeline (discover → analyze → notes → writeup → shots → publish) using GitHub CLI for repo scanning, Claude for AI-generated write-ups and alt text, and Playwright for automated 2560×1440 screenshots, with full resumability via SHA-256 fingerprinting and transactional artifact promotion.
  • Built a custom React admin component injected via Payload's beforeDocumentControls that triggers Claude-based technology extraction, auto-creates missing Technology records with categorisation, and links them to projects — cataloguing 23 technologies on one project with zero manual entry.
  • Designed a file-first, manifest-driven pipeline where every stage produces hand-editable artifacts in a work directory with explicit needsReview flags, so the entire workflow degrades gracefully to manual copy-paste if upstream automation fails.
  • Implemented authenticated remote publishing over Payload's REST API with partial-update semantics (omitted fields preserve CMS values, concrete values overwrite, explicit null clears) so hand-edits in the admin panel are never clobbered by re-ingestion.

Outcome

  • Every deployment is reproducible: Husky pre-commit hooks enforce migration creation, Docker builds apply schema changes at image-build time using BuildKit secrets, and GitHub Actions + Dokploy handle zero-downtime deploys to self-hosted infrastructure.

  • The ingest pipeline is safe to re-run: fingerprint-based stage skipping, transactional screenshot promotion, manifest validation with nonzero exit on malformed input, and partial updates ensure existing content is never trashed.

The CMS gives my portfolio a properly typed, migration-tracked content backend with cloud media storage, but the real win is the ingest pipeline: adding a project went from writing a case study by hand to running a handful of commands and reviewing the output. The design is deliberately conservative — file-first artifacts, fingerprint-based resumability, transactional screenshot capture, partial updates that never overwrite hand-edits, and unit tests around each stage's logic — so the automation is safe to re-run without fear of trashing existing content.