<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Warya Wayne</title>
    <link>https://warya.2to6x.com/blog</link>
    <description>Technical write-ups on open-source contributions and the problems behind them.</description>
    <language>en</language>
    <managingEditor>waryawayne1+portfolio@gmail.com (Warya Wayne)</managingEditor>
    <lastBuildDate>Wed, 19 Aug 2026 00:00:00 GMT</lastBuildDate>
    <atom:link href="https://warya.2to6x.com/rss.xml" rel="self" type="application/rss+xml"/>
    <item>
      <title>Registering the Cloudflare Vite plugin twice breaks prerendering under Alchemy</title>
      <link>https://warya.2to6x.com/blog/alchemy-owns-the-cloudflare-vite-plugin</link>
      <guid isPermaLink="true">https://warya.2to6x.com/blog/alchemy-owns-the-cloudflare-vite-plugin</guid>
      <pubDate>Wed, 19 Aug 2026 00:00:00 GMT</pubDate>
      <dc:creator>Warya Wayne</dc:creator>
      <description>TanStack Start on Cloudflare documents adding the Cloudflare Vite plugin. Alchemy's Website.Vite adds it as well. With both registered, the build fails during prerender.</description>
      <content:encoded><![CDATA[<p>A TanStack Start app scaffolded for Cloudflare gets the Cloudflare Vite plugin in <code>vite.config.ts</code>:</p>
<pre><code class="hljs language-ts"><span class="hljs-keyword">import</span> { cloudflare } <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;@cloudflare/vite-plugin&#x27;</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-title function_">defineConfig</span>({
  <span class="hljs-attr">plugins</span>: [<span class="hljs-title function_">cloudflare</span>({ <span class="hljs-attr">viteEnvironment</span>: { <span class="hljs-attr">name</span>: <span class="hljs-string">&#x27;ssr&#x27;</span> } }), <span class="hljs-title function_">tanstackStart</span>(), <span class="hljs-title function_">viteReact</span>()],
})</code></pre><p>That matches the documentation. When the same app is deployed with Alchemy, the build fails during prerendering.</p>
<h2>The failure</h2>
<pre><code class="hljs language-plaintext">[prerender] Prerendering pages...
[prerender] Crawling: /
Error: Server is not running.
    at disposeMiniflare (@cloudflare/vite-plugin/dist/index.mjs)
    at prerender (@tanstack/start-plugin-core/dist/esm/prerender.js)</code></pre><p><code>vite build</code> locally succeeds. Only the deploy fails, and it fails inside prerendering.</p>
<h2>Why</h2>
<p>Alchemy&#39;s <code>Website.Vite</code> resource does not shell out to <code>vite build</code>. It builds in process, and it constructs the builder with its own plugin list:</p>
<pre><code class="hljs language-ts"><span class="hljs-keyword">const</span> builder = <span class="hljs-keyword">await</span> vite.<span class="hljs-title function_">createBuilder</span>({
  <span class="hljs-attr">plugins</span>: [<span class="hljs-title function_">cloudflare</span>(pluginOptions), outputPlugin.<span class="hljs-property">plugin</span>],
})</code></pre><p>Alchemy adds <code>cloudflare()</code> because it needs control over the Worker environment it is about to deploy. When the app config registers it too, the build runs with two instances of the Cloudflare plugin, and therefore two miniflare servers.</p>
<p>TanStack Start&#39;s prerenderer boots a server and requests every page through it. One of the two plugin instances finishes its own lifecycle and disposes its miniflare while the prerenderer is still rendering through the other. The server the prerenderer was using goes away mid crawl, which produces <code>Server is not running</code>.</p>
<h2>The fix</h2>
<p>Remove the Cloudflare plugin from the app config and let Alchemy supply it:</p>
<pre><code class="hljs language-ts"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-title function_">defineConfig</span>({
  <span class="hljs-attr">plugins</span>: [<span class="hljs-title function_">tanstackStart</span>({ <span class="hljs-attr">prerender</span>: { <span class="hljs-attr">enabled</span>: <span class="hljs-literal">true</span> } }), <span class="hljs-title function_">viteReact</span>()],
})</code></pre><p>Two consequences follow from that.</p>
<p><strong>Local <code>vite build</code> changes shape.</strong> Without the plugin, prerendering runs in plain Node instead of workerd. Anything imported during prerender that expects the Worker environment, such as bindings or <code>.dev.vars</code> secrets, now fails locally while continuing to work under Alchemy. In my case better-auth started throwing on a missing <code>BETTER_AUTH_SECRET</code>, which miniflare had been supplying from <code>.dev.vars</code>.</p>
<p><strong>npm build scripts do not run on deploy.</strong> Alchemy calls Vite&#39;s API directly, so <code>&quot;build&quot;: &quot;node scripts/generate-feed.mjs &amp;&amp; vite build&quot;</code> does not generate the feed when Alchemy builds. Work that has to happen on every build belongs in <code>vite.config.ts</code> as a plugin rather than in <code>package.json</code>.</p>
<h2>Which plugins belong where</h2>
<p>When a deploy tool builds the app, it owns the build pipeline. Plugins that shape the output target, the Cloudflare plugin here, belong to whoever is doing the deploying. Plugins that shape the application stay in the app config.</p>
<p>Both sets of docs are accurate on their own. TanStack documents the plugin because the target needs it, and Alchemy does not mention removing it because it provides one itself. The combination is where the ownership question comes up, and the first signal is the disposal error during prerender.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Search in Zed's markdown preview matched text that never rendered</title>
      <link>https://warya.2to6x.com/blog/markdown-preview-phantom-search-matches</link>
      <guid isPermaLink="true">https://warya.2to6x.com/blog/markdown-preview-phantom-search-matches</guid>
      <pubDate>Wed, 19 Aug 2026 00:00:00 GMT</pubDate>
      <dc:creator>Warya Wayne</dc:creator>
      <description>The match count included link destinations, image URLs, and reference definitions. This change filters matches against source ranges that produce no on-screen text.</description>
      <content:encoded><![CDATA[<p>Searching a README in Zed&#39;s markdown preview could report more matches than were visible on screen. Pressing enter to jump to one of the extra matches had no visible effect.</p>
<h2>Why it happens</h2>
<p>The preview renders markdown, but search ran against the raw markdown source. Syntax that never becomes visible text was still searchable:</p>
<ul>
<li>Link destinations. In <code>[label](url)</code>, only <code>label</code> renders, but <code>url</code> was still matched</li>
<li>Images, including both alt text and destination</li>
<li>Link reference definitions, which <code>pulldown-cmark</code> consumes without emitting any event</li>
</ul>
<p>Badge heavy READMEs show it most clearly, since every badge URL contributes words like <code>github</code>, <code>build</code>, <code>main</code>, and <code>svg</code> that never reach the screen.</p>
<h2>The fix</h2>
<p>Track which source ranges produce no on-screen text, then filter matches against them. The ranges are returned sorted and disjoint, so the preview can binary search rather than scan.</p>
<p>A second issue sat underneath. Search results derive from the parsed markdown, and the parse happens in the background, so results could be computed against a stale parse. <code>MatchesInvalidated</code> now fires once the parse lands.</p>
<h2>Testing</h2>
<p>Eight new tests cover link destinations, reference definitions, standalone images, linked images, images between link text, parse in flight, and match order independence.</p>
<p>Merged as <a href="https://github.com/zed-industries/zed/pull/62280">zed-industries/zed#62280</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>prerender.filter does not exclude pages from the TanStack Start sitemap</title>
      <link>https://warya.2to6x.com/blog/prerender-filter-does-not-gate-the-sitemap</link>
      <guid isPermaLink="true">https://warya.2to6x.com/blog/prerender-filter-does-not-gate-the-sitemap</guid>
      <pubDate>Wed, 19 Aug 2026 00:00:00 GMT</pubDate>
      <dc:creator>Warya Wayne</dc:creator>
      <description>Prerender filtering and sitemap exclusion are separate gates in TanStack Start. A page skipped by the renderer still appears in sitemap.xml.</description>
      <content:encoded><![CDATA[<p>TanStack Start can prerender pages and generate a sitemap. Both are configured on the same plugin, a few lines apart, and the filter on one does not apply to the other.</p>
<pre><code class="hljs language-ts"><span class="hljs-title function_">tanstackStart</span>({
  <span class="hljs-attr">prerender</span>: {
    <span class="hljs-attr">enabled</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">filter</span>: <span class="hljs-function">(<span class="hljs-params">page</span>) =&gt;</span> !page.<span class="hljs-property">path</span>.<span class="hljs-title function_">startsWith</span>(<span class="hljs-string">&#x27;/api&#x27;</span>),
  },
  <span class="hljs-attr">sitemap</span>: { <span class="hljs-attr">enabled</span>: <span class="hljs-literal">true</span>, <span class="hljs-attr">host</span>: <span class="hljs-string">&#x27;https://example.com&#x27;</span> },
})</code></pre><p>Every page filtered out of prerendering is still in <code>sitemap.xml</code>.</p>
<h2>Where each list comes from</h2>
<p>The sitemap builder does not look at prerender results. It builds from the discovered <code>pages</code> list, and the only thing that removes an entry is a per-page flag:</p>
<pre><code class="hljs language-js">pages.<span class="hljs-title function_">filter</span>(<span class="hljs-function">(<span class="hljs-params">page</span>) =&gt;</span> page.<span class="hljs-property">sitemap</span>?.<span class="hljs-property">exclude</span> !== <span class="hljs-literal">true</span>)</code></pre><p><code>prerender.filter</code> and <code>sitemap.exclude</code> are two independent gates, so a page can be skipped by the renderer and still advertised to crawlers as something that exists.</p>
<h2>Two cases from this site</h2>
<p><strong>A duplicate URL.</strong> With <code>autoStaticPathsDiscovery</code> on, route based discovery contributed <code>/blog/</code> while link crawling contributed <code>/blog</code>. That is one document with two sitemap entries, one of which 307 redirects to the other. Filtering the trailing slash form out of prerendering changed nothing in the sitemap. Turning auto discovery off left link crawling to produce one canonical form per page.</p>
<p><strong>The RSS feed.</strong> Adding <code>&lt;a href=&quot;/rss.xml&quot;&gt;rss&lt;/a&gt;</code> to the footer meant the crawler discovered the feed as a page. Excluding it from prerendering was not enough, since <code>/rss.xml</code> still appeared in the sitemap as an indexable URL. That case needs the flag:</p>
<pre><code class="hljs language-ts"><span class="hljs-attr">pages</span>: [
  { <span class="hljs-attr">path</span>: <span class="hljs-string">&#x27;/&#x27;</span> },
  { <span class="hljs-attr">path</span>: <span class="hljs-string">&#x27;/rss.xml&#x27;</span>, <span class="hljs-attr">sitemap</span>: { <span class="hljs-attr">exclude</span>: <span class="hljs-literal">true</span> } },
],</code></pre><h2>Declaring pages replaces the discovery seed</h2>
<p>Declaring <code>pages</code> replaces the discovery seed rather than adding to it. My first attempt listed only <code>/rss.xml</code>, and the build prerendered zero pages and wrote an empty sitemap. <code>/</code> has to be listed explicitly so link crawling still has somewhere to start.</p>
<h2>Checking the generated file</h2>
<p>The build log said <code>Prerendered 11 pages</code> in every one of these states, and that was accurate. The difference was in a file the log does not mention, so it is worth reading the generated XML directly:</p>
<pre><code class="hljs language-plaintext">grep -o &#x27;&lt;loc&gt;[^&lt;]*&lt;/loc&gt;&#x27; dist/client/sitemap.xml</code></pre><p>Every URL in there should return 200 rather than a redirect, and should be a page you want indexed.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Sitemap discovery happens at the host root, not at a Worker's base path</title>
      <link>https://warya.2to6x.com/blog/sitemap-worker-route-subdomain</link>
      <guid isPermaLink="true">https://warya.2to6x.com/blog/sitemap-worker-route-subdomain</guid>
      <pubDate>Wed, 12 Aug 2026 00:00:00 GMT</pubDate>
      <dc:creator>Warya Wayne</dc:creator>
      <description>A site deployed on a Cloudflare Worker route under a path had most of its pages missing from Google. The sitemap was valid, but nothing at the domain root pointed to it.</description>
      <content:encoded><![CDATA[<p>We had a site deployed on a Cloudflare Worker route, mounted under a path on an existing domain, something like <code>example.com/google/*</code>. Most of its pages never showed up in Google.</p>
<h2>The mechanism</h2>
<p>Sitemap discovery happens at the host root. Google fetches <code>example.com/robots.txt</code> and reads the <code>Sitemap:</code> directive there.</p>
<p><code>example.com/robots.txt</code> is not served by the Worker. It is served by whatever owns the root of the domain, in our case the main marketing site. The framework emits its sitemap relative to its own base, so it lands at <code>example.com/google/sitemap.xml</code>, and nothing at the root points to it. The sitemap exists and is valid, and no crawler finds it.</p>
<h2>What the framework can and cannot do</h2>
<p>An app mounted under a path does not own the domain root. It can generate a correct sitemap and still have no way to advertise it, because the file that does the advertising belongs to a different app.</p>
<h2>Options</h2>
<p>Moving to a subdomain resolves it structurally. On <code>warya.example.com</code> the app owns its own root, so <code>robots.txt</code> and <code>sitemap.xml</code> sit exactly where crawlers look for them.</p>
<p>It is also fixable in place. A <code>Sitemap:</code> line in the root <code>robots.txt</code> can point at a sub-path sitemap, and Search Console accepts a manual submission. Both require editing the root, which is the part outside the app&#39;s control.</p>
<h2>Checking it</h2>
<p>Fetch <code>yourdomain.com/robots.txt</code> and check whether it names a sitemap the app actually serves.</p>
]]></content:encoded>
    </item>
    <item>
      <title>A one-line fix for a redirect loop between Vite, TanStack Start, and Cloudflare</title>
      <link>https://warya.2to6x.com/blog/vite-base-path-redirect-loop</link>
      <guid isPermaLink="true">https://warya.2to6x.com/blog/vite-base-path-redirect-loop</guid>
      <pubDate>Mon, 27 Jul 2026 00:00:00 GMT</pubDate>
      <dc:creator>Warya Wayne</dc:creator>
      <description>A TanStack Start app under a non-root base in alchemy dev redirected to itself. The dev plugin was reading req.url instead of req.originalUrl.</description>
      <content:encoded><![CDATA[<p>A Vite app deployed under a non-root <code>base</code>, say <code>base: &quot;/example/&quot;</code>, running under <code>alchemy dev</code>, turned every request into a redirect loop until the browser gave up.</p>
<p>The fix was one line. Finding it meant following the request through three tools, each of which was doing what it was designed to do.</p>
<h2>The loop</h2>
<ol>
<li>The browser requests <code>/example/pricing</code></li>
<li>Vite&#39;s base middleware rewrites <code>req.url</code> to <code>/pricing</code>, by design, so internal middleware sees paths without the base</li>
<li>The Cloudflare dev plugin forwards <code>/pricing</code> to the Worker</li>
<li>TanStack Start sees a URL missing its base and redirects to <code>/example/pricing</code></li>
<li>Back to step 1</li>
</ol>
<p>Following the redirects exhausted the browser&#39;s redirect limit. Every path 307&#39;d to itself.</p>
<h2>Where the base is lost</h2>
<p>Vite strips the base deliberately, since its middleware pipeline is meant to work in base-relative terms. TanStack Start redirects deliberately, since a URL missing its configured base should be corrected. The Cloudflare plugin forwards what it is given.</p>
<p>The gap is in the handoff. Vite preserves the browser-facing URL on <code>req.originalUrl</code> precisely so that middleware needing the real URL can recover it. The dev plugin was not reading it:</p>
<pre><code class="hljs language-ts"><span class="hljs-keyword">const</span> url = <span class="hljs-keyword">new</span> <span class="hljs-title function_">URL</span>(req.<span class="hljs-property">url</span> ?? <span class="hljs-string">&quot;/&quot;</span>, address);</code></pre><p>So the Worker received a different pathname in development than the browser had requested.</p>
<h2>The fix</h2>
<pre><code class="hljs language-ts"><span class="hljs-keyword">const</span> url = <span class="hljs-keyword">new</span> <span class="hljs-title function_">URL</span>(req.<span class="hljs-property">originalUrl</span> ?? req.<span class="hljs-property">url</span> ?? <span class="hljs-string">&quot;/&quot;</span>, address);</code></pre><p><code>/example/pricing?plan=pro</code> now reaches the Worker unchanged, base path and query string intact, with the existing fallback preserved.</p>
<p>Cloudflare&#39;s own Vite plugin already restores <code>req.originalUrl</code> before constructing the Worker request, for the same reason. The fix aligns this plugin with that behaviour rather than introducing anything new.</p>
<h2>Validating it</h2>
<p>Before, against the unmodified plugin: <code>/example/</code> returned <code>307 Location: /example/</code>, and <code>/example/pricing</code> returned <code>307 Location: /example/pricing</code>. Both self-redirects.</p>
<p>After: <code>/</code> returns <code>302 Location: /example/</code>, <code>/example/</code> returns <code>200</code>, and <code>/example/pricing</code> returns <code>200</code>.</p>
<h2>Where these bugs live</h2>
<p>When requests loop between tools, the cause is often at a boundary rather than inside any one of them, in an assumption one side makes about what the other preserves. <code>req.url</code> versus <code>req.originalUrl</code> is a distinction that only surfaces once an app is mounted somewhere other than the root.</p>
<p>Merged as <a href="https://github.com/alchemy-run/cloudflare-tools/pull/86">alchemy-run/cloudflare-tools#86</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Showing link destinations on hover in Zed's markdown preview</title>
      <link>https://warya.2to6x.com/blog/link-destinations-on-hover-zed</link>
      <guid isPermaLink="true">https://warya.2to6x.com/blog/link-destinations-on-hover-zed</guid>
      <pubDate>Mon, 20 Jul 2026 00:00:00 GMT</pubDate>
      <dc:creator>Warya Wayne</dc:creator>
      <description>The markdown preview gave no way to see where a link pointed without clicking it. This change reports URL hovers from the renderer and shows the destination in the bottom-left corner.</description>
      <content:encoded><![CDATA[<p>Zed&#39;s markdown preview rendered links but did not surface their destinations. Seeing where a link pointed meant clicking it, or right clicking and copying the URL.</p>
<p>This change reports URL hovers from the markdown renderer and displays the hovered destination with the existing <code>LinkPreview</code> component in the bottom-left corner, the same position browsers use.</p>
<h2>Hit detection for linked images</h2>
<p>Text links are straightforward. The text layout already knows what is under the pointer.</p>
<p>Linked images are not. An image sits outside the text layout, so that hit detection does not see it, and it needs its own path. This is the same structural detail behind <a href="/blog/linked-images-clickable-zed">making linked images clickable</a>: nested images live outside the system that handles the rest of the content.</p>
<h2>Clearing the preview</h2>
<p>The other half of the work is clearing the indicator so it never shows a stale URL. It clears when the pointer leaves a link, when it leaves the preview entirely, and when the displayed markdown changes. That last case covers scrolling or re-rendering while a link is hovered.</p>
<h2>Test plan</h2>
<p>The test plan covers the disappearing cases alongside the appearing ones: moving away from a link, moving outside the preview, and changing the document.</p>
<p>Merged as <a href="https://github.com/zed-industries/zed/pull/60942">zed-industries/zed#60942</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Making linked images clickable in Zed's markdown preview</title>
      <link>https://warya.2to6x.com/blog/linked-images-clickable-zed</link>
      <guid isPermaLink="true">https://warya.2to6x.com/blog/linked-images-clickable-zed</guid>
      <pubDate>Thu, 02 Jul 2026 00:00:00 GMT</pubDate>
      <dc:creator>Warya Wayne</dc:creator>
      <description>Images nested inside markdown links rendered but were not interactive. The fix covers the nesting, a callback that needed to be cloneable, and event bubbling from the broken-image fallback.</description>
      <content:encoded><![CDATA[<p>In Zed&#39;s markdown preview, a linked image such as a README build badge rendered correctly but did nothing when clicked. The <code>[![alt](image.svg)](https://destination)</code> shape that badges use had no pointer cursor, no click handling, and no &quot;Copy Link&quot; entry in the context menu.</p>
<h2>Cause</h2>
<p>The renderer handled <code>&lt;a&gt;</code> tags and it handled <code>&lt;img&gt;</code> tags. It did not handle an <code>&lt;img&gt;</code> nested inside an <code>&lt;a&gt;</code>. The image drew itself and the surrounding link never got a chance to make it interactive.</p>
<h2>The fix</h2>
<p>The nesting is detected during render. For a linked image, the wrapper <code>div</code> gets <code>cursor_pointer</code> and an <code>on_click</code> handler that opens the URL, or delegates to the <code>on_url_click</code> callback when one is set. The wrapper also captures right clicks so &quot;Copy Link&quot; appears in the context menu.</p>
<p>Two details went beyond the nesting check:</p>
<p><strong>The callback had to become cloneable.</strong> <code>on_url_click</code> was a <code>Box&lt;dyn Fn&gt;</code>, which a single handler can own. Multiple handlers need it now, so it became an <code>Rc&lt;dyn Fn&gt;</code>.</p>
<p><strong>Broken images opened the URL twice.</strong> When an image fails to load, a fallback element renders in its place, and that fallback had its own click handler. Clicking it fired the fallback handler and then bubbled up to the wrapper handler, opening the destination twice. <code>cx.stop_propagation()</code> in the fallback resolves it. Images that 404 are common enough in READMEs that this path is worth exercising directly.</p>
<p>The whole feature is gated behind <code>!self.style.prevent_mouse_interaction</code>, so contexts that deliberately disable interaction are unchanged.</p>
<h2>Testing</h2>
<p><code>cargo test -p markdown</code>, 95 tests passing, plus manual verification that badges open correctly and right click offers &quot;Copy Link&quot;.</p>
<p>Merged as <a href="https://github.com/zed-industries/zed/pull/59525">zed-industries/zed#59525</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Adding total diff stats to Zed's git panel</title>
      <link>https://warya.2to6x.com/blog/git-panel-total-diff-stats</link>
      <guid isPermaLink="true">https://warya.2to6x.com/blog/git-panel-total-diff-stats</guid>
      <pubDate>Tue, 02 Jun 2026 00:00:00 GMT</pubDate>
      <dc:creator>Warya Wayne</dc:creator>
      <description>The git panel showed additions and deletions per file but no total across the change set. The change accumulates them in crates/git_ui/src/git_panel.rs.</description>
      <content:encoded><![CDATA[<p>Zed&#39;s git panel showed additions and deletions for each file. It did not show the total across the whole change set. This change adds that total. It was my first contribution to Zed.</p>
<h2>The change</h2>
<p>The panel already tracked per entry diff stats. It did not accumulate them into a single figure, which is what makes the size of a change readable at a glance. The work landed entirely in <code>crates/git_ui/src/git_panel.rs</code>: 50 lines added, 17 removed.</p>
<h2>What the work involved</h2>
<p>The arithmetic is trivial. Most of the time went into reading enough of the panel to find where its state lived and when it was recomputed, so the total stays in sync with the entries feeding it. In an unfamiliar Rust codebase, a small diff still means reading a lot of code to know where it goes.</p>
<p>I mentioned in the PR description that this was a first contribution and that I use Zed daily.</p>
<p>Merged as <a href="https://github.com/zed-industries/zed/pull/58018">zed-industries/zed#58018</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>create-vite asked React users for the framework twice</title>
      <link>https://warya.2to6x.com/blog/create-vite-framework-passthrough</link>
      <guid isPermaLink="true">https://warya.2to6x.com/blog/create-vite-framework-passthrough</guid>
      <pubDate>Fri, 08 May 2026 00:00:00 GMT</pubDate>
      <dc:creator>Warya Wayne</dc:creator>
      <description>Scaffolding a React project with TanStack Router prompted for the framework a second time. The Solid path passed --framework, the React path passed nothing.</description>
      <content:encoded><![CDATA[<p>Run <code>create-vite@latest</code>, pick React, then pick TanStack Router. The TanStack CLI opens and asks which framework you want, React or Solid, one step after the framework was already chosen.</p>
<h2>Where the second prompt came from</h2>
<p><code>create-vite</code> hands off to the TanStack CLI to finish scaffolding. The Solid path passed <code>--framework solid</code> along with the handoff. The React path passed no framework argument, so the CLI prompted for one. Solid users never saw the prompt, React users always did.</p>
<p>Both paths scaffolded a correct project, so nothing errored and no test failed. The React branch had simply not been updated after a TanStack CLI behaviour change made the argument meaningful, and the only symptom was one extra prompt.</p>
<h2>The fix</h2>
<p>Pass <code>--framework react</code>, matching what the Solid branch already did. One line in <code>packages/create-vite/src/index.ts</code>.</p>
<p>Merged as <a href="https://github.com/vitejs/vite/pull/22397">vitejs/vite#22397</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Declining demo pages in the TanStack CLI still scaffolded demo support files</title>
      <link>https://warya.2to6x.com/blog/tanstack-cli-strip-demo-files</link>
      <guid isPermaLink="true">https://warya.2to6x.com/blog/tanstack-cli-strip-demo-files</guid>
      <pubDate>Mon, 20 Apr 2026 00:00:00 GMT</pubDate>
      <dc:creator>Warya Wayne</dc:creator>
      <description>The demo filter matched only demo routes, so components, hooks, data, and images came along anyway. Two contributors' PRs were merged into a single fix.</description>
      <content:encoded><![CDATA[<p>The TanStack CLI asks whether you would like demo and example pages. Answering no still left demo files in the new project: components nothing rendered, images nothing referenced, hooks nothing called.</p>
<h2>What was leaking</h2>
<p>The filter deciding what counted as a demo file matched two patterns: files under <code>/routes/demo/</code>, and files named <code>/routes/demo.*</code>.</p>
<p>Demo routes were excluded correctly. Everything supporting them was not:</p>
<ul>
<li>support files in <code>/lib/</code>, <code>/hooks/</code>, <code>/data/</code>, <code>/components/</code>, and <code>/store/</code></li>
<li><code>demo.index.css</code> at the <code>src/</code> root</li>
<li>example assets in <code>/public/</code>, such as <code>example-guitar-*.jpg</code> and <code>demo-neon.svg</code></li>
</ul>
<p>All of it was copied into new projects with no routes referencing it.</p>
<p>Separately, the base scaffolding shipped a <code>Header</code>, a <code>Footer</code>, a <code>ThemeToggle</code>, a styled landing page, and an About route regardless of the answer.</p>
<h2>The fix</h2>
<p><code>isDemoRoutePath</code> became <code>isDemoFilePath</code>, and the scope widened to match the new name. Support directories are covered, filtering extends to add-on <code>integrations</code> rather than routes and files alone, and the Better Auth header component became a template that renders <code>null</code> when its demo route is excluded, rather than linking to a route that no longer exists.</p>
<h2>Two PRs became one</h2>
<p>I had opened <a href="https://github.com/TanStack/cli/pull/431">#431</a>. Another contributor, <a href="https://github.com/wyMinLwin">@wyMinLwin</a>, had opened <a href="https://github.com/TanStack/cli/pull/429">#429</a> against the same problem. The maintainers combined both into a single unified fix, added coverage for edge cases neither PR had caught, and preserved both original commits with author credit.</p>
<p>Closes <a href="https://github.com/TanStack/cli/issues/422">#422</a> and <a href="https://github.com/TanStack/cli/issues/409">#409</a>. Merged as <a href="https://github.com/TanStack/cli/pull/434">TanStack/cli#434</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Adding a Drizzle add-on to the TanStack CLI for Postgres, MySQL, and SQLite</title>
      <link>https://warya.2to6x.com/blog/tanstack-cli-drizzle-addon</link>
      <guid isPermaLink="true">https://warya.2to6x.com/blog/tanstack-cli-drizzle-addon</guid>
      <pubDate>Mon, 20 Oct 2025 00:00:00 GMT</pubDate>
      <dc:creator>Warya Wayne</dc:creator>
      <description>Scaffolding a TanStack app had no database option. The add-on templates a schema, a config, a client, and a working demo route across three drivers.</description>
      <content:encoded><![CDATA[<p>Before this, scaffolding a TanStack app came with no database story. You picked your framework, your router and your add-ons, then wired up persistence by hand afterwards.</p>
<p>This PR added Drizzle ORM as a first-class add-on, with support for Postgres, MySQL, and SQLite.</p>
<h2>Three drivers means most files become templates</h2>
<p>A CLI add-on is not a library you import. It is a set of templates that has to produce correct, working code for every combination of choices a user can make, and supporting three drivers makes most of the interesting files driver specific.</p>
<p>Each driver needs its own:</p>
<ul>
<li><strong>client construction</strong>: <code>src/db/index.ts</code> connects differently per driver</li>
<li><strong>schema syntax</strong>: Drizzle&#39;s column builders are driver specific, so a Postgres schema is not a MySQL schema</li>
<li><strong>config</strong>: <code>drizzle.config.ts</code> needs the right dialect and credentials shape</li>
<li><strong>environment variables</strong>: appended to <code>.env.local</code>, with a different connection string format per driver</li>
</ul>
<p>Each of those became an EJS template branching on the selected driver, alongside an <code>info.json</code> describing the add-on to the CLI engine and a <code>package.json.ejs</code> pulling in the matching driver package, <code>pg</code>, <code>mysql2</code>, or a SQLite driver, rather than all three.</p>
<h2>The demo route</h2>
<p>The add-on also ships a working demo route. It exercises the client, the schema, and the config together, so a driver specific mistake in any of the three templates shows up when the demo route runs, before a user hits it. A <a href="/blog/tanstack-cli-strip-demo-files">later PR</a> covered removing demo files like this one when a user opts out.</p>
<h2>Where the work actually is</h2>
<p>The hard part is the combinatorics rather than the feature. Every option multiplies the states the templates must produce valid output for, and the failure mode is quiet: a scaffold that looks fine and does not run. Templating one schema is easy. Templating three that are each idiomatic for their dialect is the work.</p>
<p>Merged as <a href="https://github.com/TanStack/cli/pull/207">TanStack/cli#207</a>.</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
