<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Arvin Wilderink</title>
    <description>The latest articles on Forem by Arvin Wilderink (@awilderink).</description>
    <link>https://forem.com/awilderink</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F366459%2Fb1d33b9d-15b1-41cf-940e-6c5a4b67142c.jpeg</url>
      <title>Forem: Arvin Wilderink</title>
      <link>https://forem.com/awilderink</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/awilderink"/>
    <language>en</language>
    <item>
      <title>We built a big app on htmx. Then we needed islands.</title>
      <dc:creator>Arvin Wilderink</dc:creator>
      <pubDate>Tue, 07 Apr 2026 16:09:33 +0000</pubDate>
      <link>https://forem.com/awilderink/we-built-a-big-app-on-htmx-then-we-needed-islands-2dp2</link>
      <guid>https://forem.com/awilderink/we-built-a-big-app-on-htmx-then-we-needed-islands-2dp2</guid>
      <description>&lt;h2&gt;
  
  
  Why we picked htmx
&lt;/h2&gt;

&lt;p&gt;Before I get into what broke, it is worth saying why we picked htmx in the first place, because I still think the choice was right.&lt;/p&gt;

&lt;p&gt;Our app is a domain-driven monolith. Submissions, policies, parties, parameters. Each aggregate has routes that render representations of itself, and when the user acts on a thing, the server decides what the thing should look like next. The client does not need to know the state machine of a submission. The server just tells it. That is HATEOAS, and it is not a gimmick: it is the natural fit for a DDD app where the server &lt;em&gt;is&lt;/em&gt; the source of truth. Moving that state machine into a client store would have meant duplicating a model we already had and paying a "keep the client model in sync with the server model" tax on every feature, forever.&lt;/p&gt;

&lt;p&gt;htmx also gave us end-to-end type safety for free. Our server renders typed JSX against our domain models, and the browser consumes whatever comes back. There is no API contract, no OpenAPI generator, no client SDK. The contract &lt;em&gt;is&lt;/em&gt; the HTML. Rename a field on a domain object and the type error is in the template, not six layers away in a generated client.&lt;/p&gt;

&lt;p&gt;For 90% of the app, this is still the right architecture. Routes render HTML, forms post to routes, htmx swaps the relevant fragments. No client state, no client routing, no client data layer. Just the domain, the server, and the DOM.&lt;/p&gt;

&lt;h2&gt;
  
  
  The wizard that broke me
&lt;/h2&gt;

&lt;p&gt;We have a spreadsheet-import wizard. The user uploads a file, picks a sheet and header row, then maps each column to a field in our domain schema before a background job parses the rows. Three steps, one progress bar, one submit button. From the user's perspective, it is one feature.&lt;/p&gt;

&lt;p&gt;I sat down to add a "Save mapping as template" button to it last quarter and stopped to count how many places the wizard already lived. Here it is:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;th&gt;Target&lt;/th&gt;
&lt;th&gt;Swap&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST /import/step-1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#upload-form&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;outerHTML&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST /import/step-2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#progress&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;outerHTML&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST /import/step-3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#progress&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;outerHTML&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET /import/back&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#step-content&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;innerHTML&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET /import/retry&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#step-content&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;innerHTML&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST /import/template-confirm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;&lt;code&gt;none&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET /import/progress&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#progress&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;morph&lt;/code&gt; (every 500ms)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET /import/cancel&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;&lt;code&gt;none&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Eight endpoints. Three swap targets. One in-memory &lt;code&gt;Map&amp;lt;string, UploadJob&amp;gt;&lt;/code&gt; shared between the background parser and the polling endpoint. And to add my button: find the route handler that should produce the new state. Then find the markup that owns the target id, in some other file you might or might not remember the name of. Then make sure your new fragment HTML matches the wrapper id of the existing target. Then test all four entry points that could land the user in the new state, because nothing in the codebase enumerates them.&lt;/p&gt;

&lt;p&gt;Want to rename &lt;code&gt;#step-content&lt;/code&gt;? Grep across eight files and hope you caught them all.&lt;/p&gt;

&lt;h2&gt;
  
  
  The swap graph
&lt;/h2&gt;

&lt;p&gt;The wizard is an extreme case, but the underlying pattern is everywhere in any non-trivial htmx app.&lt;/p&gt;

&lt;p&gt;A single form post in a complex layout often needs to update more than one region: the main panel, a sidebar summary, a toolbar button, a toast. In htmx that means the route handler has to return every affected fragment, threaded through &lt;code&gt;hx-swap-oob&lt;/code&gt;, and the caller has to name each target by DOM id. One action's "response shape" is now a distributed graph of IDs that live in other files.&lt;/p&gt;

&lt;p&gt;Rename the id, break the swap. Move the markup that owns the id, break the swap. Add a new place that needs to react to the same action, go edit the route handler that returned it. Nothing connects these except strings, and nothing compiles.&lt;/p&gt;

&lt;p&gt;In a small app this is fine. The graph is small enough to hold in your head. In a complex layout it becomes &lt;em&gt;the&lt;/em&gt; source of brittleness. You trace "what happens when the user clicks Upload" by grepping across four files, and you find the broken cases in production.&lt;/p&gt;

&lt;p&gt;That is one of the two locality problems an htmx-at-scale codebase hands you: the &lt;em&gt;feature locality&lt;/em&gt; problem. The unit you actually want to reason about ("the upload wizard," "the orders panel") does not exist as a thing in the code. It is implicit in a graph of IDs and route handlers, and the only tool you have for navigating it is grep.&lt;/p&gt;

&lt;p&gt;The other locality problem shows up the moment you try to build a stateful interaction inside one of those fragments.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Alpine ceiling
&lt;/h2&gt;

&lt;p&gt;For client-side state in an htmx app, the standard answer is Alpine. And it is genuinely great for the small stuff: class toggles, open/close, hover states, a confirm dialog. The ceiling is what happens when the interaction needs to do more than that.&lt;/p&gt;

&lt;p&gt;Take the simplest possible "pick some things and submit them" UI. A list of tasks with hours, click to include each one, a running total, a submit button. The "right" Alpine pattern at this size is &lt;code&gt;Alpine.data("id", () =&amp;gt; ({...}))&lt;/code&gt; in its own file. So:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// alpine/data/picker.ts&lt;/span&gt;
&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Alpine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;picker&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;selected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

  &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="nf"&gt;total&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;selected&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;

  &lt;span class="nf"&gt;toggle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;selected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;delete&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;selected&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`[data-id="&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"]`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;selected&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;getAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data-hours&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the markup, in a file hundreds of lines away:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;x-data=&lt;/span&gt;&lt;span class="s"&gt;"picker"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  {tasks.map((task) =&amp;gt; (
    &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;data-id=&lt;/span&gt;&lt;span class="s"&gt;{task.id}&lt;/span&gt; &lt;span class="na"&gt;data-hours=&lt;/span&gt;&lt;span class="s"&gt;{task.hours}&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"checkbox"&lt;/span&gt; &lt;span class="na"&gt;x-on:change=&lt;/span&gt;&lt;span class="s"&gt;{`toggle('${task.id}')`}&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
      {task.name} ({task.hours}h)
    &lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
  ))}

  &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Total: &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;x-text=&lt;/span&gt;&lt;span class="s"&gt;"total"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;h&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt;
    &lt;span class="na"&gt;hx-post=&lt;/span&gt;&lt;span class="s"&gt;"/invoice"&lt;/span&gt;
    &lt;span class="na"&gt;x-bind:hx-vals=&lt;/span&gt;&lt;span class="s"&gt;"JSON.stringify({task_ids: Object.keys(selected)})"&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    Create invoice
  &lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look at the seams.&lt;/p&gt;

&lt;p&gt;The server already has the task data (id, hours, name) at render time. To get the hours into the picker's running total, we serialize it back into a &lt;code&gt;data-hours&lt;/code&gt; attribute on each row, then &lt;code&gt;getAttribute&lt;/code&gt; it out and &lt;code&gt;Number()&lt;/code&gt; it the moment it is touched. The selection state is &lt;code&gt;Record&amp;lt;string, number&amp;gt;&lt;/code&gt; because the value just made a round trip through a string DOM attribute. No type system has any opinion on whether &lt;code&gt;data-hours&lt;/code&gt; exists, what shape it is, or whether &lt;code&gt;task.hours&lt;/code&gt; upstream is even still called that.&lt;/p&gt;

&lt;p&gt;When the user clicks "Create invoice," Alpine has to hand its in-memory state over to htmx. There is no clean way to do this, so it goes through &lt;code&gt;x-bind:hx-vals="JSON.stringify(...)"&lt;/code&gt;. Alpine serializes its selection so htmx can deserialize it into a form body so the server can deserialize it again. Two frameworks negotiating over the same state, in two different attribute namespaces, with no shared type.&lt;/p&gt;

&lt;p&gt;This is the &lt;em&gt;component locality&lt;/em&gt; problem. The behavior lives in one file, the markup in another, the types implied by &lt;code&gt;data-*&lt;/code&gt; attributes exist nowhere, and the handoff between Alpine and htmx is a stringly-typed protocol you invent each time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rewrite that wasn't
&lt;/h2&gt;

&lt;p&gt;The temptation, of course, is to rip the bandage off. Pick Next.js or Remix or SvelteKit, port everything over the next two quarters, and live happily ever after. We are not doing that. The htmx app works. Half of it would not benefit from React-style state management at all. A rewrite would burn months and ship nothing new to customers in the meantime. It is not the right trade.&lt;/p&gt;

&lt;p&gt;The Astro insight is the one I wish I had earlier: &lt;strong&gt;server-render everything, ship JavaScript only where it earns its keep.&lt;/strong&gt; Astro calls these islands. Most of your page is HTML, with small interactive components dotted through it. The trouble with Astro itself, for us, is that Astro is its own server. We already have Elysia. We already have our auth, our middleware, our htmx, our routing. We want islands without giving up any of that.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/honojs/honox" rel="noopener noreferrer"&gt;HonoX&lt;/a&gt; is the other option worth naming. It is a Hono-based meta-framework with Vite-backed islands hydration, and it supports React, Preact, and Solid. But "getting started" in HonoX is not a library install. It means a prescribed project layout: &lt;code&gt;app/server.ts&lt;/code&gt; with &lt;code&gt;createApp()&lt;/code&gt;, &lt;code&gt;app/client.ts&lt;/code&gt;, &lt;code&gt;app/global.d.ts&lt;/code&gt;, a &lt;code&gt;_renderer.tsx&lt;/code&gt; file, all your routes rewritten as &lt;code&gt;createRoute()&lt;/code&gt; calls inside &lt;code&gt;app/routes/&lt;/code&gt;, and islands placed in &lt;code&gt;app/islands/&lt;/code&gt; or named with a &lt;code&gt;$&lt;/code&gt; prefix. Islands cannot access the request context, so data has to be threaded down from route files. The build is two steps. If you are starting a new project on Hono and are happy building inside those conventions, it is a reasonable choice. If you already have a running Elysia or Hono app with its own structure, adopting HonoX means migrating into its layout, and you are back to the rewrite problem by a different name.&lt;/p&gt;

&lt;p&gt;What I wanted was a layer, not a framework. Something that adds islands to the server I already have, leaves htmx alone, and gets out of the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Atollic
&lt;/h2&gt;

&lt;p&gt;So I built &lt;a href="https://github.com/awilderink/atollic" rel="noopener noreferrer"&gt;Atollic&lt;/a&gt;. One sentence: &lt;strong&gt;Astro-style islands for any WinterCG server.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Bring your own server (Elysia, Hono, Bun, Workers, anything that speaks &lt;code&gt;Request&lt;/code&gt;/&lt;code&gt;Response&lt;/code&gt;). Bring your own UI framework via an adapter. Solid and React ship today; Preact and others are pluggable. Each island picks its framework via the JSX pragma, so you can have a React island next to a Solid island on the same page — useful when one component has a React-only dependency and you do not want to rewrite everything to match. Mark a component with &lt;code&gt;"use client"&lt;/code&gt; and Atollic SSRs it on the server, then hydrates it on the client. Everything else is zero-JS HTML.&lt;/p&gt;

&lt;p&gt;The same picker, as a single Solid island, looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/** @jsxImportSource solid-js */&lt;/span&gt;
&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createSignal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;For&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;solid-js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;hours&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;TaskPicker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;selected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setSelected&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;createSignal&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tasks&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;selected&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hours&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;toggle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nf"&gt;setSelected&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;For&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{(&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;
              &lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;checkbox&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
              &lt;span class="nx"&gt;checked&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;selected&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
              &lt;span class="nx"&gt;onChange&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;toggle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
            &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;            &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hours&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/label&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;        &lt;span class="p"&gt;)}&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/For&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;total&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/p&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="nx"&gt;hx&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/invoice&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;hx&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;closest .order-panel&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;For&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{[...&lt;/span&gt;&lt;span class="nf"&gt;selected&lt;/span&gt;&lt;span class="p"&gt;()]}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;{(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hidden&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;task_ids&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;}
&lt;/span&gt;        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/For&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;submit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;disabled&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;selected&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nx"&gt;Create&lt;/span&gt; &lt;span class="nx"&gt;invoice&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/form&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One file. The tasks are a typed &lt;code&gt;Task[]&lt;/code&gt; prop. No &lt;code&gt;data-*&lt;/code&gt; attributes, no &lt;code&gt;Number()&lt;/code&gt; casts, no &lt;code&gt;Record&amp;lt;string, number&amp;gt;&lt;/code&gt;. Selection is a typed &lt;code&gt;Set&amp;lt;string&amp;gt;&lt;/code&gt; signal. The submit button is a normal HTML form posting &lt;code&gt;task_ids&lt;/code&gt;, so htmx and the rest of the page can swap whatever they want around it without coordination, and the form still works in a &lt;code&gt;&amp;lt;noscript&amp;gt;&lt;/code&gt; page.&lt;/p&gt;

&lt;p&gt;And critically: htmx swaps still work. Atollic installs a &lt;code&gt;MutationObserver&lt;/code&gt; on &lt;code&gt;document.body&lt;/code&gt;, so when an htmx swap drops a fresh &lt;code&gt;&amp;lt;TaskPicker&amp;gt;&lt;/code&gt; into the DOM, it gets mounted automatically. No &lt;code&gt;htmx:afterSwap&lt;/code&gt; listeners. No re-init code. It just works the way you would naively expect.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we get back
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Locality, both kinds.&lt;/strong&gt; For interactions, state + behavior + markup + types live in one file. For features that span multiple UI regions, the island &lt;em&gt;is&lt;/em&gt; the boundary. No more fan-out of &lt;code&gt;hx-swap-oob&lt;/code&gt; targets glued by string IDs across unrelated routes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type safety across the seam.&lt;/strong&gt; Props are serialized and typed on both sides. No more "oh, I forgot the server is sending a string."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero JS where it does not earn its keep.&lt;/strong&gt; Pages without islands ship zero JavaScript. No client framework on the listings page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;htmx is still in charge.&lt;/strong&gt; Routing, navigation, server fetches, partial swaps, all htmx, exactly as before. Your UI framework only owns the gnarly parts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No rewrite.&lt;/strong&gt; We dropped one component into one wizard. Nothing else moved. The other 90% of the app does not know islands exist.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where this is at, honestly
&lt;/h2&gt;

&lt;p&gt;Atollic is &lt;code&gt;v0.0.x&lt;/code&gt;. It is a real working library that ships our import wizard in production. The API will move before 1.0 and there are sharp edges. If you want the safest possible "boring stack" decision, this is not it.&lt;/p&gt;

&lt;p&gt;If you want the smallest possible escape hatch out of "htmx is starting to hurt in the complex corners" without throwing away the parts that work, &lt;code&gt;bun add atollic&lt;/code&gt;, &lt;a href="https://github.com/awilderink/atollic" rel="noopener noreferrer"&gt;read the README&lt;/a&gt;, and tell me what breaks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Help shape it
&lt;/h2&gt;

&lt;p&gt;Atollic is small enough that one motivated person can move it forward in a weekend, and there is plenty of room for hands. If any of this sounds like a problem you have lived with, I would love your help making it better. A few concrete things that would land hard right now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A Preact adapter.&lt;/strong&gt; The &lt;code&gt;FrameworkAdapter&lt;/code&gt; interface is small. Solid is the reference implementation, React is the second. Preact should be straightforward.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A Node fetch adapter recipe&lt;/strong&gt; for people not on Bun.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Examples.&lt;/strong&gt; Real little apps using Atollic with Hono, with Workers, with htmx in anger. If you build one, I will link it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bug reports.&lt;/strong&gt; Especially weird ones. The sharp edges only get sanded down when someone bumps into them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docs.&lt;/strong&gt; The README is the docs right now, and that is not going to be true forever.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Issues and PRs are open at &lt;a href="https://github.com/awilderink/atollic" rel="noopener noreferrer"&gt;github.com/awilderink/atollic&lt;/a&gt;. If you want to talk through an idea before you write code, open a discussion or just ping me. No contribution is too small. A typo fix is a contribution.&lt;/p&gt;

&lt;p&gt;And whether or not you write a line of code: I am very interested in where your htmx app starts to hurt. Drop it in the comments.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>htmx</category>
      <category>vite</category>
    </item>
  </channel>
</rss>
