Skip to main content
luke@terminal:/blog$ ls
luke@terminal:/blog$ cat building-projex-retrospective.md

I Built Projex and Never Wrote a Word About It

Created: 2026-04-14 | 7 min read

I shipped an entire open source project and never documented a single day of building it.

209 commits. February 21st to April 2nd. A full component library with a CLI, docs site, npm publishing pipeline, compound component API. And not one blog post while I was building any of it.

This is the opposite of what I said I wanted to do.


How It Started

Projex started with a question I couldn't stop thinking about: how do I showcase my projects on my personal site in a structured way?

I asked around. Did some research. Kept getting the same answer: most developers just build a custom solution. That struck me as strange. If everyone building a personal site needs a project showcase, why is everyone building the same thing from scratch?

That gap felt like an opportunity.

I had just redesigned my site with a terminal aesthetic and needed a projects page. The timing lined up. I was also looking for my first real OSS project, something with my name on it that other people could actually use.

So I started building.


The First Two Days Were a Blur

Looking at the git log, the first two days were intense. February 21st alone has something like 30 commits. Monorepo scaffold with pnpm workspaces. TypeScript types. GitHub API integration. Config helper. Compound components. A full demo app.

By the end of day one, I had the core architecture in place. That's not because I'm fast. It's because I had a clear picture of what I wanted before I started. The PRD was detailed enough that the initial implementation moved quickly.

February 22nd was more of the same. npm registry support. Product Hunt integration. Layout components. Filter and sort utilities. Tests. Performance benchmarks. CI/CD pipeline.

Two days in and I had a working product. That should have been the point where I wrote my first blog post. It wasn't.


The CLI Merge I Should Have Seen Coming

Early on, I split the project into separate CLI and core packages. Seemed like the right call at the time. Different concerns, different responsibilities, clean separation.

A few days later I merged them back into a single package.

The git log tells the story plainly: refactor: merge CLI and core into single @reallukemanning/folio package. No drama. No long agonising. Just a realisation that the separation was premature and added complexity I didn't need.

Architecture decisions made on day one are expensive to undo. I learned that one firsthand. The merge itself wasn't painful, but it was a reminder to bias toward simplicity when possible. I could always split things later if I actually needed to.


VitePress on Vercel: The Silent 404s

The docs site was its own adventure.

I built the documentation with VitePress and deployed it to Vercel. The build succeeded. Everything looked green. But the site returned 404s.

Not helpful error messages. Not warnings during build. Just... 404s. Silent, unhelpful 404s.

The git log from February 22nd tells the whole debugging arc:

fix: update VitePress output directory to point to src folder
fix: copy VitePress assets to src directory for Vercel deployment
refactor: use Vercel rewrites instead of file copying
fix: add rewrite for vp-icons.css at dist root
fix: remove cleanUrls and rewrites, let Vercel handle routing
fix: use copy-assets approach with all files in src/
fix: copy all root-level files to src/ for Vercel
fix: move HTML files to dist root for proper VitePress deployment
fix: resolve Vercel 404s by moving all build files to dist root
fix: add srcDir to VitePress config to resolve client-side 404s

Nine commits. Nine attempts. The problem was that VitePress and Vercel disagree about where built files should live and how routing should work. If your output directory or routing config is even slightly wrong, Vercel just silently 404s. No error. No hint. Just nothing.

I tried file copying. Then Vercel rewrites. Then removing rewrites. Then different directory structures. What finally worked was setting srcDir in the VitePress config to get the build output in the right place.

Obvious in hindsight. Everything is.


The OIDC Publishing Odyssey

This is the one that nearly broke me.

Getting npm publishing working through GitHub Actions took from March 7th to March 8th. Two full days of failed publish attempts, each one a commit trying a different approach.

The commit log from that stretch is almost comedy:

fix: use npm trusted publishing (remove NPM_TOKEN)
fix: add id-token permission for trusted publishing
fix: add registry-url for pnpm OIDC publishing
fix: use npm directly for OIDC publishing
revert: use NPM_TOKEN like v1.7.1
fix: use trusted publishing with pnpm
fix: use NPM_TOKEN for publishing
fix: use .npmrc for npm authentication
fix: use npm with OIDC for publishing
fix: add fetch mocking to npm-config tests to prevent CI timeout
fix: use npm with OIDC (remove all token references)
fix: configure npm registry and publish from correct directory
fix: add --provenance flag for OIDC publishing
fix: add repository field and upgrade npm for trusted publishing
fix: use npm@11.5.1 and publish directly with npm
fix: add contents:write permission for GitHub releases

I went back and forth between NPM_TOKEN and OIDC trusted publishing at least three times. The problem wasn't that OIDC is hard conceptually. The problem was that every combination of pnpm vs npm, registry-url config, provenance flags, and GitHub Actions permissions had its own subtle failure mode.

And the error messages were unhelpful. npm publish failures tend to be vague. You'd get "authentication required" or "unauthorized" with no indication of whether the token was wrong, the registry URL was wrong, or the permissions were wrong.

What finally worked: OIDC trusted publishing with the right combination of npm version (11.5.1 for provenance support), correct registry-url setup, and the --provenance flag. But getting there required trying basically every permutation.

Was it worth it? Yeah. OIDC means no secrets to rotate, no tokens to leak, no NPM_TOKEN sitting in GitHub Actions secrets. But the setup pain was real.


The Rename: Folio to Projex

The project was originally called Folio. Package name @reallukemanning/folio. Everything was Folio for the first two weeks.

Then on March 8th, I renamed the whole thing. chore: rename Folio to Projex and repackage to @manningworks/projex.

A rename sounds simple. It wasn't. Every data attribute in every component changed from data-folio-* to data-projex-*. Every import path. Every reference in docs. The pnpm lockfile. The GitHub workflows. The CLI commands. The README.

chore: rename Folio to Projex and repackage to @manningworks/projex
fix: update test files to use new data-projex-* attributes
fix: remove all remaining Folio references
fix: regenerate pnpm-lock.yaml with new package name

Four commits just for the rename, and that's not counting the docs updates.

I renamed it because I wanted a more distinctive name. Folio is generic. Every portfolio project is called Folio. Projex felt more unique and more aligned with what I was actually building, a project showcase framework, not a portfolio template.

The funny part: I didn't catch all the references. On April 2nd, nearly a month later, I was still fixing leftover Folio references: fix: correct Folio→Projex branding, unscoped npx commands, and CSS variable names (v1.1.4).


What I Actually Built

209 commits later, Projex is:

  • A shadcn-style component library with compound components (ProjectCard.Header, ProjectCard.Stats, etc.)
  • A CLI that auto-discovers your GitHub repos with npx @manningworks/projex init --github
  • Build-time data fetching from GitHub, npm, and Product Hunt (no runtime API calls)
  • Zero CSS shipped by default, style everything with data attributes
  • Multiple project types: GitHub, npm, Product Hunt, YouTube, Gumroad, manual, hybrid
  • A full VitePress docs site with interactive examples
  • OIDC trusted publishing with automated GitHub Releases
  • Vitest tests, ESLint, CI/CD

It's used in production on my own site. It's on npm. The docs are live. It's real.


Why I Never Wrote About It

I don't have a great answer for this.

Partly it was momentum. I was building fast and writing would have slowed me down. Partly it was the classic developer trap: I'll write about it when it's done. Then done kept moving. First it was "done when the core works." Then "done when I have docs." Then "done when publishing works." Then "done when I rename it." Then "done when it's public on GitHub."

Spoiler: it's never done.

Partly it was the thing I wrote about in posting into the void. I didn't want to write about something that no one was going to read. Which is backwards, because my blog is supposed to be documentation for myself first. I wrote a whole voice guide about this and then didn't follow my own advice.

The irony of saying I want to build in public and then building an entire project in private isn't lost on me.


What I'd Do Differently

I'd write while building. Even short posts. Even rough notes. The OIDC publishing saga alone deserved its own post. The VitePress debugging arc would have been useful for someone else going through the same thing.

The retrospective you're reading right now has less detail than it would have if I'd written it at the time. I'm reconstructing from git logs instead of writing from experience. That's a loss. The specific feeling of trying OIDC approach number seven at 11pm is gone. All I have is the commit message.

I'd also have started simpler with the architecture. The split CLI/core package was premature. I knew that pretty quickly but it's still time I spent on something I undid.

And I'd have picked the right name from the start.


Where Projex Is Now

Projex is at v1.1.4. It's on npm as @manningworks/projex. The docs are at projex.manningworks.dev. The repo is public on GitHub.

It hasn't set the world on fire. But it's real, it works, and I use it every day on my own site.

The next step for me is to actually build in public going forward. Not as a brand strategy or a growth hack. Just as documentation of what I'm working on, as it happens. This post is the start of that, even if it's late.

209 commits and not a single blog post. That's the antithesis of what I said I wanted to embody.

Better late than never, I guess.