<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Luke Manning - Blog — Projex</title>
        <link>https://lukemanning.ie/</link>
        <description>Breaking things. Building things. Writing about it. (tag: Projex)</description>
        <lastBuildDate>Wed, 30 Sep 2026 12:46:24 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <copyright>All rights reserved 2026, Luke Manning</copyright>
        <atom:link href="https://lukemanning.ie/feeds/projex.xml" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[I Built the ProjectGrid. It Still Doesn't Solve the Problem.]]></title>
            <link>https://lukemanning.ie/blog/i-built-the-projectgrid-it-still-doesnt-solve-the-problem</link>
            <guid isPermaLink="true">https://lukemanning.ie/blog/i-built-the-projectgrid-it-still-doesnt-solve-the-problem</guid>
            <pubDate>Wed, 19 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[<p>In April I wrote a post called <a href="/blog/i-shipped-a-library-now-what">I Shipped a Component Library. I Have No Idea If Anyone Actually Uses It.</a></p>
<p>For anyone who hasn't read that post, Projex is an npm library that renders a searchable, filterable grid of project cards from a config file.</p>
<p>Code review found twelve issues, the review agent asked if I could demo Projex in 60 seconds, I couldn't, and the conclusion was that the code wasn't the blocker. The first-time experience was. The fix I gestured at was a single component: <code>&#x3C;ProjectGrid></code>, one component that accepts the config and handles the data fetching internally. No manual async, no server/client split.</p>
<p>"The thing I need is something like a <code>&#x3C;ProjectGrid></code> component."</p>
<p>That was the unlock I named.</p>
<p>I built it. It shipped in Projex 1.4.0 on August 7th and it's live on npm, where the package went from 755 downloads in April to 143 last month.</p>
<p>Then I sat down and asked whether it actually did what I said it would.</p>
<p>It didn't.</p>
<hr>
<h2>What I actually shipped</h2>
<p>Here's the component. Stripped down (the actual export is <code>SmartProjectGrid</code>; the April post called it <code>&#x3C;ProjectGrid></code>, so I'll keep using that name):</p>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#E1E4E8">&#x3C;</span><span style="color:#79B8FF">SmartProjectGrid</span><span style="color:#B392F0"> projects</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{projects} </span><span style="color:#B392F0">showSearch</span><span style="color:#B392F0"> showFilters</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">  {(</span><span style="color:#FFAB70">project</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> &#x3C;</span><span style="color:#79B8FF">MyCard</span><span style="color:#B392F0"> project</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{project} />}</span></span>
<span class="line"><span style="color:#E1E4E8">&#x3C;/</span><span style="color:#79B8FF">SmartProjectGrid</span><span style="color:#E1E4E8">></span></span></code></pre>
<p>Vs the old way:</p>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#F97583">const</span><span style="color:#E1E4E8"> [</span><span style="color:#79B8FF">query</span><span style="color:#E1E4E8">, </span><span style="color:#79B8FF">setQuery</span><span style="color:#E1E4E8">] </span><span style="color:#F97583">=</span><span style="color:#B392F0"> useState</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">''</span><span style="color:#E1E4E8">)</span></span>
<span class="line"><span style="color:#F97583">const</span><span style="color:#E1E4E8"> [</span><span style="color:#79B8FF">tags</span><span style="color:#E1E4E8">, </span><span style="color:#79B8FF">setTags</span><span style="color:#E1E4E8">] </span><span style="color:#F97583">=</span><span style="color:#B392F0"> useState</span><span style="color:#E1E4E8">&#x3C;</span><span style="color:#79B8FF">string</span><span style="color:#E1E4E8">[]>([])</span></span>
<span class="line"><span style="color:#F97583">const</span><span style="color:#79B8FF"> searched</span><span style="color:#F97583"> =</span><span style="color:#B392F0"> useProjectSearch</span><span style="color:#E1E4E8">(projects, query)</span></span>
<span class="line"><span style="color:#F97583">const</span><span style="color:#79B8FF"> filtered</span><span style="color:#F97583"> =</span><span style="color:#B392F0"> useProjectFilters</span><span style="color:#E1E4E8">(searched, tags)</span></span></code></pre>
<p>That's the diff. The April post described a component that handles the data fetching internally, no manual async. The shipped one takes <code>projects</code> as a prop. You still write the fetching code yourself. That part of the idea didn't survive.</p>
<p>The new wrapper saves you maybe twenty lines of boilerplate per page where you want a sortable, filterable, searchable grid. Real savings. Worth shipping.</p>
<p>It does not solve the demo problem. Not even a little bit.</p>
<hr>
<h2>What I thought the demo problem was</h2>
<p>The April post framed the demo problem like this:</p>
<blockquote>
<p>Here's what getting Projex running actually involves right now:</p>
<ul>
<li>Install it</li>
<li>Write async data fetching code</li>
<li>Understand that GitHub data only loads at build time, not dev time</li>
<li>Wire up a server component for the data fetch</li>
<li>Wire up a client component for search</li>
<li>Deal with the server/client split</li>
</ul>
</blockquote>
<p>The conclusion was that this list is what blocks the 60-second demo. Six steps, server/client split, build-time fetching that doesn't show up in <code>pnpm dev</code>. None of that is the <code>&#x3C;ProjectGrid></code> fix.</p>
<p>What <code>&#x3C;ProjectGrid></code> actually does is collapse steps 4 through 6. It does not collapse steps 1 through 3. Steps 1 through 3 happen before any code matters. Someone lands on the npm page, reads the README, finds out the data fetching is manual and the GitHub data only loads at build time, and decides whether to install it at all. The wrapper component isn't visible at that moment. They haven't installed it yet. They're trying to imagine what their portfolio would look like.</p>
<p>If they install it, they get the wrapper. If they don't install it, the wrapper doesn't matter.</p>
<p>The wrapper helps the people who already decided they wanted it. It does not help the people who are deciding.</p>
<hr>
<h2>What I should have framed the problem as</h2>
<p>The demo problem is not "the API has too many steps."</p>
<p>The demo problem is "I cannot show someone what Projex does without them first installing it."</p>
<p>To show someone what Projex does, I need a URL. A page on the internet that renders Projex output with real data, that doesn't require them to clone a repo, that doesn't require them to wire up a Next.js project.</p>
<p>The npm page is a README. The README is text. Text is not a demo.</p>
<p>The <code>&#x3C;ProjectGrid></code> component does not solve that. It cannot solve that. No component can solve that. Only a URL solves that.</p>
<p>I already had one. <a href="/projects">My projects page</a> has been rendering straight from the Projex package since March, pulling its data from this blog's <a href="/blog/projex-cli-config-editor"><code>projex.config.ts</code></a>. The April post links to it as proof the library works. I named a component as the unlock in the same post that linked to the actual unlock.</p>
<hr>
<h2>Where that leaves the component</h2>
<p>In April I named the wrong thing. I named a component because Projex is code, so code felt like the answer. What I needed was something to send someone, and I already had it. The URL existed before the post did.</p>
<p>If I'd asked in April "can someone see what this does without installing it?", the answer would have been yes: my own projects page. I could have gone straight from that question to the Product Hunt launch, the thing the April post said couldn't happen without a demo. I asked "can I demo this in 60 seconds?" instead, which framed the answer as code I hadn't written yet. So I waited for code that was never the blocker.</p>
<p>The component itself is fine, for what it is. It saves maybe twenty lines per page for people who install Projex, and I moved this blog's catalogue onto Projex's own search, filter and sort helpers so the page and the library share one implementation. That's real. It was never the unlock.</p>
<p>So the next thing is a real demo site: a standalone URL whose whole job is showing what Projex does. This post is the soft launch of that idea. The demo doesn't exist yet. The post does.</p>]]></description>
            <content:encoded><![CDATA[<p>In April I wrote a post called <a href="/blog/i-shipped-a-library-now-what">I Shipped a Component Library. I Have No Idea If Anyone Actually Uses It.</a></p>
<p>For anyone who hasn't read that post, Projex is an npm library that renders a searchable, filterable grid of project cards from a config file.</p>
<p>Code review found twelve issues, the review agent asked if I could demo Projex in 60 seconds, I couldn't, and the conclusion was that the code wasn't the blocker. The first-time experience was. The fix I gestured at was a single component: <code>&#x3C;ProjectGrid></code>, one component that accepts the config and handles the data fetching internally. No manual async, no server/client split.</p>
<p>"The thing I need is something like a <code>&#x3C;ProjectGrid></code> component."</p>
<p>That was the unlock I named.</p>
<p>I built it. It shipped in Projex 1.4.0 on August 7th and it's live on npm, where the package went from 755 downloads in April to 143 last month.</p>
<p>Then I sat down and asked whether it actually did what I said it would.</p>
<p>It didn't.</p>
<hr>
<h2>What I actually shipped</h2>
<p>Here's the component. Stripped down (the actual export is <code>SmartProjectGrid</code>; the April post called it <code>&#x3C;ProjectGrid></code>, so I'll keep using that name):</p>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#E1E4E8">&#x3C;</span><span style="color:#79B8FF">SmartProjectGrid</span><span style="color:#B392F0"> projects</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{projects} </span><span style="color:#B392F0">showSearch</span><span style="color:#B392F0"> showFilters</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">  {(</span><span style="color:#FFAB70">project</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> &#x3C;</span><span style="color:#79B8FF">MyCard</span><span style="color:#B392F0"> project</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{project} />}</span></span>
<span class="line"><span style="color:#E1E4E8">&#x3C;/</span><span style="color:#79B8FF">SmartProjectGrid</span><span style="color:#E1E4E8">></span></span></code></pre>
<p>Vs the old way:</p>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#F97583">const</span><span style="color:#E1E4E8"> [</span><span style="color:#79B8FF">query</span><span style="color:#E1E4E8">, </span><span style="color:#79B8FF">setQuery</span><span style="color:#E1E4E8">] </span><span style="color:#F97583">=</span><span style="color:#B392F0"> useState</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">''</span><span style="color:#E1E4E8">)</span></span>
<span class="line"><span style="color:#F97583">const</span><span style="color:#E1E4E8"> [</span><span style="color:#79B8FF">tags</span><span style="color:#E1E4E8">, </span><span style="color:#79B8FF">setTags</span><span style="color:#E1E4E8">] </span><span style="color:#F97583">=</span><span style="color:#B392F0"> useState</span><span style="color:#E1E4E8">&#x3C;</span><span style="color:#79B8FF">string</span><span style="color:#E1E4E8">[]>([])</span></span>
<span class="line"><span style="color:#F97583">const</span><span style="color:#79B8FF"> searched</span><span style="color:#F97583"> =</span><span style="color:#B392F0"> useProjectSearch</span><span style="color:#E1E4E8">(projects, query)</span></span>
<span class="line"><span style="color:#F97583">const</span><span style="color:#79B8FF"> filtered</span><span style="color:#F97583"> =</span><span style="color:#B392F0"> useProjectFilters</span><span style="color:#E1E4E8">(searched, tags)</span></span></code></pre>
<p>That's the diff. The April post described a component that handles the data fetching internally, no manual async. The shipped one takes <code>projects</code> as a prop. You still write the fetching code yourself. That part of the idea didn't survive.</p>
<p>The new wrapper saves you maybe twenty lines of boilerplate per page where you want a sortable, filterable, searchable grid. Real savings. Worth shipping.</p>
<p>It does not solve the demo problem. Not even a little bit.</p>
<hr>
<h2>What I thought the demo problem was</h2>
<p>The April post framed the demo problem like this:</p>
<blockquote>
<p>Here's what getting Projex running actually involves right now:</p>
<ul>
<li>Install it</li>
<li>Write async data fetching code</li>
<li>Understand that GitHub data only loads at build time, not dev time</li>
<li>Wire up a server component for the data fetch</li>
<li>Wire up a client component for search</li>
<li>Deal with the server/client split</li>
</ul>
</blockquote>
<p>The conclusion was that this list is what blocks the 60-second demo. Six steps, server/client split, build-time fetching that doesn't show up in <code>pnpm dev</code>. None of that is the <code>&#x3C;ProjectGrid></code> fix.</p>
<p>What <code>&#x3C;ProjectGrid></code> actually does is collapse steps 4 through 6. It does not collapse steps 1 through 3. Steps 1 through 3 happen before any code matters. Someone lands on the npm page, reads the README, finds out the data fetching is manual and the GitHub data only loads at build time, and decides whether to install it at all. The wrapper component isn't visible at that moment. They haven't installed it yet. They're trying to imagine what their portfolio would look like.</p>
<p>If they install it, they get the wrapper. If they don't install it, the wrapper doesn't matter.</p>
<p>The wrapper helps the people who already decided they wanted it. It does not help the people who are deciding.</p>
<hr>
<h2>What I should have framed the problem as</h2>
<p>The demo problem is not "the API has too many steps."</p>
<p>The demo problem is "I cannot show someone what Projex does without them first installing it."</p>
<p>To show someone what Projex does, I need a URL. A page on the internet that renders Projex output with real data, that doesn't require them to clone a repo, that doesn't require them to wire up a Next.js project.</p>
<p>The npm page is a README. The README is text. Text is not a demo.</p>
<p>The <code>&#x3C;ProjectGrid></code> component does not solve that. It cannot solve that. No component can solve that. Only a URL solves that.</p>
<p>I already had one. <a href="/projects">My projects page</a> has been rendering straight from the Projex package since March, pulling its data from this blog's <a href="/blog/projex-cli-config-editor"><code>projex.config.ts</code></a>. The April post links to it as proof the library works. I named a component as the unlock in the same post that linked to the actual unlock.</p>
<hr>
<h2>Where that leaves the component</h2>
<p>In April I named the wrong thing. I named a component because Projex is code, so code felt like the answer. What I needed was something to send someone, and I already had it. The URL existed before the post did.</p>
<p>If I'd asked in April "can someone see what this does without installing it?", the answer would have been yes: my own projects page. I could have gone straight from that question to the Product Hunt launch, the thing the April post said couldn't happen without a demo. I asked "can I demo this in 60 seconds?" instead, which framed the answer as code I hadn't written yet. So I waited for code that was never the blocker.</p>
<p>The component itself is fine, for what it is. It saves maybe twenty lines per page for people who install Projex, and I moved this blog's catalogue onto Projex's own search, filter and sort helpers so the page and the library share one implementation. That's real. It was never the unlock.</p>
<p>So the next thing is a real demo site: a standalone URL whose whole job is showing what Projex does. This post is the soft launch of that idea. The demo doesn't exist yet. The post does.</p>]]></content:encoded>
            <category>projex</category>
            <category>reflections</category>
        </item>
        <item>
            <title><![CDATA[My Agent Closed Most of the Open Projex Issues in One Round Without Me Reading the Code]]></title>
            <link>https://lukemanning.ie/blog/my-agent-closed-most-projex-issues-in-one-round</link>
            <guid isPermaLink="true">https://lukemanning.ie/blog/my-agent-closed-most-projex-issues-in-one-round</guid>
            <pubDate>Wed, 19 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[<p>On a Saturday afternoon in August, I told my agent to fix every open issue in the Projex repo and prepare a release. There were ten open. Some were HIGH priority. Some were LOW.</p>
<p>I went for a walk.</p>
<p>When I came back, the work had landed across three branches. Each subagent had worked in its own worktree on its own branch. The build was green on each one. The tests were green. The lint was green. The typecheck was green. The release manager subagent had drafted the changelog. Nine of the ten issues were closed. The tenth stayed open — it needed a real union restructure, not a mechanical fix.</p>
<p>I had not read a single line of the diff yet.</p>
<h2>What I actually asked for</h2>
<p>I had a backlog of small things in <a href="/blog/building-projex-retrospective">the Projex repo</a>. Tagged-union cleanups. Type tightening. Documentation gaps. A bug where one of the smart-grid props was a documented prop but a no-op at runtime. A redundancy where two functions with slightly different spellings did the same thing.</p>
<p>I described this to my main agent. The main agent looked at the issue tracker, saw the labels (<code>bug</code>, <code>enhancement</code>, <code>documentation</code>), grouped them by file area, and <a href="/blog/opencode-subagent-permissions-ordering-trap">dispatched three subagents</a> in parallel.</p>
<p>One subagent got the package.json + bundling issues. One got the type-system + tagged-union issues. One got the documentation + test-coverage issues. Each one worked in a separate worktree on a separate branch. Each one committed locally and reported back. A fourth subagent, the release manager, handled release prep alongside them: the version bump and the changelog.</p>
<p>I watched the transcript. Mostly I stayed out of the way. I made tea.</p>
<h2>What the subagents actually did</h2>
<p>I saw the dispatch messages. I saw the report-back messages. I did not read the intermediate diffs. The subagents were set up to commit per-issue. Each commit was meant to be independently reviewable.</p>
<p>A few things I noticed in the report-backs:</p>
<ul>
<li>One subagent caught a redundancy I hadn't seen. Two exported functions, <code>normalizeStats</code> and <code>normaliseStats</code>, with the American and British spellings. Both did the same thing. The codebase had drifted to the British spelling in the actual logic. The American spelling was the alias. Both were exported. The subagent deprecated the American one with a JSDoc tag and updated the docs to point at the British spelling.</li>
<li>One subagent found a related issue while fixing another one. While narrowing the <code>ProjectStats</code> union, it noticed <code>FetchProjectDataResult.commits</code> was using <code>undefined</code> while sibling fields used <code>null</code>. The subagent opened a new issue and included the fix in the same branch.</li>
<li>One subagent flagged a peer-dependency problem I had been ignoring for two months. The CLI packages (<code>ts-morph</code>, <code>chalk</code>, <code>@inquirer/prompts</code>, <code>commander</code>) were installed by every consumer, even ones who only imported the components. The subagent moved them to optional <code>peerDependencies</code> so consumers importing only components stopped dragging in the CLI bundle.</li>
</ul>
<p>None of these were in my original brief. The subagents went past the edges of what I asked for, in the direction of "things that were obviously wrong in the same file area."</p>
<h2>Where I read the code</h2>
<p>The first time I read any of the code was after all three subagents finished and opened their PRs. I skimmed the diffs before merging. Not a line-by-line review. A sanity check.</p>
<p>I was looking for decisions the AI made without asking me. Function names I wouldn't have picked. Behaviour that wasn't in the brief. Edits that touched code outside the file area I asked about. The kind of things a real code review catches, except I was reviewing the decisions, not the code.</p>
<p>Some of the diffs were four lines. Some were thirty. None of them were complex enough to need a real review. They were tagged-union narrowings, JSDoc additions, dependency relocations. The kind of work where you skim it once, you understand it, you move on.</p>
<p>If I'd skimmed each PR as it landed, I'd have read the rename with no idea the docs were about to change under it. Reading the batch, I could see the <code>normaliseStats</code> deprecation and the docs update pointing at it in the same sitting. The batch skim was faster than piecemeal would have been.</p>
<h2>Where I did intervene</h2>
<p>I didn't push back on any of the code. The three branches each shipped clean. I steered the architecture around the loop, not the code inside it.</p>
<p>The dispatch went out as three parallel <code>opencode run</code> invocations, not three subagents. I asked for that change when the opencode TUI failed on the first attempt and the right path was to skip the interactive layer. I also argued for splitting the release prep out from the fix work, because trying to do both in the same dispatch kept blocking on the release-manager hitting its timeout before the fix branches landed.</p>
<h2>What this loop replaced</h2>
<p>My previous loop was one PR at a time. I'd describe an issue to the agent. The agent would open a PR. I'd skim it, sanity-check the decisions, merge or push back. One issue, one PR, one round of skimming. Repeat.</p>
<p>For this kind of small mechanical work, that's fine. It works. But the context switching adds up. Each PR is its own session — its own dispatch, its own transcript, its own review pass. The overhead is small per PR and large per backlog.</p>
<p>The new loop:</p>
<ol>
<li>Describe the backlog.</li>
<li>Wait.</li>
<li>Skim the batch of PRs.</li>
<li>Push the release prep.</li>
</ol>
<p>The release prep runs alongside the fix work instead of after it. One description covers all of them.</p>
<p>I want to be careful about what I'm claiming here. I'm not claiming the subagents did better work than the agent would have done one PR at a time. Most of these issues were mechanical. The interesting decisions — which redundancy to deprecate, which naming to standardize — the agent would have surfaced them either way, given the brief. What I'm claiming is that the per-PR overhead moved out of my hands and the interesting decisions stayed in my hands.</p>
<h2>Reading at the end, not in the middle</h2>
<p>I did not read the code while it was being written.</p>
<p>In the old loop, I skimmed each PR after the agent opened it. One PR at a time.</p>
<p>In the new loop, the skim happened after the writing finished across all three PRs. The subagents were the feedback loop during the work. I was the feedback loop at the end.</p>
<p>There's a different cost structure. A wrong fix in the old loop was caught in the per-PR skim, or it shipped. A wrong fix in the new loop is caught in the batch skim, or it ships.</p>
<p>I shipped none of the wrong fixes in this batch. The issues were mechanical and the code area was small. If I'd asked the subagents to redesign the <code>normalise</code> function, I would have read every line, pushed back, rewritten pieces.</p>
<p>The loop works for the kind of work that has a clear right answer. The loop does not work for the kind of work that needs taste. I haven't found the line yet.</p>
<p>For this batch, the line was "moves stuff around, adds JSDoc, narrows types." Below the line, I delegated. Above the line, I didn't. The line is in a different place than I would have guessed.</p>
<h2>Running it again</h2>
<p>I'm going to run this loop again. On a different repo. On a different kind of work.</p>
<p>I want to see what happens when the issues aren't mechanical. I want to see where the line moves. I want to see what kinds of work I delegate that I later wish I hadn't, and what kinds I keep that the loop could have handled.</p>
<p>I'm not going to delegate design decisions. I'm not going to delegate <a href="/blog/i-shipped-a-library-now-what">"what should this library do"</a>. I'm going to delegate "make this library do what it already says it does, correctly."</p>
<p>The batch skim at the end is non-negotiable. That's the part I own.</p>]]></description>
            <content:encoded><![CDATA[<p>On a Saturday afternoon in August, I told my agent to fix every open issue in the Projex repo and prepare a release. There were ten open. Some were HIGH priority. Some were LOW.</p>
<p>I went for a walk.</p>
<p>When I came back, the work had landed across three branches. Each subagent had worked in its own worktree on its own branch. The build was green on each one. The tests were green. The lint was green. The typecheck was green. The release manager subagent had drafted the changelog. Nine of the ten issues were closed. The tenth stayed open — it needed a real union restructure, not a mechanical fix.</p>
<p>I had not read a single line of the diff yet.</p>
<h2>What I actually asked for</h2>
<p>I had a backlog of small things in <a href="/blog/building-projex-retrospective">the Projex repo</a>. Tagged-union cleanups. Type tightening. Documentation gaps. A bug where one of the smart-grid props was a documented prop but a no-op at runtime. A redundancy where two functions with slightly different spellings did the same thing.</p>
<p>I described this to my main agent. The main agent looked at the issue tracker, saw the labels (<code>bug</code>, <code>enhancement</code>, <code>documentation</code>), grouped them by file area, and <a href="/blog/opencode-subagent-permissions-ordering-trap">dispatched three subagents</a> in parallel.</p>
<p>One subagent got the package.json + bundling issues. One got the type-system + tagged-union issues. One got the documentation + test-coverage issues. Each one worked in a separate worktree on a separate branch. Each one committed locally and reported back. A fourth subagent, the release manager, handled release prep alongside them: the version bump and the changelog.</p>
<p>I watched the transcript. Mostly I stayed out of the way. I made tea.</p>
<h2>What the subagents actually did</h2>
<p>I saw the dispatch messages. I saw the report-back messages. I did not read the intermediate diffs. The subagents were set up to commit per-issue. Each commit was meant to be independently reviewable.</p>
<p>A few things I noticed in the report-backs:</p>
<ul>
<li>One subagent caught a redundancy I hadn't seen. Two exported functions, <code>normalizeStats</code> and <code>normaliseStats</code>, with the American and British spellings. Both did the same thing. The codebase had drifted to the British spelling in the actual logic. The American spelling was the alias. Both were exported. The subagent deprecated the American one with a JSDoc tag and updated the docs to point at the British spelling.</li>
<li>One subagent found a related issue while fixing another one. While narrowing the <code>ProjectStats</code> union, it noticed <code>FetchProjectDataResult.commits</code> was using <code>undefined</code> while sibling fields used <code>null</code>. The subagent opened a new issue and included the fix in the same branch.</li>
<li>One subagent flagged a peer-dependency problem I had been ignoring for two months. The CLI packages (<code>ts-morph</code>, <code>chalk</code>, <code>@inquirer/prompts</code>, <code>commander</code>) were installed by every consumer, even ones who only imported the components. The subagent moved them to optional <code>peerDependencies</code> so consumers importing only components stopped dragging in the CLI bundle.</li>
</ul>
<p>None of these were in my original brief. The subagents went past the edges of what I asked for, in the direction of "things that were obviously wrong in the same file area."</p>
<h2>Where I read the code</h2>
<p>The first time I read any of the code was after all three subagents finished and opened their PRs. I skimmed the diffs before merging. Not a line-by-line review. A sanity check.</p>
<p>I was looking for decisions the AI made without asking me. Function names I wouldn't have picked. Behaviour that wasn't in the brief. Edits that touched code outside the file area I asked about. The kind of things a real code review catches, except I was reviewing the decisions, not the code.</p>
<p>Some of the diffs were four lines. Some were thirty. None of them were complex enough to need a real review. They were tagged-union narrowings, JSDoc additions, dependency relocations. The kind of work where you skim it once, you understand it, you move on.</p>
<p>If I'd skimmed each PR as it landed, I'd have read the rename with no idea the docs were about to change under it. Reading the batch, I could see the <code>normaliseStats</code> deprecation and the docs update pointing at it in the same sitting. The batch skim was faster than piecemeal would have been.</p>
<h2>Where I did intervene</h2>
<p>I didn't push back on any of the code. The three branches each shipped clean. I steered the architecture around the loop, not the code inside it.</p>
<p>The dispatch went out as three parallel <code>opencode run</code> invocations, not three subagents. I asked for that change when the opencode TUI failed on the first attempt and the right path was to skip the interactive layer. I also argued for splitting the release prep out from the fix work, because trying to do both in the same dispatch kept blocking on the release-manager hitting its timeout before the fix branches landed.</p>
<h2>What this loop replaced</h2>
<p>My previous loop was one PR at a time. I'd describe an issue to the agent. The agent would open a PR. I'd skim it, sanity-check the decisions, merge or push back. One issue, one PR, one round of skimming. Repeat.</p>
<p>For this kind of small mechanical work, that's fine. It works. But the context switching adds up. Each PR is its own session — its own dispatch, its own transcript, its own review pass. The overhead is small per PR and large per backlog.</p>
<p>The new loop:</p>
<ol>
<li>Describe the backlog.</li>
<li>Wait.</li>
<li>Skim the batch of PRs.</li>
<li>Push the release prep.</li>
</ol>
<p>The release prep runs alongside the fix work instead of after it. One description covers all of them.</p>
<p>I want to be careful about what I'm claiming here. I'm not claiming the subagents did better work than the agent would have done one PR at a time. Most of these issues were mechanical. The interesting decisions — which redundancy to deprecate, which naming to standardize — the agent would have surfaced them either way, given the brief. What I'm claiming is that the per-PR overhead moved out of my hands and the interesting decisions stayed in my hands.</p>
<h2>Reading at the end, not in the middle</h2>
<p>I did not read the code while it was being written.</p>
<p>In the old loop, I skimmed each PR after the agent opened it. One PR at a time.</p>
<p>In the new loop, the skim happened after the writing finished across all three PRs. The subagents were the feedback loop during the work. I was the feedback loop at the end.</p>
<p>There's a different cost structure. A wrong fix in the old loop was caught in the per-PR skim, or it shipped. A wrong fix in the new loop is caught in the batch skim, or it ships.</p>
<p>I shipped none of the wrong fixes in this batch. The issues were mechanical and the code area was small. If I'd asked the subagents to redesign the <code>normalise</code> function, I would have read every line, pushed back, rewritten pieces.</p>
<p>The loop works for the kind of work that has a clear right answer. The loop does not work for the kind of work that needs taste. I haven't found the line yet.</p>
<p>For this batch, the line was "moves stuff around, adds JSDoc, narrows types." Below the line, I delegated. Above the line, I didn't. The line is in a different place than I would have guessed.</p>
<h2>Running it again</h2>
<p>I'm going to run this loop again. On a different repo. On a different kind of work.</p>
<p>I want to see what happens when the issues aren't mechanical. I want to see where the line moves. I want to see what kinds of work I delegate that I later wish I hadn't, and what kinds I keep that the loop could have handled.</p>
<p>I'm not going to delegate design decisions. I'm not going to delegate <a href="/blog/i-shipped-a-library-now-what">"what should this library do"</a>. I'm going to delegate "make this library do what it already says it does, correctly."</p>
<p>The batch skim at the end is non-negotiable. That's the part I own.</p>]]></content:encoded>
            <category>ai</category>
            <category>opencode</category>
            <category>projex</category>
            <category>github</category>
        </item>
        <item>
            <title><![CDATA[I Shipped a Component Library. I Have No Idea If Anyone Actually Uses It.]]></title>
            <link>https://lukemanning.ie/blog/i-shipped-a-library-now-what</link>
            <guid isPermaLink="true">https://lukemanning.ie/blog/i-shipped-a-library-now-what</guid>
            <pubDate>Fri, 17 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[<p>I built a component library. It's called <a href="/projects/projex">Projex</a>. It's on npm at @manningworks/projex. It does one thing: makes it stupidly easy to put your GitHub repos, npm packages, Product Hunt launches, Gumroad products — all of it — on a portfolio page with stat counters, links, learnings and zero manual updating.</p>
<p>I wrote about <a href="/blog/building-projex-retrospective">building Projex</a> and <a href="/blog/dogfooding-projex-devto">dogfooding it on dev.to</a> if you want to read the full story.</p>
<p>v1.3.1 is shipped as of this article. It's Public on <a href="https://github.com/ManningWorks/Projex">Github</a> and <a href="https://www.npmjs.com/package/@manningworks/projex">npm</a>.</p>
<p>Over six hundred people downloaded it last month according to NPM stats.</p>
<p>I have no idea if any of them actually used it.</p>
<hr>
<h2>The Problem I Was Solving</h2>
<p>I wanted a portfolio page. I didn't want to manually update it every time I pushed a commit. I wanted live data — GitHub stars, npm downloads, that kind of thing — to just be there.</p>
<p>I looked around. GitHub Readme Stats exists. It's just badges. README generators are for READMEs. Nothing I found did the thing I wanted: a proper portfolio page, shadcn-style, where you own the components, where the data comes from the actual APIs.</p>
<p>So I described the problem to my AI agent and we started building.</p>
<p>That was months ago.</p>
<hr>
<h2>What the Code Review Turned Up</h2>
<p>Last week I asked my AI agent to do a proper review. Line by line through the normalisation function, the fetchers, the component API. I wanted it to evaluate code but I also wanted to consider the perspective of a user trying to implement Projex.</p>
<p>It found twelve issues.</p>
<p>Some of them were real problems I'd missed. The error handling was inconsistent — Zod validation throws, but API failures return null and warn. The 400-line <code>normalise</code> function does too much: validation, seven different API calls, field resolution, commits fetching. The TypeScript output types don't match runtime behavior — a <code>github</code> type project can theoretically have Lemon Squeezy revenue stats in the type system even though it never will.</p>
<p>Twelve issues. The code still works. My own <a href="/projects">Projects</a> page is proof of that.</p>
<p>The issues were real. They were also not the problem.</p>
<hr>
<h2>The Question I Couldn't Answer</h2>
<p>I was talking through the findings with my agent and it asked me something I didn't have a good answer for.</p>
<p>"Could you demo this in 60 seconds?"</p>
<p>I sat with that for a while. Here's what getting Projex running actually involves right now:</p>
<ul>
<li>Install it</li>
<li>Write async data fetching code</li>
<li>Understand that GitHub data only loads at build time, not dev time — the fetches are server-side and only run during a build, so <code>pnpm dev</code> shows empty cards until you actually build</li>
<li>Wire up a server component for the data fetch</li>
<li>Wire up a client component for search</li>
<li>Deal with the server/client split</li>
</ul>
<p>That is not demoable.</p>
<p>I couldn't show you what Projex does in 60 seconds because to see what it does, you have to build something with it. The value is hidden behind implementation.</p>
<p>Six hundred installs tells me people are curious enough to try. It doesn't tell me anyone got to the part where it clicks.</p>
<hr>
<h2>The Thing Blocking Everything</h2>
<p>The code isn't the blocker.</p>
<p>The first-time experience is the blocker.</p>
<p>If I can't show someone "install this, add these five lines, here's your live portfolio" in under five minutes, the demo falls apart. If the demo falls apart, Product Hunt doesn't make sense. If PH doesn't make sense, the distribution strategy doesn't work.</p>
<p>I started picturing what the demo would actually look like. Me, a fresh Next.js project, pasting in a snippet and getting a live portfolio back. For that to work, the async fetching and the server/client split have to disappear inside one component. The person using it shouldn't have to know any of that exists.</p>
<p>The thing I need is something like a <code>&#x3C;ProjectGrid></code> component. One component that accepts the config, handles the data fetching internally, works with just <code>&#x3C;ProjectGrid projects={projects} /></code> in a page.</p>
<p>No manual async. No server/client split. No reading 575 lines of getting-started docs first.</p>
<p>Just add your project details. Here's the grid. Done.</p>
<p>Once that exists, I can screenshot it. Once I can screenshot it, I can demo it. Once I can demo it, I can ship it on PH. The chain is short but it starts with that component.</p>
<hr>
<h2>What I Learned Building in Public With an AI</h2>
<p>I don't know if I'm doing this right.</p>
<p>I describe problems to an AI agent. It suggests solutions. We iterate. Sometimes I understand why. Sometimes I just know it works.</p>
<p>The code review was the first time I really sat with what we'd built and asked "but is this good?" Not "does it work?" I know it works. I just hadn't put much thought into whether it was good.</p>
<p>The twelve issues were real. The agent found them faster than I would have. But the question of whether it <em>matters</em> is still mine to answer.</p>
<p>And the answer, I think, is that the library is probably fine. The problem is I've been building a library when I should have been building an experience.</p>
<p>I went back to the getting-started docs after that conversation. Five hundred and seventy-five lines. All necessary, as far as I could tell when I wrote each one. None of it gets a person to the moment where Projex clicks. Nobody stumbles into five hundred lines of setup and comes out thinking "oh, this is what I needed."</p>
<p>The six hundred people who installed it found it because they were already looking. They searched, they read a post, they followed a link. That's a real audience and I'm grateful for it. But they'd already decided they needed something like this before they got there.</p>
<p>This is what I keep <a href="/blog/posting-into-the-void">posting into the void</a> about. I know people downloaded it. I don't know if any of them got to the part where it clicks.</p>
<p>Same codebase. Different demo.</p>
<hr>
<h2>What I'm Going to Find Out</h2>
<p>I'm going to build the <code>&#x3C;ProjectGrid></code> component. I'm going to get it to the point where I can open a fresh Next.js project and have a live portfolio in under five minutes.</p>
<p>Then I'm going to actually try to demo it.</p>
<p>I don't know if it'll work. I don't know if 612 installs becomes 6,000 or stays at 612 or drops to zero.</p>
<p>But I know the code isn't the problem anymore.</p>
<p>I think I needed to talk to my AI agent about it to figure that out.</p>]]></description>
            <content:encoded><![CDATA[<p>I built a component library. It's called <a href="/projects/projex">Projex</a>. It's on npm at @manningworks/projex. It does one thing: makes it stupidly easy to put your GitHub repos, npm packages, Product Hunt launches, Gumroad products — all of it — on a portfolio page with stat counters, links, learnings and zero manual updating.</p>
<p>I wrote about <a href="/blog/building-projex-retrospective">building Projex</a> and <a href="/blog/dogfooding-projex-devto">dogfooding it on dev.to</a> if you want to read the full story.</p>
<p>v1.3.1 is shipped as of this article. It's Public on <a href="https://github.com/ManningWorks/Projex">Github</a> and <a href="https://www.npmjs.com/package/@manningworks/projex">npm</a>.</p>
<p>Over six hundred people downloaded it last month according to NPM stats.</p>
<p>I have no idea if any of them actually used it.</p>
<hr>
<h2>The Problem I Was Solving</h2>
<p>I wanted a portfolio page. I didn't want to manually update it every time I pushed a commit. I wanted live data — GitHub stars, npm downloads, that kind of thing — to just be there.</p>
<p>I looked around. GitHub Readme Stats exists. It's just badges. README generators are for READMEs. Nothing I found did the thing I wanted: a proper portfolio page, shadcn-style, where you own the components, where the data comes from the actual APIs.</p>
<p>So I described the problem to my AI agent and we started building.</p>
<p>That was months ago.</p>
<hr>
<h2>What the Code Review Turned Up</h2>
<p>Last week I asked my AI agent to do a proper review. Line by line through the normalisation function, the fetchers, the component API. I wanted it to evaluate code but I also wanted to consider the perspective of a user trying to implement Projex.</p>
<p>It found twelve issues.</p>
<p>Some of them were real problems I'd missed. The error handling was inconsistent — Zod validation throws, but API failures return null and warn. The 400-line <code>normalise</code> function does too much: validation, seven different API calls, field resolution, commits fetching. The TypeScript output types don't match runtime behavior — a <code>github</code> type project can theoretically have Lemon Squeezy revenue stats in the type system even though it never will.</p>
<p>Twelve issues. The code still works. My own <a href="/projects">Projects</a> page is proof of that.</p>
<p>The issues were real. They were also not the problem.</p>
<hr>
<h2>The Question I Couldn't Answer</h2>
<p>I was talking through the findings with my agent and it asked me something I didn't have a good answer for.</p>
<p>"Could you demo this in 60 seconds?"</p>
<p>I sat with that for a while. Here's what getting Projex running actually involves right now:</p>
<ul>
<li>Install it</li>
<li>Write async data fetching code</li>
<li>Understand that GitHub data only loads at build time, not dev time — the fetches are server-side and only run during a build, so <code>pnpm dev</code> shows empty cards until you actually build</li>
<li>Wire up a server component for the data fetch</li>
<li>Wire up a client component for search</li>
<li>Deal with the server/client split</li>
</ul>
<p>That is not demoable.</p>
<p>I couldn't show you what Projex does in 60 seconds because to see what it does, you have to build something with it. The value is hidden behind implementation.</p>
<p>Six hundred installs tells me people are curious enough to try. It doesn't tell me anyone got to the part where it clicks.</p>
<hr>
<h2>The Thing Blocking Everything</h2>
<p>The code isn't the blocker.</p>
<p>The first-time experience is the blocker.</p>
<p>If I can't show someone "install this, add these five lines, here's your live portfolio" in under five minutes, the demo falls apart. If the demo falls apart, Product Hunt doesn't make sense. If PH doesn't make sense, the distribution strategy doesn't work.</p>
<p>I started picturing what the demo would actually look like. Me, a fresh Next.js project, pasting in a snippet and getting a live portfolio back. For that to work, the async fetching and the server/client split have to disappear inside one component. The person using it shouldn't have to know any of that exists.</p>
<p>The thing I need is something like a <code>&#x3C;ProjectGrid></code> component. One component that accepts the config, handles the data fetching internally, works with just <code>&#x3C;ProjectGrid projects={projects} /></code> in a page.</p>
<p>No manual async. No server/client split. No reading 575 lines of getting-started docs first.</p>
<p>Just add your project details. Here's the grid. Done.</p>
<p>Once that exists, I can screenshot it. Once I can screenshot it, I can demo it. Once I can demo it, I can ship it on PH. The chain is short but it starts with that component.</p>
<hr>
<h2>What I Learned Building in Public With an AI</h2>
<p>I don't know if I'm doing this right.</p>
<p>I describe problems to an AI agent. It suggests solutions. We iterate. Sometimes I understand why. Sometimes I just know it works.</p>
<p>The code review was the first time I really sat with what we'd built and asked "but is this good?" Not "does it work?" I know it works. I just hadn't put much thought into whether it was good.</p>
<p>The twelve issues were real. The agent found them faster than I would have. But the question of whether it <em>matters</em> is still mine to answer.</p>
<p>And the answer, I think, is that the library is probably fine. The problem is I've been building a library when I should have been building an experience.</p>
<p>I went back to the getting-started docs after that conversation. Five hundred and seventy-five lines. All necessary, as far as I could tell when I wrote each one. None of it gets a person to the moment where Projex clicks. Nobody stumbles into five hundred lines of setup and comes out thinking "oh, this is what I needed."</p>
<p>The six hundred people who installed it found it because they were already looking. They searched, they read a post, they followed a link. That's a real audience and I'm grateful for it. But they'd already decided they needed something like this before they got there.</p>
<p>This is what I keep <a href="/blog/posting-into-the-void">posting into the void</a> about. I know people downloaded it. I don't know if any of them got to the part where it clicks.</p>
<p>Same codebase. Different demo.</p>
<hr>
<h2>What I'm Going to Find Out</h2>
<p>I'm going to build the <code>&#x3C;ProjectGrid></code> component. I'm going to get it to the point where I can open a fresh Next.js project and have a live portfolio in under five minutes.</p>
<p>Then I'm going to actually try to demo it.</p>
<p>I don't know if it'll work. I don't know if 612 installs becomes 6,000 or stays at 612 or drops to zero.</p>
<p>But I know the code isn't the problem anymore.</p>
<p>I think I needed to talk to my AI agent about it to figure that out.</p>]]></content:encoded>
            <category>projex</category>
        </item>
        <item>
            <title><![CDATA[I Got Tired of Editing the Projex Config File So I Built a CLI]]></title>
            <link>https://lukemanning.ie/blog/projex-cli-config-editor</link>
            <guid isPermaLink="true">https://lukemanning.ie/blog/projex-cli-config-editor</guid>
            <pubDate>Thu, 16 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[<p>Projex is my internal tool for managing the projects page on this site. It stores all project data in <code>projex.config.ts</code>. That's by design. Type-safe, validated at build time, lives in the repo alongside the code. Works well.</p>
<p>Until you want to add a learning entry to a project.</p>
<p>Then you're opening a TypeScript file, finding the right project in the array, counting commas to make sure you don't mess up the object syntax, adding a new entry to the <code>struggles</code> array, making sure the date format is right, closing the brackets properly. All for something that should be one command.</p>
<p>I built this library. I wrote the config schema. And even I was getting it wrong. I'd actually <a href="/blog/building-projex-retrospective">written about Projex before</a> - you can see it on my <a href="/projects/projex">projects page</a>. The dev.to integration <a href="/blog/dogfooding-projex-devto">broke immediately</a> the first time I tried it. Same theme: I built it, but I wasn't actually using it like a real user would.</p>
<hr>
<h2>The Tipping Point</h2>
<p>It was something small. I wanted to add a timeline entry to a project. Opened <code>projex.config.ts</code>, scrolled to the right project, added the entry. Saved. Build passed. Checked the site.</p>
<p>The entry was on the wrong project.</p>
<p>I'd pasted it inside the wrong object in the array. Easy mistake when your config is a nested TypeScript data structure and you're editing it like a text file. No validation catches it because the types are fine. The entry is valid. It's just... in the wrong place.</p>
<p>That was the moment I thought: there has to be a better way.</p>
<hr>
<h2>The CLI</h2>
<p>I decided to build a CLI. Not because CLIs are fun to build. Because the alternative was continuing to hand-edit a TypeScript file every time I wanted to update my projects page.</p>
<p>The scope was straightforward. I needed commands for everything I was doing manually:</p>
<ul>
<li>Add and remove projects</li>
<li>Add and remove learning entries, timeline entries, posts</li>
<li>Edit project fields</li>
<li>List what's in the config</li>
</ul>
<p>The tricky part was that <code>projex.config.ts</code> is a real TypeScript file. Not JSON. Not YAML. It uses <code>defineProjects()</code>, has imports, can have comments. I needed to parse and modify it without destroying the structure.</p>
<p>I Googled around for TypeScript AST manipulation and found <a href="https://ts-morph.com/">ts-morph</a>. It reads the file as a tree of nodes rather than raw text. Find a project by ID, add an entry to an array, change a property value. All without touching the surrounding code, comments, or formatting.</p>
<p>I'd seen ts-morph mentioned before when I was working on something similar. It felt like the right tool for this specific job. The other option was just string manipulation, which seemed brittle.</p>
<p>The actual manipulation was less painful than I expected. I figured I'd have to write a lot of traversal code to navigate the nested structure. But ts-morph's API handles array operations cleanly. Adding to an array, removing from an array, finding nodes by specific properties. It just worked.</p>
<hr>
<h2>The Bugs I Found By Testing More</h2>
<p>The first version worked. But there were edge cases I didn't catch until I sat down and wrote tests for scenarios that seemed unlikely.</p>
<p>The remove commands showed entries as <code>#0</code>, <code>#1</code>, <code>#2</code> in the interactive prompt. Useless. You'd have to know which index corresponded to which entry. I changed them to show the actual content. Learning entries show <code>[challenge] Struggled with state management...</code>. Timeline entries show <code>2026-04-15 - v1.0 released</code>. Posts show the title and date. Obvious in retrospect.</p>
<p>Then there was a subtler bug. The code that reads entries from the config filters for object literals in the array. If someone had a spread element in there, like <code>[...sharedEntries, { type: 'challenge', text: 'actual entry' }]</code>, the filtered list would skip the spread. But the index reported to the user would be wrong. They'd pick what looked like entry 0, but the actual array index was 1. The wrong entry would get deleted silently.</p>
<p>Fixed that by tracking original indices through the parsed structure instead of using the filtered list position. Added an integration test with a spread element to prove it works.</p>
<p>The edit command had its own issue. It let you set any field on any project type with just a warning. <code>--channel-id</code> on a GitHub project? Sure, warned and proceeded. That's not a warning situation. That's a "you're doing something wrong" situation. Changed it to error and exit.</p>
<hr>
<h2>The --unset Flag</h2>
<p>The one feature I didn't originally plan for was removing fields. The CLI could add, edit, and remove projects and entries. But if you accidentally set a field that shouldn't be there, you were back to editing the config file manually.</p>
<p>That defeated the purpose.</p>
<p>So I added <code>--unset</code>. <code>projex edit project my-project --unset description</code> removes the field entirely. Protected fields like <code>id</code>, <code>type</code>, and the array fields can't be removed. It can't be combined with other edit flags either, because that would be ambiguous.</p>
<p>Simple feature. But without it, the CLI wasn't complete. You'd still need to touch the config file for at least one class of changes.</p>
<hr>
<h2>Where Things Stand</h2>
<p>881 tests. 55 test files. The integration tests create actual temporary config files and exercise the real parsing logic, not just mocked versions of it.</p>
<p>The CLI handles init, add, edit, remove, and list. Interactive mode when you don't provide flags, non-interactive mode with flags for scripting. Type-specific field validation that errors instead of warns. Descriptive labels on remove prompts instead of index numbers.</p>
<p>I've been using it for a day and it's already changed how I interact with my projects page. Adding a learning entry went from "open file, find project, count commas, hope for the best" to:</p>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">projex</span><span style="color:#9ECBFF"> add</span><span style="color:#9ECBFF"> learning</span><span style="color:#9ECBFF"> projex</span><span style="color:#79B8FF"> --type</span><span style="color:#9ECBFF"> challenge</span><span style="color:#79B8FF"> --text</span><span style="color:#9ECBFF"> "Config file parsing is surprisingly tricky"</span></span></code></pre>]]></description>
            <content:encoded><![CDATA[<p>Projex is my internal tool for managing the projects page on this site. It stores all project data in <code>projex.config.ts</code>. That's by design. Type-safe, validated at build time, lives in the repo alongside the code. Works well.</p>
<p>Until you want to add a learning entry to a project.</p>
<p>Then you're opening a TypeScript file, finding the right project in the array, counting commas to make sure you don't mess up the object syntax, adding a new entry to the <code>struggles</code> array, making sure the date format is right, closing the brackets properly. All for something that should be one command.</p>
<p>I built this library. I wrote the config schema. And even I was getting it wrong. I'd actually <a href="/blog/building-projex-retrospective">written about Projex before</a> - you can see it on my <a href="/projects/projex">projects page</a>. The dev.to integration <a href="/blog/dogfooding-projex-devto">broke immediately</a> the first time I tried it. Same theme: I built it, but I wasn't actually using it like a real user would.</p>
<hr>
<h2>The Tipping Point</h2>
<p>It was something small. I wanted to add a timeline entry to a project. Opened <code>projex.config.ts</code>, scrolled to the right project, added the entry. Saved. Build passed. Checked the site.</p>
<p>The entry was on the wrong project.</p>
<p>I'd pasted it inside the wrong object in the array. Easy mistake when your config is a nested TypeScript data structure and you're editing it like a text file. No validation catches it because the types are fine. The entry is valid. It's just... in the wrong place.</p>
<p>That was the moment I thought: there has to be a better way.</p>
<hr>
<h2>The CLI</h2>
<p>I decided to build a CLI. Not because CLIs are fun to build. Because the alternative was continuing to hand-edit a TypeScript file every time I wanted to update my projects page.</p>
<p>The scope was straightforward. I needed commands for everything I was doing manually:</p>
<ul>
<li>Add and remove projects</li>
<li>Add and remove learning entries, timeline entries, posts</li>
<li>Edit project fields</li>
<li>List what's in the config</li>
</ul>
<p>The tricky part was that <code>projex.config.ts</code> is a real TypeScript file. Not JSON. Not YAML. It uses <code>defineProjects()</code>, has imports, can have comments. I needed to parse and modify it without destroying the structure.</p>
<p>I Googled around for TypeScript AST manipulation and found <a href="https://ts-morph.com/">ts-morph</a>. It reads the file as a tree of nodes rather than raw text. Find a project by ID, add an entry to an array, change a property value. All without touching the surrounding code, comments, or formatting.</p>
<p>I'd seen ts-morph mentioned before when I was working on something similar. It felt like the right tool for this specific job. The other option was just string manipulation, which seemed brittle.</p>
<p>The actual manipulation was less painful than I expected. I figured I'd have to write a lot of traversal code to navigate the nested structure. But ts-morph's API handles array operations cleanly. Adding to an array, removing from an array, finding nodes by specific properties. It just worked.</p>
<hr>
<h2>The Bugs I Found By Testing More</h2>
<p>The first version worked. But there were edge cases I didn't catch until I sat down and wrote tests for scenarios that seemed unlikely.</p>
<p>The remove commands showed entries as <code>#0</code>, <code>#1</code>, <code>#2</code> in the interactive prompt. Useless. You'd have to know which index corresponded to which entry. I changed them to show the actual content. Learning entries show <code>[challenge] Struggled with state management...</code>. Timeline entries show <code>2026-04-15 - v1.0 released</code>. Posts show the title and date. Obvious in retrospect.</p>
<p>Then there was a subtler bug. The code that reads entries from the config filters for object literals in the array. If someone had a spread element in there, like <code>[...sharedEntries, { type: 'challenge', text: 'actual entry' }]</code>, the filtered list would skip the spread. But the index reported to the user would be wrong. They'd pick what looked like entry 0, but the actual array index was 1. The wrong entry would get deleted silently.</p>
<p>Fixed that by tracking original indices through the parsed structure instead of using the filtered list position. Added an integration test with a spread element to prove it works.</p>
<p>The edit command had its own issue. It let you set any field on any project type with just a warning. <code>--channel-id</code> on a GitHub project? Sure, warned and proceeded. That's not a warning situation. That's a "you're doing something wrong" situation. Changed it to error and exit.</p>
<hr>
<h2>The --unset Flag</h2>
<p>The one feature I didn't originally plan for was removing fields. The CLI could add, edit, and remove projects and entries. But if you accidentally set a field that shouldn't be there, you were back to editing the config file manually.</p>
<p>That defeated the purpose.</p>
<p>So I added <code>--unset</code>. <code>projex edit project my-project --unset description</code> removes the field entirely. Protected fields like <code>id</code>, <code>type</code>, and the array fields can't be removed. It can't be combined with other edit flags either, because that would be ambiguous.</p>
<p>Simple feature. But without it, the CLI wasn't complete. You'd still need to touch the config file for at least one class of changes.</p>
<hr>
<h2>Where Things Stand</h2>
<p>881 tests. 55 test files. The integration tests create actual temporary config files and exercise the real parsing logic, not just mocked versions of it.</p>
<p>The CLI handles init, add, edit, remove, and list. Interactive mode when you don't provide flags, non-interactive mode with flags for scripting. Type-specific field validation that errors instead of warns. Descriptive labels on remove prompts instead of index numbers.</p>
<p>I've been using it for a day and it's already changed how I interact with my projects page. Adding a learning entry went from "open file, find project, count commas, hope for the best" to:</p>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">projex</span><span style="color:#9ECBFF"> add</span><span style="color:#9ECBFF"> learning</span><span style="color:#9ECBFF"> projex</span><span style="color:#79B8FF"> --type</span><span style="color:#9ECBFF"> challenge</span><span style="color:#79B8FF"> --text</span><span style="color:#9ECBFF"> "Config file parsing is surprisingly tricky"</span></span></code></pre>]]></content:encoded>
            <category>projex</category>
        </item>
        <item>
            <title><![CDATA[Dogfooding Projex: My Own Library Broke on the First New Feature I Tried]]></title>
            <link>https://lukemanning.ie/blog/dogfooding-projex-devto</link>
            <guid isPermaLink="true">https://lukemanning.ie/blog/dogfooding-projex-devto</guid>
            <pubDate>Wed, 15 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[<p>I wanted to add my dev.to profile to my projects page. Simple enough. Projex, my own library, has a <code>devto</code> type built in. I wrote the code. Added it to <code>projex.config.ts</code>. Checked the page.</p>
<p>No stats. Nothing. Just an empty project card staring back at me.</p>
<hr>
<h2>The Setup</h2>
<p>Projex supports nine project types. GitHub, npm, Product Hunt, dev.to, and a few others. Each type fetches data from a different API and normalises it into a standard format. Stats, links, descriptions.</p>
<p>I'd tested GitHub and hybrid (GitHub + npm) types extensively. Those are what I use for my other projects. The dev.to type had been sitting there since I shipped it. Never actually used it.</p>
<p>So I added this to my config:</p>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#E1E4E8">{</span></span>
<span class="line"><span style="color:#B392F0">  id</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">'devto'</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#B392F0">  name</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">'dev.to/manningworks'</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#B392F0">  type</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">'devto'</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#B392F0">  username</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">'manningworks'</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#B392F0">  status</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">'active'</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#6A737D">  // ... background, timeline, all the usual fields</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span></code></pre>
<p>Hit the projects page. The card rendered fine. Name, description, timeline, all good. But the stats section was completely empty. No articles. No views. No reactions.</p>
<p>My first thought was that the API call was failing silently. Maybe a network error I wasn't catching, or the username was wrong somehow. I checked the <code>npm run dev</code> console logs. Nothing. The fetch had run fine. One article, valid JSON. The data was there.</p>
<p>So the fetch was working. Something else was wrong.</p>
<hr>
<h2>Layer One: Wrong API Field Names</h2>
<p>I opened the API response side by side with the Projex source code. That's when I saw it.</p>
<p>Here's what Projex's <code>fetchDevToUser</code> was doing:</p>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#F97583">const</span><span style="color:#79B8FF"> totalViews</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> data.</span><span style="color:#B392F0">reduce</span><span style="color:#E1E4E8">(</span></span>
<span class="line"><span style="color:#E1E4E8">  (</span><span style="color:#FFAB70">sum</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">article</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> sum </span><span style="color:#F97583">+</span><span style="color:#E1E4E8"> article.page_views_count, </span><span style="color:#79B8FF">0</span></span>
<span class="line"><span style="color:#E1E4E8">);</span></span>
<span class="line"><span style="color:#F97583">const</span><span style="color:#79B8FF"> totalReactions</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> data.</span><span style="color:#B392F0">reduce</span><span style="color:#E1E4E8">(</span></span>
<span class="line"><span style="color:#E1E4E8">  (</span><span style="color:#FFAB70">sum</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">article</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> sum </span><span style="color:#F97583">+</span><span style="color:#E1E4E8"> article.positive_reactions_count, </span><span style="color:#79B8FF">0</span></span>
<span class="line"><span style="color:#E1E4E8">);</span></span></code></pre>
<p>And here's what the live API response actually contained: <code>public_reactions_count</code> and no <code>page_views_count</code> at all.</p>
<p>Two problems.</p>
<p><code>page_views_count</code> is only returned on authenticated requests. The public API doesn't include it. So it was <code>undefined</code>, and <code>0 + undefined = NaN</code>.</p>
<p><code>positive_reactions_count</code> was deprecated by dev.to in favour of <code>public_reactions_count</code>. The public response returns <code>public_reactions_count</code>, not <code>positive_reactions_count</code>.</p>
<p>So both values were <code>NaN</code>. The stats existed but were garbage. The component saw <code>NaN !== undefined</code> as true, tried to render them, and... nothing rendered properly.</p>
<p>Classic. I'd written code against API docs without actually checking what the live response looks like.</p>
<p>Raised an issue on my own repo. Fixed the field names. Shipped Projex 1.2.0.</p>
<p>Then I updated my site to 1.2.0, reloaded the page, and... still no stats. Different reason this time, but same empty card.</p>
<hr>
<h2>Layer Two: Missing Render Block</h2>
<p>But that wasn't the only problem. Even if the stats had been correct from the start, they still wouldn't have shown up.</p>
<p>I started digging through <code>ProjectDetail.tsx</code>, the component that renders project cards. It had render blocks for GitHub stats (stars, forks), npm stats (downloads, version), and Product Hunt stats (upvotes, comments).</p>
<p>No dev.to block. At all.</p>
<p>The component knew about three stat types. Projex supported nine project types. I'd never added the render logic for the newer ones because I'd never needed it. The data was there, normalised and everything. The component just had no idea what to do with it.</p>
<p>So I added a dev.to section:</p>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#E1E4E8">{project.stats.articleCount </span><span style="color:#F97583">!==</span><span style="color:#79B8FF"> undefined</span><span style="color:#F97583"> ||</span><span style="color:#6A737D"> /* ... */</span><span style="color:#F97583"> ?</span><span style="color:#E1E4E8"> (</span></span>
<span class="line"><span style="color:#E1E4E8">  &#x3C;</span><span style="color:#85E89D">div</span><span style="color:#B392F0"> className</span><span style="color:#F97583">=</span><span style="color:#9ECBFF">"mb-4"</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">    &#x3C;</span><span style="color:#85E89D">span</span><span style="color:#B392F0"> className</span><span style="color:#F97583">=</span><span style="color:#9ECBFF">"text-xs text-terminal-dim"</span><span style="color:#E1E4E8">>dev.to: &#x3C;/</span><span style="color:#85E89D">span</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">    &#x3C;</span><span style="color:#85E89D">div</span><span style="color:#B392F0"> className</span><span style="color:#F97583">=</span><span style="color:#9ECBFF">"flex flex-wrap gap-2 mt-2"</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">      {</span><span style="color:#6A737D">/* articles, views, reactions */</span><span style="color:#E1E4E8">}</span></span>
<span class="line"><span style="color:#E1E4E8">    &#x3C;/</span><span style="color:#85E89D">div</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">  &#x3C;/</span><span style="color:#85E89D">div</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">) </span><span style="color:#F97583">:</span><span style="color:#79B8FF"> null</span><span style="color:#E1E4E8">}</span></span></code></pre>
<p>Same pattern as the other stat blocks. Nothing clever.</p>
<p>Saved it. Reloaded. Articles showed up. Views showed <code>0</code>. Reactions were still missing. Of course they were.</p>
<hr>
<h2>Layer Three: Renamed Property</h2>
<p>The 1.2.0 release renamed <code>averageReactions</code> to <code>totalReactions</code> (separate fix, separate issue). But my component was still checking for <code>averageReactions</code>. TypeScript caught it immediately — red squiggle, clear error. Which was nice, but still. Three separate issues stacked on top of each other.</p>
<p>Fixed the property name in the component. Reactions showed up. <code>0</code> reactions, which is accurate since I'd just published my first post.</p>
<hr>
<h2>Layer Four: Views Need an API Key</h2>
<p>Views were showing <code>0</code>. The dev.to public API doesn't return <code>page_views_count</code>. The 1.2.0 fix added support for <code>DEV_TO_API_KEY</code> as an environment variable. If set, the library sends it as an <code>api-key</code> header, and dev.to returns view counts.</p>
<p>I have a dev.to API key. Just need to add it to <code>.env.local</code>.</p>
<p>That's a config step, not a code fix. But it's another thing I wouldn't have known about without actually trying to use the feature.</p>
<hr>
<h2>Tests Passed. Nothing Worked.</h2>
<p>Projex has tests. It has Zod schema validation. The types all check out. None of that caught any of this.</p>
<p>The dev.to integration passed validation because the schema was correct. The types were correct. The API call was going to the right endpoint. The code was doing exactly what it was written to do. It was just written against the wrong field names and the component had no way to display the results.</p>
<p>Four separate issues. All invisible until I actually used the thing.</p>
<p>I <a href="/blog/building-projex-retrospective">wrote a retrospective</a> about shipping this library. Published it on npm. And the first time I tried to use a feature I hadn't personally tested, it fell apart immediately.</p>
<p>That's dogfooding. Not the fun kind where your product works great and you feel smug. The kind where you realise your test coverage gave you false confidence and the only way to find real problems is to actually run the software yourself.</p>
<p>Projex 1.2.0 is out now. Dev.to integration works. I know because I'm using it.</p>
<hr>
<p>The four issues, if you're keeping score:</p>
<ol>
<li><code>page_views_count</code> not in public API response (needs auth)</li>
<li><code>positive_reactions_count</code> deprecated, should be <code>public_reactions_count</code></li>
<li>No render block in the component for dev.to stats</li>
<li><code>averageReactions</code> renamed to <code>totalReactions</code>, component not updated</li>
</ol>
<p>All found by trying to add one project to my own site.</p>]]></description>
            <content:encoded><![CDATA[<p>I wanted to add my dev.to profile to my projects page. Simple enough. Projex, my own library, has a <code>devto</code> type built in. I wrote the code. Added it to <code>projex.config.ts</code>. Checked the page.</p>
<p>No stats. Nothing. Just an empty project card staring back at me.</p>
<hr>
<h2>The Setup</h2>
<p>Projex supports nine project types. GitHub, npm, Product Hunt, dev.to, and a few others. Each type fetches data from a different API and normalises it into a standard format. Stats, links, descriptions.</p>
<p>I'd tested GitHub and hybrid (GitHub + npm) types extensively. Those are what I use for my other projects. The dev.to type had been sitting there since I shipped it. Never actually used it.</p>
<p>So I added this to my config:</p>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#E1E4E8">{</span></span>
<span class="line"><span style="color:#B392F0">  id</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">'devto'</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#B392F0">  name</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">'dev.to/manningworks'</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#B392F0">  type</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">'devto'</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#B392F0">  username</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">'manningworks'</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#B392F0">  status</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">'active'</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#6A737D">  // ... background, timeline, all the usual fields</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span></code></pre>
<p>Hit the projects page. The card rendered fine. Name, description, timeline, all good. But the stats section was completely empty. No articles. No views. No reactions.</p>
<p>My first thought was that the API call was failing silently. Maybe a network error I wasn't catching, or the username was wrong somehow. I checked the <code>npm run dev</code> console logs. Nothing. The fetch had run fine. One article, valid JSON. The data was there.</p>
<p>So the fetch was working. Something else was wrong.</p>
<hr>
<h2>Layer One: Wrong API Field Names</h2>
<p>I opened the API response side by side with the Projex source code. That's when I saw it.</p>
<p>Here's what Projex's <code>fetchDevToUser</code> was doing:</p>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#F97583">const</span><span style="color:#79B8FF"> totalViews</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> data.</span><span style="color:#B392F0">reduce</span><span style="color:#E1E4E8">(</span></span>
<span class="line"><span style="color:#E1E4E8">  (</span><span style="color:#FFAB70">sum</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">article</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> sum </span><span style="color:#F97583">+</span><span style="color:#E1E4E8"> article.page_views_count, </span><span style="color:#79B8FF">0</span></span>
<span class="line"><span style="color:#E1E4E8">);</span></span>
<span class="line"><span style="color:#F97583">const</span><span style="color:#79B8FF"> totalReactions</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> data.</span><span style="color:#B392F0">reduce</span><span style="color:#E1E4E8">(</span></span>
<span class="line"><span style="color:#E1E4E8">  (</span><span style="color:#FFAB70">sum</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">article</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> sum </span><span style="color:#F97583">+</span><span style="color:#E1E4E8"> article.positive_reactions_count, </span><span style="color:#79B8FF">0</span></span>
<span class="line"><span style="color:#E1E4E8">);</span></span></code></pre>
<p>And here's what the live API response actually contained: <code>public_reactions_count</code> and no <code>page_views_count</code> at all.</p>
<p>Two problems.</p>
<p><code>page_views_count</code> is only returned on authenticated requests. The public API doesn't include it. So it was <code>undefined</code>, and <code>0 + undefined = NaN</code>.</p>
<p><code>positive_reactions_count</code> was deprecated by dev.to in favour of <code>public_reactions_count</code>. The public response returns <code>public_reactions_count</code>, not <code>positive_reactions_count</code>.</p>
<p>So both values were <code>NaN</code>. The stats existed but were garbage. The component saw <code>NaN !== undefined</code> as true, tried to render them, and... nothing rendered properly.</p>
<p>Classic. I'd written code against API docs without actually checking what the live response looks like.</p>
<p>Raised an issue on my own repo. Fixed the field names. Shipped Projex 1.2.0.</p>
<p>Then I updated my site to 1.2.0, reloaded the page, and... still no stats. Different reason this time, but same empty card.</p>
<hr>
<h2>Layer Two: Missing Render Block</h2>
<p>But that wasn't the only problem. Even if the stats had been correct from the start, they still wouldn't have shown up.</p>
<p>I started digging through <code>ProjectDetail.tsx</code>, the component that renders project cards. It had render blocks for GitHub stats (stars, forks), npm stats (downloads, version), and Product Hunt stats (upvotes, comments).</p>
<p>No dev.to block. At all.</p>
<p>The component knew about three stat types. Projex supported nine project types. I'd never added the render logic for the newer ones because I'd never needed it. The data was there, normalised and everything. The component just had no idea what to do with it.</p>
<p>So I added a dev.to section:</p>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#E1E4E8">{project.stats.articleCount </span><span style="color:#F97583">!==</span><span style="color:#79B8FF"> undefined</span><span style="color:#F97583"> ||</span><span style="color:#6A737D"> /* ... */</span><span style="color:#F97583"> ?</span><span style="color:#E1E4E8"> (</span></span>
<span class="line"><span style="color:#E1E4E8">  &#x3C;</span><span style="color:#85E89D">div</span><span style="color:#B392F0"> className</span><span style="color:#F97583">=</span><span style="color:#9ECBFF">"mb-4"</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">    &#x3C;</span><span style="color:#85E89D">span</span><span style="color:#B392F0"> className</span><span style="color:#F97583">=</span><span style="color:#9ECBFF">"text-xs text-terminal-dim"</span><span style="color:#E1E4E8">>dev.to: &#x3C;/</span><span style="color:#85E89D">span</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">    &#x3C;</span><span style="color:#85E89D">div</span><span style="color:#B392F0"> className</span><span style="color:#F97583">=</span><span style="color:#9ECBFF">"flex flex-wrap gap-2 mt-2"</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">      {</span><span style="color:#6A737D">/* articles, views, reactions */</span><span style="color:#E1E4E8">}</span></span>
<span class="line"><span style="color:#E1E4E8">    &#x3C;/</span><span style="color:#85E89D">div</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">  &#x3C;/</span><span style="color:#85E89D">div</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">) </span><span style="color:#F97583">:</span><span style="color:#79B8FF"> null</span><span style="color:#E1E4E8">}</span></span></code></pre>
<p>Same pattern as the other stat blocks. Nothing clever.</p>
<p>Saved it. Reloaded. Articles showed up. Views showed <code>0</code>. Reactions were still missing. Of course they were.</p>
<hr>
<h2>Layer Three: Renamed Property</h2>
<p>The 1.2.0 release renamed <code>averageReactions</code> to <code>totalReactions</code> (separate fix, separate issue). But my component was still checking for <code>averageReactions</code>. TypeScript caught it immediately — red squiggle, clear error. Which was nice, but still. Three separate issues stacked on top of each other.</p>
<p>Fixed the property name in the component. Reactions showed up. <code>0</code> reactions, which is accurate since I'd just published my first post.</p>
<hr>
<h2>Layer Four: Views Need an API Key</h2>
<p>Views were showing <code>0</code>. The dev.to public API doesn't return <code>page_views_count</code>. The 1.2.0 fix added support for <code>DEV_TO_API_KEY</code> as an environment variable. If set, the library sends it as an <code>api-key</code> header, and dev.to returns view counts.</p>
<p>I have a dev.to API key. Just need to add it to <code>.env.local</code>.</p>
<p>That's a config step, not a code fix. But it's another thing I wouldn't have known about without actually trying to use the feature.</p>
<hr>
<h2>Tests Passed. Nothing Worked.</h2>
<p>Projex has tests. It has Zod schema validation. The types all check out. None of that caught any of this.</p>
<p>The dev.to integration passed validation because the schema was correct. The types were correct. The API call was going to the right endpoint. The code was doing exactly what it was written to do. It was just written against the wrong field names and the component had no way to display the results.</p>
<p>Four separate issues. All invisible until I actually used the thing.</p>
<p>I <a href="/blog/building-projex-retrospective">wrote a retrospective</a> about shipping this library. Published it on npm. And the first time I tried to use a feature I hadn't personally tested, it fell apart immediately.</p>
<p>That's dogfooding. Not the fun kind where your product works great and you feel smug. The kind where you realise your test coverage gave you false confidence and the only way to find real problems is to actually run the software yourself.</p>
<p>Projex 1.2.0 is out now. Dev.to integration works. I know because I'm using it.</p>
<hr>
<p>The four issues, if you're keeping score:</p>
<ol>
<li><code>page_views_count</code> not in public API response (needs auth)</li>
<li><code>positive_reactions_count</code> deprecated, should be <code>public_reactions_count</code></li>
<li>No render block in the component for dev.to stats</li>
<li><code>averageReactions</code> renamed to <code>totalReactions</code>, component not updated</li>
</ol>
<p>All found by trying to add one project to my own site.</p>]]></content:encoded>
            <category>projex</category>
        </item>
        <item>
            <title><![CDATA[I Built Projex and Never Wrote a Word About It]]></title>
            <link>https://lukemanning.ie/blog/building-projex-retrospective</link>
            <guid isPermaLink="true">https://lukemanning.ie/blog/building-projex-retrospective</guid>
            <pubDate>Fri, 10 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[<p>I shipped an entire open source project and never documented a single day of building it.</p>
<p>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.</p>
<p>This is the opposite of what I said I wanted to do.</p>
<hr>
<h2>How It Started</h2>
<p>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?</p>
<p>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?</p>
<p>That gap felt like an opportunity.</p>
<p>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.</p>
<p>So I started building.</p>
<hr>
<h2>The First Two Days Were a Blur</h2>
<p>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.</p>
<p>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.</p>
<p>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.</p>
<p>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.</p>
<hr>
<h2>The CLI Merge I Should Have Seen Coming</h2>
<p>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.</p>
<p>A few days later I merged them back into a single package.</p>
<p>The git log tells the story plainly: <code>refactor: merge CLI and core into single @reallukemanning/folio package</code>. No drama. No long agonising. Just a realisation that the separation was premature and added complexity I didn't need.</p>
<p>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.</p>
<hr>
<h2>VitePress on Vercel: The Silent 404s</h2>
<p>The docs site was its own adventure.</p>
<p>I built the documentation with VitePress and deployed it to Vercel. The build succeeded. Everything looked green. But the site returned 404s.</p>
<p>Not helpful error messages. Not warnings during build. Just... 404s. Silent, unhelpful 404s.</p>
<p>The git log from February 22nd tells the whole debugging arc:</p>
<pre><code>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
</code></pre>
<p>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.</p>
<p>I tried file copying. Then Vercel rewrites. Then removing rewrites. Then different directory structures. What finally worked was setting <code>srcDir</code> in the VitePress config to get the build output in the right place.</p>
<p>Obvious in hindsight. Everything is.</p>
<hr>
<h2>The OIDC Publishing Odyssey</h2>
<p>This is the one that nearly broke me.</p>
<p>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.</p>
<p>The commit log from that stretch is almost comedy:</p>
<pre><code>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
</code></pre>
<p>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.</p>
<p>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.</p>
<p>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 <code>--provenance</code> flag. But getting there required trying basically every permutation.</p>
<p>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.</p>
<hr>
<h2>The Rename: Folio to Projex</h2>
<p>The project was originally called Folio. Package name <code>@reallukemanning/folio</code>. Everything was Folio for the first two weeks.</p>
<p>Then on March 8th, I renamed the whole thing. <code>chore: rename Folio to Projex and repackage to @manningworks/projex</code>.</p>
<p>A rename sounds simple. It wasn't. Every data attribute in every component changed from <code>data-folio-*</code> to <code>data-projex-*</code>. Every import path. Every reference in docs. The pnpm lockfile. The GitHub workflows. The CLI commands. The README.</p>
<pre><code>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
</code></pre>
<p>Four commits just for the rename, and that's not counting the docs updates.</p>
<p>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.</p>
<p>The funny part: I didn't catch all the references. On April 2nd, nearly a month later, I was still fixing leftover Folio references: <code>fix: correct Folio→Projex branding, unscoped npx commands, and CSS variable names (v1.1.4)</code>.</p>
<hr>
<h2>What I Actually Built</h2>
<p>209 commits later, Projex is:</p>
<ul>
<li>A shadcn-style component library with compound components (<code>ProjectCard.Header</code>, <code>ProjectCard.Stats</code>, etc.)</li>
<li>A CLI that auto-discovers your GitHub repos with <code>npx @manningworks/projex init --github</code></li>
<li>Build-time data fetching from GitHub, npm, and Product Hunt (no runtime API calls)</li>
<li>Zero CSS shipped by default, style everything with data attributes</li>
<li>Multiple project types: GitHub, npm, Product Hunt, YouTube, Gumroad, manual, hybrid</li>
<li>A full VitePress docs site with interactive examples</li>
<li>OIDC trusted publishing with automated GitHub Releases</li>
<li>Vitest tests, ESLint, CI/CD</li>
</ul>
<p>It's used in production on my own site. It's on npm. The docs are live. It's real.</p>
<hr>
<h2>Why I Never Wrote About It</h2>
<p>I don't have a great answer for this.</p>
<p>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."</p>
<p>Spoiler: it's never done.</p>
<p>Partly it was the thing I wrote about in <a href="/blog/posting-into-the-void">posting into the void</a>. 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.</p>
<p>The irony of saying I want to build in public and then building an entire project in private isn't lost on me.</p>
<hr>
<h2>What I'd Do Differently</h2>
<p>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.</p>
<p>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.</p>
<p>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.</p>
<p>And I'd have picked the right name from the start.</p>
<hr>
<h2>Where Projex Is Now</h2>
<p>Projex is at v1.1.4. It's on npm as <code>@manningworks/projex</code>. The docs are at <a href="https://projex.manningworks.dev">projex.manningworks.dev</a>. The repo is public on GitHub.</p>
<p>It hasn't set the world on fire. But it's real, it works, and I use it every day on my own site.</p>
<p>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.</p>
<p>209 commits and not a single blog post. That's the antithesis of what I said I wanted to embody.</p>
<p>Better late than never, I guess.</p>]]></description>
            <content:encoded><![CDATA[<p>I shipped an entire open source project and never documented a single day of building it.</p>
<p>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.</p>
<p>This is the opposite of what I said I wanted to do.</p>
<hr>
<h2>How It Started</h2>
<p>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?</p>
<p>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?</p>
<p>That gap felt like an opportunity.</p>
<p>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.</p>
<p>So I started building.</p>
<hr>
<h2>The First Two Days Were a Blur</h2>
<p>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.</p>
<p>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.</p>
<p>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.</p>
<p>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.</p>
<hr>
<h2>The CLI Merge I Should Have Seen Coming</h2>
<p>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.</p>
<p>A few days later I merged them back into a single package.</p>
<p>The git log tells the story plainly: <code>refactor: merge CLI and core into single @reallukemanning/folio package</code>. No drama. No long agonising. Just a realisation that the separation was premature and added complexity I didn't need.</p>
<p>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.</p>
<hr>
<h2>VitePress on Vercel: The Silent 404s</h2>
<p>The docs site was its own adventure.</p>
<p>I built the documentation with VitePress and deployed it to Vercel. The build succeeded. Everything looked green. But the site returned 404s.</p>
<p>Not helpful error messages. Not warnings during build. Just... 404s. Silent, unhelpful 404s.</p>
<p>The git log from February 22nd tells the whole debugging arc:</p>
<pre><code>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
</code></pre>
<p>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.</p>
<p>I tried file copying. Then Vercel rewrites. Then removing rewrites. Then different directory structures. What finally worked was setting <code>srcDir</code> in the VitePress config to get the build output in the right place.</p>
<p>Obvious in hindsight. Everything is.</p>
<hr>
<h2>The OIDC Publishing Odyssey</h2>
<p>This is the one that nearly broke me.</p>
<p>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.</p>
<p>The commit log from that stretch is almost comedy:</p>
<pre><code>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
</code></pre>
<p>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.</p>
<p>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.</p>
<p>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 <code>--provenance</code> flag. But getting there required trying basically every permutation.</p>
<p>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.</p>
<hr>
<h2>The Rename: Folio to Projex</h2>
<p>The project was originally called Folio. Package name <code>@reallukemanning/folio</code>. Everything was Folio for the first two weeks.</p>
<p>Then on March 8th, I renamed the whole thing. <code>chore: rename Folio to Projex and repackage to @manningworks/projex</code>.</p>
<p>A rename sounds simple. It wasn't. Every data attribute in every component changed from <code>data-folio-*</code> to <code>data-projex-*</code>. Every import path. Every reference in docs. The pnpm lockfile. The GitHub workflows. The CLI commands. The README.</p>
<pre><code>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
</code></pre>
<p>Four commits just for the rename, and that's not counting the docs updates.</p>
<p>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.</p>
<p>The funny part: I didn't catch all the references. On April 2nd, nearly a month later, I was still fixing leftover Folio references: <code>fix: correct Folio→Projex branding, unscoped npx commands, and CSS variable names (v1.1.4)</code>.</p>
<hr>
<h2>What I Actually Built</h2>
<p>209 commits later, Projex is:</p>
<ul>
<li>A shadcn-style component library with compound components (<code>ProjectCard.Header</code>, <code>ProjectCard.Stats</code>, etc.)</li>
<li>A CLI that auto-discovers your GitHub repos with <code>npx @manningworks/projex init --github</code></li>
<li>Build-time data fetching from GitHub, npm, and Product Hunt (no runtime API calls)</li>
<li>Zero CSS shipped by default, style everything with data attributes</li>
<li>Multiple project types: GitHub, npm, Product Hunt, YouTube, Gumroad, manual, hybrid</li>
<li>A full VitePress docs site with interactive examples</li>
<li>OIDC trusted publishing with automated GitHub Releases</li>
<li>Vitest tests, ESLint, CI/CD</li>
</ul>
<p>It's used in production on my own site. It's on npm. The docs are live. It's real.</p>
<hr>
<h2>Why I Never Wrote About It</h2>
<p>I don't have a great answer for this.</p>
<p>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."</p>
<p>Spoiler: it's never done.</p>
<p>Partly it was the thing I wrote about in <a href="/blog/posting-into-the-void">posting into the void</a>. 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.</p>
<p>The irony of saying I want to build in public and then building an entire project in private isn't lost on me.</p>
<hr>
<h2>What I'd Do Differently</h2>
<p>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.</p>
<p>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.</p>
<p>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.</p>
<p>And I'd have picked the right name from the start.</p>
<hr>
<h2>Where Projex Is Now</h2>
<p>Projex is at v1.1.4. It's on npm as <code>@manningworks/projex</code>. The docs are at <a href="https://projex.manningworks.dev">projex.manningworks.dev</a>. The repo is public on GitHub.</p>
<p>It hasn't set the world on fire. But it's real, it works, and I use it every day on my own site.</p>
<p>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.</p>
<p>209 commits and not a single blog post. That's the antithesis of what I said I wanted to embody.</p>
<p>Better late than never, I guess.</p>]]></content:encoded>
            <category>projex</category>
        </item>
        <item>
            <title><![CDATA[Posting Into The Void]]></title>
            <link>https://lukemanning.ie/blog/posting-into-the-void</link>
            <guid isPermaLink="true">https://lukemanning.ie/blog/posting-into-the-void</guid>
            <pubDate>Sat, 04 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[<p>I spent the last few weeks and months working on my personal site. I shipped Projex — a shadcn-style component library for developer portfolio pages — posted about it on X to silence, posted about it on Reddit to near-silence, and sat with that feeling for a while today.</p>
<p>It doesn't feel great.</p>
<hr>
<h2>The Specific Kind of Demoralizing</h2>
<p>It's not burnout exactly. I'm not tired of building. I enjoy it. This has become a hobby, and I mean that genuinely, not as a cope, but because the time I spend building doesn't feel wasted even when nothing comes of it.</p>
<p>What stings is the silence after you share something.</p>
<p>You build a thing. You think it's useful. You give it away for free. You're not asking for money, you're not running ads, you're not trying to acquire users in some predatory growth-hack way. You just want to know someone tried it. Maybe filed an issue. Maybe said "this is neat." That's it.</p>
<p>Instead: nothing.</p>
<hr>
<h2>Reddit Is Broken For This</h2>
<p>Reddit right now is flooded with AI-generated spam and people shilling SaaS products that barely work. I hate that. And I hate that when I go to post about something I genuinely built and care about, I feel like one of them.</p>
<p>That's the uncomfortable part. I'm not shilling. Projex is free. I'm not asking for anything. But the act of posting about your own work in that environment feels gross now, because the context is so poisoned.</p>
<p>I don't want to learn marketing. I don't want to become someone who optimises posts for engagement. My time is genuinely better spent getting better at building. That's where I am right now.</p>
<hr>
<h2>X Isn't Working Either</h2>
<p>I don't like X. Posting there feels like talking into a void. Maybe that changes with audience size (probably does), but right now it's not where I want to spend energy.</p>
<p>I was hoping Projex might get a small handful of users. Not thousands. Not virality. Just a handful of developers who found it useful and maybe said so. That hasn't happened yet, and that's what knocked me sideways a bit today.</p>
<hr>
<h2>DevBreakdown and The Fuzzy Middle</h2>
<p>I've also been stalling on DevBreakdown — my AI model and subscription comparison site. Partly because the affiliate angle I originally had in mind is basically a non-starter now. Partly because the space moves so fast I can hardly keep up with it myself, let alone maintain a site tracking it.</p>
<p>And then there's the motivation problem: if I'm earning nothing from it, and it's hard to keep current, and I'm already feeling the silence from Projex... why start another thing that shouts into the same void? <a href="/blog/the-entrepreneurial-conundrum">The entrepreneurial conundrum</a> is familiar territory here — the gap between having ideas and actually executing on them.</p>
<p>I don't have a clean answer to that. What I do know is that the clarity problem is real. I know what I want DB to be, but the steps between "start" and "ship something useful" are fuzzy. That fuzziness is what kills momentum before you even begin.</p>
<hr>
<h2>Why I'm Still Going</h2>
<p>I kept coming back to something today. I've shipped more in the last few months than most people who <em>talk</em> about building ever do.</p>
<p>That's not a flex. It's just what I noticed. And it made me realise: component libraries don't go viral. They get quietly discovered by developers who need them. npm downloads tick up without fanfare. Someone uses your thing without ever telling you.</p>
<p>Maybe that's already happening with Projex. I genuinely don't know.</p>
<p>I know this is a long game. Knowing that doesn't make today any less shit. But my real goal was never a Reddit post going off. It's building a portfolio of work that compounds, covering my AI subs with something I made, and eventually having enough momentum that the audience finds me rather than me chasing it.</p>
<p>€100/month net neutral would genuinely feel like a win right now. That's not a low bar out of pessimism. It's a concrete, honest first milestone.</p>
<hr>
<p>Today I'm going back to the Straico API proxy I half-built a while ago. It's broken in ways I want to fix. It's contained. It's mine. And finishing it will feel like a win without needing anyone to validate it.</p>
<p>That's enough for today.</p>
<p>There's something underneath all of this I keep coming back to: <a href="/blog/fluorescent-office-essay">the essay making the rounds about not following your passion</a> makes a case for accepting where you are. But I think there's a version of that argument that quietly recommends giving up on building anything outside the office. That's the part I can't get on board with.</p>]]></description>
            <content:encoded><![CDATA[<p>I spent the last few weeks and months working on my personal site. I shipped Projex — a shadcn-style component library for developer portfolio pages — posted about it on X to silence, posted about it on Reddit to near-silence, and sat with that feeling for a while today.</p>
<p>It doesn't feel great.</p>
<hr>
<h2>The Specific Kind of Demoralizing</h2>
<p>It's not burnout exactly. I'm not tired of building. I enjoy it. This has become a hobby, and I mean that genuinely, not as a cope, but because the time I spend building doesn't feel wasted even when nothing comes of it.</p>
<p>What stings is the silence after you share something.</p>
<p>You build a thing. You think it's useful. You give it away for free. You're not asking for money, you're not running ads, you're not trying to acquire users in some predatory growth-hack way. You just want to know someone tried it. Maybe filed an issue. Maybe said "this is neat." That's it.</p>
<p>Instead: nothing.</p>
<hr>
<h2>Reddit Is Broken For This</h2>
<p>Reddit right now is flooded with AI-generated spam and people shilling SaaS products that barely work. I hate that. And I hate that when I go to post about something I genuinely built and care about, I feel like one of them.</p>
<p>That's the uncomfortable part. I'm not shilling. Projex is free. I'm not asking for anything. But the act of posting about your own work in that environment feels gross now, because the context is so poisoned.</p>
<p>I don't want to learn marketing. I don't want to become someone who optimises posts for engagement. My time is genuinely better spent getting better at building. That's where I am right now.</p>
<hr>
<h2>X Isn't Working Either</h2>
<p>I don't like X. Posting there feels like talking into a void. Maybe that changes with audience size (probably does), but right now it's not where I want to spend energy.</p>
<p>I was hoping Projex might get a small handful of users. Not thousands. Not virality. Just a handful of developers who found it useful and maybe said so. That hasn't happened yet, and that's what knocked me sideways a bit today.</p>
<hr>
<h2>DevBreakdown and The Fuzzy Middle</h2>
<p>I've also been stalling on DevBreakdown — my AI model and subscription comparison site. Partly because the affiliate angle I originally had in mind is basically a non-starter now. Partly because the space moves so fast I can hardly keep up with it myself, let alone maintain a site tracking it.</p>
<p>And then there's the motivation problem: if I'm earning nothing from it, and it's hard to keep current, and I'm already feeling the silence from Projex... why start another thing that shouts into the same void? <a href="/blog/the-entrepreneurial-conundrum">The entrepreneurial conundrum</a> is familiar territory here — the gap between having ideas and actually executing on them.</p>
<p>I don't have a clean answer to that. What I do know is that the clarity problem is real. I know what I want DB to be, but the steps between "start" and "ship something useful" are fuzzy. That fuzziness is what kills momentum before you even begin.</p>
<hr>
<h2>Why I'm Still Going</h2>
<p>I kept coming back to something today. I've shipped more in the last few months than most people who <em>talk</em> about building ever do.</p>
<p>That's not a flex. It's just what I noticed. And it made me realise: component libraries don't go viral. They get quietly discovered by developers who need them. npm downloads tick up without fanfare. Someone uses your thing without ever telling you.</p>
<p>Maybe that's already happening with Projex. I genuinely don't know.</p>
<p>I know this is a long game. Knowing that doesn't make today any less shit. But my real goal was never a Reddit post going off. It's building a portfolio of work that compounds, covering my AI subs with something I made, and eventually having enough momentum that the audience finds me rather than me chasing it.</p>
<p>€100/month net neutral would genuinely feel like a win right now. That's not a low bar out of pessimism. It's a concrete, honest first milestone.</p>
<hr>
<p>Today I'm going back to the Straico API proxy I half-built a while ago. It's broken in ways I want to fix. It's contained. It's mine. And finishing it will feel like a win without needing anyone to validate it.</p>
<p>That's enough for today.</p>
<p>There's something underneath all of this I keep coming back to: <a href="/blog/fluorescent-office-essay">the essay making the rounds about not following your passion</a> makes a case for accepting where you are. But I think there's a version of that argument that quietly recommends giving up on building anything outside the office. That's the part I can't get on board with.</p>]]></content:encoded>
            <category>projex</category>
            <category>reflections</category>
        </item>
    </channel>
</rss>