<?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>DEV Community: Dennis Morello</title>
    <description>The latest articles on DEV Community by Dennis Morello (@morellodev).</description>
    <link>https://dev.to/morellodev</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F219117%2F1f9c2fb4-774c-4736-a658-17104c6ba25d.JPG</url>
      <title>DEV Community: Dennis Morello</title>
      <link>https://dev.to/morellodev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/morellodev"/>
    <language>en</language>
    <item>
      <title>Baseline 2026: 4 APIs That Changed How I Code</title>
      <dc:creator>Dennis Morello</dc:creator>
      <pubDate>Wed, 22 Jul 2026 17:54:22 +0000</pubDate>
      <link>https://dev.to/morellodev/baseline-2026-4-apis-that-changed-how-i-code-43n9</link>
      <guid>https://dev.to/morellodev/baseline-2026-4-apis-that-changed-how-i-code-43n9</guid>
      <description>&lt;p&gt;Baseline 2026 brought a wave of new interoperable web platform APIs. Not the kind of big where everything changes overnight. The kind where APIs that have been experimental for years finally ship in every browser. Baseline means you can use them in production without a polyfill, and the 2026 cohort has a handful that actually changed what I reach for when I start a new project. Not "technically you could," but "I stopped installing this."&lt;/p&gt;

&lt;h2&gt;
  
  
  The Navigation API: one &lt;code&gt;navigate&lt;/code&gt; event instead of three different APIs
&lt;/h2&gt;

&lt;p&gt;The Navigation API &lt;a href="https://web.dev/blog/baseline-navigation-api" rel="noopener noreferrer"&gt;became Baseline Newly available in early 2026&lt;/a&gt;, supported in Chrome, Edge, Firefox 147, and Safari 26.2. It replaces the fragmented mess that was History API routing: &lt;code&gt;pushState&lt;/code&gt; for navigation, &lt;code&gt;popstate&lt;/code&gt; for back/forward, and manual link click handlers to prevent full-page reloads. Three separate APIs you had to wire together yourself, or reach for a router library that did it for you.&lt;/p&gt;

&lt;p&gt;Here is what client-side routing looked like before:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before: three separate concerns, manually wired&lt;/span&gt;
&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;popstate&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="nx"&gt;e&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="nf"&gt;renderRoute&lt;/span&gt;&lt;span class="p"&gt;(&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;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&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;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;click&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="nx"&gt;e&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;link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;closest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;a[data-route]&lt;/span&gt;&lt;span class="dl"&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;link&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pushState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;renderRoute&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;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;pathname&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="c1"&gt;// Initial render&lt;/span&gt;
&lt;span class="nf"&gt;renderRoute&lt;/span&gt;&lt;span class="p"&gt;(&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;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here is the Navigation API equivalent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// After: one centralized event&lt;/span&gt;
&lt;span class="nx"&gt;navigation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;navigate&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="nx"&gt;e&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;url&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;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;intercept&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;renderRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One event. One interception point. The &lt;code&gt;intercept()&lt;/code&gt; call tells the browser "I'll handle this navigation, wait for me," which also means the browser can show a loading indicator natively. You get scroll restoration (&lt;code&gt;e.scroll()&lt;/code&gt;), form data access (&lt;code&gt;e.formData&lt;/code&gt;), and entry traversal (&lt;code&gt;navigation.traverseTo(key)&lt;/code&gt;) without wiring any of it yourself.&lt;/p&gt;

&lt;p&gt;This does not kill React Router or TanStack Router. Both have open discussions about adopting the Navigation API as a backend: &lt;a href="https://github.com/remix-run/react-router/discussions/11046" rel="noopener noreferrer"&gt;React Router's&lt;/a&gt; and &lt;a href="https://github.com/TanStack/router/discussions/821" rel="noopener noreferrer"&gt;TanStack Router's&lt;/a&gt;. They would sit on top of it rather than reimplement the History API workaround they currently maintain. The frameworks add value (nested routes, data loading, error boundaries) that the raw Navigation API does not. What the API kills is the need to pull in a router &lt;em&gt;just&lt;/em&gt; to avoid full-page reloads on a content site. If your routing needs are modest (a blog, a docs site, a dashboard with a few views), the platform now has you covered.&lt;/p&gt;

&lt;p&gt;The caveat: Safari 26.2 is &lt;a href="https://www.infoq.com/news/2026/05/navigation-api-browser" rel="noopener noreferrer"&gt;missing &lt;code&gt;precommitHandler&lt;/code&gt;&lt;/a&gt; support, which limits some advanced interception patterns. And Ian Hickson, the spec author, &lt;a href="https://html5doctor.com/interview-with-ian-hickson-html-editor" rel="noopener noreferrer"&gt;famously called&lt;/a&gt; &lt;code&gt;pushState()&lt;/code&gt; his "favourite mistake," so the replacement had a low bar to clear.&lt;/p&gt;

&lt;h2&gt;
  
  
  Container style queries: theming without JS class toggling
&lt;/h2&gt;

&lt;p&gt;Container style queries for custom properties &lt;a href="https://web.dev/blog/web-platform-05-2026" rel="noopener noreferrer"&gt;became Baseline Newly available in May 2026&lt;/a&gt;, with Firefox 151 shipping the last piece and Chrome 148 adding name-only container queries the same month.&lt;/p&gt;

&lt;p&gt;This one solves a problem I have written far too much JavaScript for: switching visual variants based on context. A card that renders differently in a sidebar vs. a hero section. A theme toggle that propagates across the component tree. All done with CSS classes, JS observers, or both.&lt;/p&gt;

&lt;p&gt;Here is the old pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before: JS class toggling for a card variant&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Card&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;variant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;default&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="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`card card--&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h3&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h3&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;excerpt&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.card--featured&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c"&gt;/* special styles */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.card--compact&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c"&gt;/* compact styles */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And with container style queries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.sidebar&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;container-name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sidebar&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c"&gt;/* no container-type needed for style queries */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.card&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c"&gt;/* default styles */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@container&lt;/span&gt; &lt;span class="n"&gt;sidebar&lt;/span&gt; &lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--variant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;featured&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.card&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;/* featured layout */&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@container&lt;/span&gt; &lt;span class="n"&gt;sidebar&lt;/span&gt; &lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--compact&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.card&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;/* compact layout */&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;The container declares its context via CSS custom properties, and descendant elements react. No JS. No class chains. The component does not need to know where it is rendered; the container owns that relationship.&lt;/p&gt;

&lt;p&gt;The limitation worth mentioning: &lt;a href="https://github.com/mdn/content/issues/44701" rel="noopener noreferrer"&gt;style queries currently only support custom properties&lt;/a&gt;. The spec allows querying any CSS property, but no browser ships that yet. So you query &lt;code&gt;style(--theme: dark)&lt;/code&gt;, not &lt;code&gt;style(background-color: black)&lt;/code&gt;. For the use case it solves (theming, layout variants, contextual styling): custom properties are exactly what you want. For arbitrary CSS property introspection, we are still waiting.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;:open&lt;/code&gt;: state tracking you never write
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://web.dev/blog/web-platform-05-2026" rel="noopener noreferrer"&gt;&lt;code&gt;:open&lt;/code&gt; pseudo-class became Baseline Newly available in May 2026&lt;/a&gt; when Safari 26.5 shipped it. It matches any element with an open semantic state: &lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt;, and &lt;code&gt;&amp;lt;input type="color"&amp;gt;&lt;/code&gt; / &lt;code&gt;&amp;lt;input type="date"&amp;gt;&lt;/code&gt; with their pickers open.&lt;/p&gt;

&lt;p&gt;Before &lt;code&gt;:open&lt;/code&gt;, styling an open disclosure meant either an attribute selector or JS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* Before: attribute selector, limited */&lt;/span&gt;
&lt;span class="nt"&gt;details&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;open&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;summary&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before: manual state tracking for a dialog&lt;/span&gt;
&lt;span class="nx"&gt;dialog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;toggle&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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dialog-open&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dialog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;open&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;With &lt;code&gt;:open&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;details&lt;/span&gt;&lt;span class="nd"&gt;:open&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;summary&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* Style the page when any dialog is open */&lt;/span&gt;
&lt;span class="nt"&gt;html&lt;/span&gt;&lt;span class="nd"&gt;:has&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;dialog&lt;/span&gt;&lt;span class="nd"&gt;:open&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;overflow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* Style a label when its associated select is open */&lt;/span&gt;
&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="nd"&gt;:has&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;select&lt;/span&gt;&lt;span class="nd"&gt;:open&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--accent&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;The &lt;code&gt;:has()&lt;/code&gt; combo is where this gets useful. &lt;code&gt;html:has(dialog:open)&lt;/code&gt; is a body scroll lock in one line of CSS. &lt;code&gt;label:has(select:open)&lt;/code&gt; is a parent style change driven by a child's state: something that used to require a JS mutation observer or a framework binding. The &lt;code&gt;:open&lt;/code&gt; selector tracks semantic state, not visibility: a &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt; that is semantically open but visually hidden still matches &lt;code&gt;:open&lt;/code&gt;. That distinction matters for things like &lt;code&gt;display: none&lt;/code&gt; inside an open disclosure.&lt;/p&gt;

&lt;p&gt;This replaces the &lt;code&gt;details[open]&lt;/code&gt; attribute selector (which only works for &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt;) and the pattern of manually toggling CSS classes on &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; or parent elements when modals or pickers open. One pseudo-class, all openable elements, no JS.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;Math.sumPrecise()&lt;/code&gt;: the "wait, &lt;code&gt;reduce&lt;/code&gt; is wrong?" moment
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Math.sumPrecise()&lt;/code&gt; &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/sumPrecise" rel="noopener noreferrer"&gt;became Baseline Newly available in April 2026&lt;/a&gt;, part of ES2026 after &lt;a href="https://socket.dev/blog/tc39-advances-11-proposals-for-math-precision-binary-apis-and-more" rel="noopener noreferrer"&gt;reaching TC39 Stage 4&lt;/a&gt; in July 2025.&lt;/p&gt;

&lt;p&gt;The problem it solves is floating-point summation error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;numbers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;e20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;e20&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="nx"&gt;numbers&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="c1"&gt;// 0&lt;/span&gt;
&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sumPrecise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;numbers&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;              &lt;span class="c1"&gt;// 0.1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The naive &lt;code&gt;reduce&lt;/code&gt; approach loses precision because floating-point addition is not associative. When you add a very small number to a very large one, the small number disappears. &lt;code&gt;Math.sumPrecise()&lt;/code&gt; uses the &lt;a href="https://github.com/tc39/proposal-math-sum" rel="noopener noreferrer"&gt;Shewchuk algorithm&lt;/a&gt; (or an equivalent) to produce the maximally correct answer: the result you would get with arbitrary-precision arithmetic rounded back to a float.&lt;/p&gt;

&lt;p&gt;What it does NOT fix: &lt;code&gt;0.1 + 0.2&lt;/code&gt;. That is a floating-point &lt;em&gt;representation&lt;/em&gt; problem, not a summation problem. &lt;code&gt;Math.sumPrecise([0.1, 0.2])&lt;/code&gt; still returns &lt;code&gt;0.30000000000000004&lt;/code&gt; because the literals &lt;code&gt;0.1&lt;/code&gt; and &lt;code&gt;0.2&lt;/code&gt; are inexact before &lt;code&gt;sumPrecise&lt;/code&gt; ever sees them.&lt;/p&gt;

&lt;p&gt;The API is deliberately not variadic (&lt;code&gt;Math.sumPrecise(1, 2, 3)&lt;/code&gt; throws). It takes an iterable to avoid stack overflows on large datasets. Empty iterables return &lt;code&gt;-0&lt;/code&gt; (the floating-point additive identity). Non-number elements throw a &lt;code&gt;TypeError&lt;/code&gt;, unlike &lt;code&gt;Math.max&lt;/code&gt; which silently coerces. TypeScript does not ship type definitions for it yet (&lt;a href="https://github.com/microsoft/TypeScript/issues/63427" rel="noopener noreferrer"&gt;issue #63427&lt;/a&gt;), so you may need a manual declaration depending on your &lt;a href="https://morello.dev/blog/typescript-7-is-here" rel="noopener noreferrer"&gt;TypeScript 7&lt;/a&gt; version.&lt;/p&gt;

&lt;p&gt;This is not the API that will headline anyone's year-end retrospective. But it fixes a real footgun: the kind of bug that passes code review because nobody spots the summation error in a 50-item array spread across a few chained &lt;code&gt;.map()&lt;/code&gt; and &lt;code&gt;.filter()&lt;/code&gt; calls. I wrote about &lt;a href="https://morello.dev/blog/five-things-you-might-not-know-about-javascript" rel="noopener noreferrer"&gt;JavaScript gotchas&lt;/a&gt; before, and floating-point behavior is the category that surprises experienced developers the most. Having a built-in that handles the common case correctly is a quiet win.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "Baseline Newly available" actually means for Baseline 2026
&lt;/h2&gt;

&lt;p&gt;All four of these APIs are Baseline &lt;em&gt;Newly&lt;/em&gt; available, not Widely available. That distinction matters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Newly available:&lt;/strong&gt; All core browsers (Chrome, Edge, Firefox, and Safari across desktop and mobile) support the feature. You can use it in production.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Widely available:&lt;/strong&gt; The feature has been interoperable for 30 months. You can use it without thinking about browser support.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These four are firmly in the "use it today" category. All core browsers ship them. If you still support older Safari or Firefox versions, feature detection is straightforward: &lt;code&gt;"navigation" in window&lt;/code&gt;, &lt;code&gt;CSS.supports("selector(:open)")&lt;/code&gt;, &lt;code&gt;@supports (container-name: x)&lt;/code&gt; in CSS. None of these require a polyfill to degrade gracefully; they just need a fallback path.&lt;/p&gt;

&lt;p&gt;The 30-month clock to Widely availability means these become "don't even think about it" territory between mid-2028 and late 2028.&lt;/p&gt;

&lt;h2&gt;
  
  
  The web platform is eating libraries from the edges in
&lt;/h2&gt;

&lt;p&gt;None of these four APIs kills a major framework. You will still reach for React Router if your app has nested layouts, data loaders, and route-level error boundaries. You will still use a CSS-in-JS library if your design system demands it. What they kill is the dependency you pulled in for &lt;em&gt;one&lt;/em&gt; thing: the router for a content site, the classnames utility for variant switching, the state tracking for modal-open classes.&lt;/p&gt;

&lt;p&gt;The gap between "what the platform gives you" and "what you need a library for" keeps narrowing. &lt;a href="https://morello.dev/blog/the-new-http-query-method" rel="noopener noreferrer"&gt;The HTTP QUERY method&lt;/a&gt; landed earlier this year for safe reads with a body. Now we have routing, variant-driven styling, state selectors, and precise math, all Baseline, all in every browser.&lt;/p&gt;

&lt;p&gt;We are not at zero dependencies. But each Baseline release shrinks the surface area that justifies pulling in someone else's code. And 2026 has been a good year for that.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>css</category>
    </item>
    <item>
      <title>Astro 7: What's New, What's Faster, and What Breaks</title>
      <dc:creator>Dennis Morello</dc:creator>
      <pubDate>Tue, 21 Jul 2026 13:25:59 +0000</pubDate>
      <link>https://dev.to/morellodev/astro-7-rust-compiler-vite-8-and-the-speed-release-4f2p</link>
      <guid>https://dev.to/morellodev/astro-7-rust-compiler-vite-8-and-the-speed-release-4f2p</guid>
      <description>&lt;p&gt;Astro 7, released &lt;a href="https://astro.build/blog/astro-7" rel="noopener noreferrer"&gt;June 22, 2026&lt;/a&gt;, is a speed release. The &lt;code&gt;.astro&lt;/code&gt; compiler got rewritten in Rust, the Markdown pipeline got rebuilt in Rust, and the bundler switched to Vite 8 with Rolldown. It is the same framework on faster infrastructure. Builds on real production Astro sites are 15 to 61 percent faster, with the biggest wins on sites where Markdown and &lt;code&gt;.astro&lt;/code&gt; compilation dominate the build.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Astro 7 actually is
&lt;/h2&gt;

&lt;p&gt;Astro 7 is an infrastructure release. The component format, the island architecture, the routing model: all the same. What changed is what sits underneath. The &lt;a href="https://astro.build/blog/astro-7" rel="noopener noreferrer"&gt;official announcement&lt;/a&gt; calls it the speed release, and that is the honest framing. Four systems got rebuilt or swapped, five experimental features went stable, and the result is faster builds without a new mental model.&lt;/p&gt;

&lt;p&gt;The closest parallel is &lt;a href="https://morello.dev/blog/typescript-7-is-here" rel="noopener noreferrer"&gt;TypeScript 7 rewriting its compiler in Go&lt;/a&gt;: keep the surface still, rebuild the engine. Astro 7 does the same, with Rust, across more layers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vite 8 and Rolldown
&lt;/h2&gt;

&lt;p&gt;Astro 7 upgrades to &lt;a href="https://astro.build/blog/astro-7" rel="noopener noreferrer"&gt;Vite 8&lt;/a&gt;, which ships &lt;a href="https://rolldown.rs" rel="noopener noreferrer"&gt;Rolldown&lt;/a&gt; as its bundler. Rolldown is a Rust bundler that replaces both esbuild and Rollup in the Vite pipeline, and runs 10 to 30 times faster than Rollup in benchmarks. If you had &lt;code&gt;esbuild&lt;/code&gt; options or &lt;code&gt;rollupOptions&lt;/code&gt; in your config, Vite 8 auto-converts them. The plugin API is the same, so existing Vite plugins keep working.&lt;/p&gt;

&lt;p&gt;Vite is Astro's build core: dev server, dependency optimization, production bundling. A faster bundler there is a faster everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Astro 7's new Rust compiler
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;.astro&lt;/code&gt; compiler was rewritten in Rust. It was Go before. The new compiler is built on &lt;a href="https://oxc.rs" rel="noopener noreferrer"&gt;oxc&lt;/a&gt; and &lt;a href="https://lightningcss.dev" rel="noopener noreferrer"&gt;Lightning CSS&lt;/a&gt;, and Astro reports about a 6 percent improvement in isolation. That sounds modest next to the Vite 8 jump, and it is. The compiler was already fast. But 6 percent compounds on a large build that runs thousands of &lt;code&gt;.astro&lt;/code&gt; files.&lt;/p&gt;

&lt;p&gt;Two behavior changes, both making the compiler stricter. It no longer auto-corrects HTML: markup is treated as-is. Unclosed tags are now errors, not warnings. If you had sloppy markup the Go compiler quietly fixed, the Rust compiler will tell you. It ships as native binaries per platform, with a WASM fallback.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sätteri: Markdown and MDX in Rust
&lt;/h2&gt;

&lt;p&gt;Sätteri is the new default Markdown and MDX pipeline. It is Rust-powered, built by Erika on &lt;a href="https://github.com/pulldown-cmark/pulldown-cmark" rel="noopener noreferrer"&gt;pulldown-cmark&lt;/a&gt; and oxc, and it replaces the unified/remark/rehype stack Astro used before.&lt;/p&gt;

&lt;p&gt;The performance is real. Astro's docs build and Cloudflare's docs build each shed over a minute. On a content-heavy site like this one, that is the win you feel.&lt;/p&gt;

&lt;p&gt;Sätteri ships built-in support for GFM, smartypants, heading IDs, directives, math, frontmatter, and wikilinks. The plugin API works differently from unified: plugins declare the node types they care about, and Sätteri routes only those. If you need the old stack, &lt;code&gt;unified()&lt;/code&gt; is still available via &lt;a href="https://docs.astro.build/en/guides/upgrade-to/v7/" rel="noopener noreferrer"&gt;&lt;code&gt;@astrojs/markdown-remark&lt;/code&gt;&lt;/a&gt;. But for most sites, Sätteri is the default and the faster path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Queued rendering is now the default
&lt;/h2&gt;

&lt;p&gt;Queued rendering was experimental in Astro 6. In Astro 7 it is stable and default. The old renderer walked the component tree recursively. Queued rendering uses a queue-based approach instead, and Astro reports roughly 2.4x faster rendering on expression-dense pages. You do not configure this. It is just on. The &lt;code&gt;queuedRendering&lt;/code&gt; experimental flag is gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Advanced routing with src/fetch.ts
&lt;/h2&gt;

&lt;p&gt;Advanced routing gives you a &lt;code&gt;src/fetch.ts&lt;/code&gt; entrypoint with full control over the request pipeline. It uses the standard &lt;a href="https://developers.cloudflare.com/workers/runtime-apis/handlers/fetch/" rel="noopener noreferrer"&gt;fetch handler&lt;/a&gt; pattern, the same &lt;code&gt;Request&lt;/code&gt; in, &lt;code&gt;Response&lt;/code&gt; out shape you see in &lt;a href="https://developers.cloudflare.com/workers/" rel="noopener noreferrer"&gt;Cloudflare Workers&lt;/a&gt; and WinterCG runtimes. It is Hono-compatible, so Hono middleware drops in directly.&lt;/p&gt;

&lt;p&gt;The interesting part is composability. Individual Astro features can be composed as middleware: auth before your actions, a logging layer before everything, all in one file that owns the request lifecycle. &lt;code&gt;src/fetch.ts&lt;/code&gt; is a reserved filename, so rename any existing file with that name.&lt;/p&gt;

&lt;h2&gt;
  
  
  Route caching and CDN providers
&lt;/h2&gt;

&lt;p&gt;Route caching is now stable. You configure it with &lt;code&gt;routeRules&lt;/code&gt; specifying &lt;code&gt;maxAge&lt;/code&gt; and &lt;code&gt;swr&lt;/code&gt; per route. Astro also ships experimental CDN cache providers for Netlify, Vercel, and Cloudflare, so cached responses live at the edge instead of in your origin's memory.&lt;/p&gt;

&lt;p&gt;For a docs site or a blog, this is straightforward. Set a &lt;code&gt;maxAge&lt;/code&gt; on your content routes, a shorter one with &lt;code&gt;swr&lt;/code&gt; for pages that update occasionally, and the framework handles invalidation. The CDN providers mean the cache survives deploys on those platforms, which is the part that was missing before.&lt;/p&gt;

&lt;h2&gt;
  
  
  What breaks when upgrading to Astro 7
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://docs.astro.build/en/guides/upgrade-to/v7/" rel="noopener noreferrer"&gt;migration guide&lt;/a&gt; lists the breaking changes. Here is what to expect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rust compiler is stricter.&lt;/strong&gt; Unclosed tags error. No more HTML auto-correction. Fix your markup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sätteri is the default Markdown processor.&lt;/strong&gt; If you depend on a specific remark or rehype plugin, install &lt;code&gt;@astrojs/markdown-remark&lt;/code&gt; to keep the unified pipeline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;compressHTML&lt;/code&gt; defaults to &lt;code&gt;jsx&lt;/code&gt;&lt;/strong&gt; instead of &lt;code&gt;true&lt;/code&gt;. Update your config if you relied on the old default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Experimental flags removed:&lt;/strong&gt; &lt;code&gt;rustCompiler&lt;/code&gt;, &lt;code&gt;queuedRendering&lt;/code&gt;, &lt;code&gt;advancedRouting&lt;/code&gt;, &lt;code&gt;cache&lt;/code&gt;, and &lt;code&gt;logger&lt;/code&gt;. Their behaviors are now standard or gone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;@astrojs/db&lt;/code&gt; is removed.&lt;/strong&gt; If you used the experimental database integration, you need a replacement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;src/fetch.ts&lt;/code&gt; is a reserved filename.&lt;/strong&gt; Rename any existing file with that name.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Container renderer imports changed&lt;/strong&gt; to the &lt;code&gt;/container-renderer&lt;/code&gt; entrypoint.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are also two AI-oriented additions: a background dev server mode for agents, and JSON logging output. Both are aimed at tooling that drives Astro programmatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should you upgrade to Astro 7?
&lt;/h2&gt;

&lt;p&gt;Yes, and soon if your site is content-heavy. The build speedup alone is worth it. The migration is mostly mechanical: remove the dead experimental flags, check your Markdown plugins, fix any markup the new compiler rejects.&lt;/p&gt;

&lt;p&gt;Do the upgrade in an isolated &lt;a href="https://morello.dev/blog/git-worktrees-are-underrated" rel="noopener noreferrer"&gt;git worktree&lt;/a&gt; so your main checkout keeps building while you shake out the Sätteri differences and the stricter compiler.&lt;/p&gt;

&lt;p&gt;The broader story is the one I keep writing about. The JavaScript ecosystem is rewriting its foundations in native code. &lt;a href="https://morello.dev/blog/typescript-7-is-here" rel="noopener noreferrer"&gt;TypeScript went to Go&lt;/a&gt;, &lt;a href="https://morello.dev/blog/pnpm-v12-rust-rewrite" rel="noopener noreferrer"&gt;pnpm went to Rust&lt;/a&gt;, Vite swapped its bundler for Rolldown. And now Astro has rewritten its compiler, its Markdown pipeline, and its rendering engine in Rust, all in one release. The tools you use every day are getting faster underneath you, and the surface you write against is staying still. That is the best kind of progress.&lt;/p&gt;

</description>
      <category>astro</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>rust</category>
    </item>
    <item>
      <title>Hermes Agent: A Beginner's Tutorial</title>
      <dc:creator>Dennis Morello</dc:creator>
      <pubDate>Sat, 18 Jul 2026 11:32:08 +0000</pubDate>
      <link>https://dev.to/morellodev/getting-started-with-hermes-agent-install-configure-write-a-skill-47pn</link>
      <guid>https://dev.to/morellodev/getting-started-with-hermes-agent-install-configure-write-a-skill-47pn</guid>
      <description>&lt;p&gt;You've probably used an AI coding assistant by now. Copilot finishes your lines, Cursor rewrites your functions, Claude Code debugs your PRs. They're all variations on the same idea: an AI that lives inside your editor and helps you write code faster.&lt;/p&gt;

&lt;p&gt;Hermes Agent is something different. It's not a copilot. It's an &lt;a href="https://github.com/NousResearch/hermes-agent" rel="noopener noreferrer"&gt;open-source autonomous agent&lt;/a&gt; built by &lt;a href="https://nousresearch.com" rel="noopener noreferrer"&gt;Nous Research&lt;/a&gt; that learns from its own experience, creates reusable skills on the fly, and runs wherever you put it: a cheap VPS, a home server, or serverless infrastructure that costs nearly nothing when idle. You talk to it from Telegram while it works on a cloud VM you never SSH into. It's &lt;a href="https://hermes-agent.nousresearch.com/docs" rel="noopener noreferrer"&gt;not tied to your laptop&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The project has 217,000 GitHub stars for a reason. Here's what it actually is, how to get started in under two minutes, and the first three things worth trying.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Hermes Agent actually is
&lt;/h2&gt;

&lt;p&gt;Hermes Agent is an autonomous agent with a &lt;a href="https://hermes-agent.nousresearch.com/docs" rel="noopener noreferrer"&gt;built-in learning loop&lt;/a&gt;. The loop works like this: every time you and Hermes accomplish something non-trivial, like fixing a tricky bug, configuring a deployment pipeline, or wiring up a new tool. It can save that procedure as a &lt;strong&gt;skill&lt;/strong&gt;. A skill is just a markdown file with instructions. Next time a similar task comes up, Hermes loads the skill and follows the playbook instead of figuring it out from scratch.&lt;/p&gt;

&lt;p&gt;Skills get better with use. If a skill's instructions are outdated or missing a step, Hermes patches them on the spot. Over time, you accumulate a library of battle-tested procedures that compound. This is the thing that separates Hermes from a chatbot. It doesn't just answer questions, it gets more capable the longer it runs.&lt;/p&gt;

&lt;p&gt;Under the hood, it's an MIT-licensed Python project that talks to any LLM provider you point it at. &lt;a href="https://hermes-agent.nousresearch.com/docs/integrations/providers/" rel="noopener noreferrer"&gt;Over 20 are supported&lt;/a&gt;, including OpenRouter, Anthropic, OpenAI, Google, DeepSeek, and local models. The recommended setup is through &lt;a href="https://portal.nousresearch.com" rel="noopener noreferrer"&gt;Nous Portal&lt;/a&gt;, which gives you one OAuth login, one bill, and access to 300+ models plus built-in tools for web search, image generation, text-to-speech, and browser automation. But you can also bring your own API keys if you prefer.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to install it
&lt;/h2&gt;

&lt;p&gt;The installer handles everything. On macOS, Linux, WSL2, or Android via Termux:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://hermes-agent.nousresearch.com/install.sh | bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Windows, in PowerShell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;iex&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;irm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;https://hermes-agent.nousresearch.com/install.ps1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The installer pulls in Python, Node.js, ripgrep, ffmpeg, and everything else Hermes needs. It clones the repo, sets up a virtual environment, and wires up the &lt;code&gt;hermes&lt;/code&gt; command globally. The whole thing takes &lt;a href="https://hermes-agent.nousresearch.com/docs/getting-started/installation" rel="noopener noreferrer"&gt;under two minutes&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;After installing, reload your shell and run the fastest path to a working agent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;hermes setup &lt;span class="nt"&gt;--portal&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This opens a browser for OAuth. One login covers your model access plus all four Tool Gateway tools: web search, image generation, text-to-speech, and a cloud browser. No API keys to juggle. Once that's done, just type &lt;code&gt;hermes&lt;/code&gt; and you're chatting.&lt;/p&gt;

&lt;p&gt;There's also a &lt;a href="https://hermes-agent.nousresearch.com/docs/getting-started/installation" rel="noopener noreferrer"&gt;native desktop app&lt;/a&gt; for macOS, Linux, and Windows. Launch it with &lt;code&gt;hermes desktop&lt;/code&gt;. But the CLI is where most people start.&lt;/p&gt;

&lt;h2&gt;
  
  
  First thing to try: write a skill from experience
&lt;/h2&gt;

&lt;p&gt;Skills are where Hermes actually earns its keep. Here's the fastest way to see it work.&lt;/p&gt;

&lt;p&gt;Do something non-trivial with Hermes. Ask it to set up a project, configure a tool, or debug something that takes a few turns. When it succeeds, Hermes will offer to save the approach as a skill. Say yes. That skill now lives in &lt;code&gt;~/.hermes/skills/&lt;/code&gt; as a markdown file with instructions, pitfalls, and a verification checklist.&lt;/p&gt;

&lt;p&gt;Next time you (or anyone else using that Hermes instance) hits the same kind of task, Hermes loads the skill and follows the playbook. The skill gets patched when it's wrong and sharpened when it's vague. After a few weeks of regular use, you stop repeating yourself. Hermes remembers how your projects are structured, which linter you prefer, and how to run your tests.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://hermes-agent.nousresearch.com/docs/skills" rel="noopener noreferrer"&gt;Skills Hub&lt;/a&gt; has 88,000+ community skills you can install too, covering everything from Apple Notes management to LLM fine-tuning. Every installed skill becomes a slash command, like &lt;code&gt;/gif-search funny cats&lt;/code&gt; or &lt;code&gt;/axolotl fine-tune Llama 3&lt;/code&gt;. Hermes loads the skill's instructions on demand, so you're not burning tokens on stuff you're not using.&lt;/p&gt;

&lt;h2&gt;
  
  
  Second thing: hook up a messaging platform
&lt;/h2&gt;

&lt;p&gt;Hermes is designed to be something you talk to from anywhere, not something you SSH into. After &lt;code&gt;hermes setup --portal&lt;/code&gt;, configure a messaging gateway:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;hermes gateway setup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It walks you through connecting Telegram, Discord, Slack, WhatsApp, Signal, or any of the &lt;a href="https://hermes-agent.nousresearch.com/docs" rel="noopener noreferrer"&gt;20+ supported platforms&lt;/a&gt;. Once it's wired up, you message Hermes from your phone while it crunches through a long-running task on a server somewhere. It notifies you when it's done. This is the moment where Hermes stops feeling like a terminal tool and starts feeling like an agent you keep around.&lt;/p&gt;

&lt;h2&gt;
  
  
  Third thing: spawn subagents for parallel work
&lt;/h2&gt;

&lt;p&gt;Hermes can delegate work to isolated subagents that run in parallel with their own context windows. This is useful when you have independent tasks that don't need to share state. Research a topic while a different subagent lints your code, or check three different APIs at once.&lt;/p&gt;

&lt;p&gt;From a conversation, just ask Hermes to "research X and Y in parallel" and it spawns the subagents. They report back when they're done. It's a simple model, but it's the kind of thing that makes multi-step workflows feel fast instead of sequential.&lt;/p&gt;

&lt;h2&gt;
  
  
  What makes it different from Copilot, Cursor, and Claude Code
&lt;/h2&gt;

&lt;p&gt;Those tools are IDE copilots. They help you write code faster inside your editor. Hermes is an autonomous agent that lives wherever you deploy it. The table version:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Copilot / Cursor / Claude Code&lt;/th&gt;
&lt;th&gt;Hermes Agent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Where it lives&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Your editor&lt;/td&gt;
&lt;td&gt;Anywhere (VPS, server, Modal, Daytona)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;What it does&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Helps you write code&lt;/td&gt;
&lt;td&gt;Does work autonomously, learns from it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;How you talk to it&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;IDE chat panel&lt;/td&gt;
&lt;td&gt;CLI, Telegram, Discord, Slack, 20+ platforms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Memory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Per-session context&lt;/td&gt;
&lt;td&gt;Persistent, self-improving across sessions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Skills&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Creates, patches, and reuses procedures&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Scheduling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Built-in cron with any-platform delivery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;License&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Proprietary&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you want faster autocomplete, use Copilot. If you want an agent that learns your stack, runs on your infrastructure, and gets more useful the longer it's around, Hermes is the thing to try.&lt;/p&gt;

&lt;p&gt;I've written before about &lt;a href="https://morello.dev/blog/configuring-my-site-for-ai-discoverability" rel="noopener noreferrer"&gt;making sites readable by AI agents&lt;/a&gt; and &lt;a href="https://morello.dev/blog/webmcp-making-your-site-usable-by-ai-agents" rel="noopener noreferrer"&gt;making them usable by agents with WebMCP&lt;/a&gt;. Hermes is the other side of that equation. It's the agent that can actually consume and act on that content. Install it, teach it one thing, and watch it get better.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Cross-Origin Storage API: Stop Downloading Twice</title>
      <dc:creator>Dennis Morello</dc:creator>
      <pubDate>Fri, 17 Jul 2026 19:29:33 +0000</pubDate>
      <link>https://dev.to/morellodev/cross-origin-storage-api-stop-downloading-files-twice-56kb</link>
      <guid>https://dev.to/morellodev/cross-origin-storage-api-stop-downloading-files-twice-56kb</guid>
      <description>&lt;p&gt;There was a time when loading React from a public CDN was a real performance trick. If enough sites pulled the same &lt;code&gt;react.production.min.js&lt;/code&gt; off the same CDN URL, a first-time visitor to your site stood a good chance of already having it cached from some other site they'd visited. One download, reused everywhere. That was the whole pitch for shared CDNs.&lt;/p&gt;

&lt;p&gt;Browsers took it away on purpose. To stop sites from using the cache as a &lt;a href="https://developer.chrome.com/blog/http-cache-partitioning" rel="noopener noreferrer"&gt;cross-site tracking signal&lt;/a&gt;, they started partitioning the HTTP cache by top-level site. The attack they were closing: you can detect whether someone visited another site by timing whether a shared resource loads from cache. Chrome partitioned in version 86 in October 2020, Firefox in 85, Safari years before either. Now the same file at the same URL gets downloaded and stored separately for every site that uses it, and the old "just use a public CDN" advice &lt;a href="https://addyosmani.com/blog/double-keyed-caching/" rel="noopener noreferrer"&gt;mostly stopped making sense&lt;/a&gt;. The &lt;a href="https://wicg.github.io/cross-origin-storage/" rel="noopener noreferrer"&gt;Cross-Origin Storage (COS) API&lt;/a&gt; is a WICG proposal to bring that shared cache back.&lt;/p&gt;

&lt;p&gt;Its trick is to change what the shared cache is keyed on. Rather than the URL, which is what leaked, COS keys the file by its cryptographic hash, and only lets you ask for a file whose exact bytes you already know. That closes the tracking hole the old shared cache opened. One copy of React, or a web font, or a big WebAssembly binary, shared across every origin that wants it.&lt;/p&gt;

&lt;p&gt;It's early. Nothing ships it natively yet. But the problem it solves is real and getting worse, so it's worth understanding now.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is the Cross-Origin Storage API?
&lt;/h2&gt;

&lt;p&gt;Cross-Origin Storage is a proposed browser API for storing and retrieving large files by their &lt;strong&gt;content hash&lt;/strong&gt; rather than their URL, in a store that can be shared across origins with the user's device acting as the shared cache. A file is identified by its SHA-256 digest, so the same bytes fetched by two different sites from two different URLs map to a single entry on disk.&lt;/p&gt;

&lt;p&gt;The whole surface is one method on a new &lt;code&gt;navigator.crossOriginStorage&lt;/code&gt; interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;requestFileHandle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It returns a &lt;code&gt;Promise&amp;lt;FileSystemFileHandle&amp;gt;&lt;/code&gt;, the same handle type as the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/File_System_API" rel="noopener noreferrer"&gt;File System API&lt;/a&gt;, so once you have it you read the file exactly the way you already know how. There's no bespoke &lt;code&gt;store()&lt;/code&gt;/&lt;code&gt;retrieve()&lt;/code&gt; pair to learn; &lt;code&gt;requestFileHandle()&lt;/code&gt; covers both directions, and the &lt;code&gt;create&lt;/code&gt; flag decides which.&lt;/p&gt;

&lt;p&gt;The proposal comes from Googlers &lt;a href="https://blog.tomayac.com/" rel="noopener noreferrer"&gt;Thomas Steiner&lt;/a&gt; and François Beaufort, together with &lt;a href="https://christianliebel.com/" rel="noopener noreferrer"&gt;Christian Liebel&lt;/a&gt; of Thinktecture, and it's still at the incubation stage. The motivating cases are the files a lot of sites already share byte-for-byte: JavaScript libraries, web fonts, WebAssembly modules, game engines. A font served by Google Fonts gets requested by thousands of sites; store it once and any of them could read it locally instead of fetching it from a CDN on every visit.&lt;/p&gt;

&lt;p&gt;The problem gets extreme with AI models. The explainer's headline example is a single 8 GB model that two origins both need, which without COS means 16 GB downloaded and 16 GB sitting on disk for one file. The size list runs up from there: Gemma 2 at 1.35 GB, Llama-3.1-70B at 33 GB. That's the group feeling the most pain, but the mechanism is the same one that would let two sites share a copy of React.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why can't the Cache API or IndexedDB share files across origins?
&lt;/h2&gt;

&lt;p&gt;Because every existing storage mechanism is partitioned by origin, and every one of them is addressed by URL or key rather than by content. Those two facts are the whole problem.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Store&lt;/th&gt;
&lt;th&gt;Addressed by&lt;/th&gt;
&lt;th&gt;Shared across origins?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cache API&lt;/td&gt;
&lt;td&gt;URL&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IndexedDB&lt;/td&gt;
&lt;td&gt;key&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Origin Private File System&lt;/td&gt;
&lt;td&gt;path&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cross-Origin Storage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;content hash&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes, within a declared scope&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The Cache API keys entries on the request URL, so the same copy of React served from &lt;code&gt;cdn-a.com/react.js&lt;/code&gt; and &lt;code&gt;cdn-b.com/react.js&lt;/code&gt; is two unrelated entries even if the bytes are identical, and each origin gets its own private Cache, so even the &lt;em&gt;same&lt;/em&gt; URL is a separate entry per site. IndexedDB and the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system" rel="noopener noreferrer"&gt;Origin Private File System&lt;/a&gt; are both walled inside a single origin by design. None of them can say "I don't care where this came from, I care that it hashes to &lt;code&gt;8f43…&lt;/code&gt;". Content addressing is the one thing that makes cross-origin sharing safe to attempt, because you can only ask for a file whose exact bytes you already know.&lt;/p&gt;

&lt;p&gt;COS is explicitly &lt;em&gt;not&lt;/em&gt; trying to replace any of them. It's a fourth thing for a narrow case: big files that lots of sites legitimately share.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does requestFileHandle() read and store files?
&lt;/h2&gt;

&lt;p&gt;You compute the file's SHA-256 hash, ask for a handle, and read it. Retrieving a file another origin already stored looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;algorithm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SHA-256&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;8f434346648f6b96df89dda901c5176b10a6d83961dd3c1ac88b59b2dc327aa4&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;crossOriginStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requestFileHandle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hash&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;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getFile&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the file isn't there, you fetch it the normal way and store it, passing &lt;code&gt;create: true&lt;/code&gt; and the origins you're willing to share it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;crossOriginStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requestFileHandle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;origins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://example.org&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;writable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createWritable&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;writable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fileBlob&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;writable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;hash&lt;/code&gt; argument is a dictionary of &lt;code&gt;{ algorithm, value }&lt;/code&gt;, where &lt;code&gt;value&lt;/code&gt; is a 64-character lowercase hex digest. SHA-256 is the algorithm the spec builds around. On write, the browser verifies the bytes you hand it actually hash to the value you claimed and throws a &lt;code&gt;DataError&lt;/code&gt; if they don't, so a stored file can never lie about its own identity. That verification is what lets a &lt;em&gt;different&lt;/em&gt; origin trust the entry later without re-downloading it.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;origins&lt;/code&gt; option is the access-control knob. Omit it and the file is scoped to your own site. Pass an explicit list and only those origins can see it. Pass &lt;code&gt;"*"&lt;/code&gt; and you're offering it to the whole web, which is where the privacy design gets interesting.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does Cross-Origin Storage avoid becoming a supercookie?
&lt;/h2&gt;

&lt;p&gt;This is the question the whole design orbits, because a naive "do you have file X?" lookup is a cross-site tracking primitive. If a rare file were stored by exactly one obscure origin, any other site that could confirm its presence would learn you'd visited that origin. Content addressing alone doesn't save you here; the &lt;em&gt;answer&lt;/em&gt; to the query is the leak.&lt;/p&gt;

&lt;p&gt;COS defends against it with layered gating rather than a permission prompt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You can't enumerate.&lt;/strong&gt; There's no "list what's stored" call. As the spec puts it, developers "cannot enumerate the contents of Cross-Origin Storage or access a file without already knowing its hash." You can only probe for bytes you already have.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Global files need a crowd.&lt;/strong&gt; A &lt;code&gt;"*"&lt;/code&gt;-scoped file's presence is only confirmable to an outside origin if its hash is on the &lt;strong&gt;Public Hash List&lt;/strong&gt;, a registry a file joins only after clearing a k-anonymity-style popularity bar, appearing byte-identical across a minimum number of independent origins (the reference &lt;a href="https://github.com/tomayac/public-hash-list" rel="noopener noreferrer"&gt;public-hash-list&lt;/a&gt; currently uses roughly 100). If a file is popular enough that "you have it" tells an attacker nothing about you specifically, sharing its presence is safe. If it's rare, the browser refuses to confirm.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The browser lies on purpose.&lt;/strong&gt; For files where the presence signal would be sensitive, the user agent may apply &lt;em&gt;GREASE'ing&lt;/em&gt;: "occasionally responding as if a disclosable entry were absent, even though [gating] would otherwise permit disclosure," adding noise so a site can't distinguish a real miss from a privacy-motivated one. There's a nice pragmatic carve-out: the browser won't GREASE gigabyte-scale weights, since forcing a spurious multi-gigabyte re-download to protect a signal nobody's mining is a bad trade.&lt;/p&gt;

&lt;p&gt;The upshot is that a &lt;code&gt;NotFoundError&lt;/code&gt; is deliberately ambiguous. It might mean the file isn't stored, or that you're out of scope, or that the hash isn't on the Public Hash List, or that the browser just decided not to tell you. The spec is explicit that it "does not prove the file is physically absent."&lt;/p&gt;

&lt;p&gt;One thing that surprised me, and corrects a lot of secondhand write-ups: &lt;strong&gt;there's no per-file permission prompt, and no human-readable hash shown to the user.&lt;/strong&gt; Earlier tellings of this idea imagined a dialog where you'd confirm a file by some readable fingerprint. The current design dropped that. A handle returned from COS is already fully authorized, so calling &lt;code&gt;getFile()&lt;/code&gt; or &lt;code&gt;createWritable()&lt;/code&gt; "never triggers an additional permission prompt." User control lives in settings UI for inspecting and evicting stored files, not in an interstitial on every download.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does the declarative crossoriginstorage attribute work?
&lt;/h2&gt;

&lt;p&gt;Most sites shouldn't have to hash their own script tags, so the proposal also sketches a declarative path where the browser does the COS lookup for you. It leans on the &lt;code&gt;integrity&lt;/code&gt; attribute you may already use for &lt;a href="https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity" rel="noopener noreferrer"&gt;Subresource Integrity&lt;/a&gt; (the same SHA-256 digest COS keys on), plus a new &lt;code&gt;crossoriginstorage&lt;/code&gt; attribute:&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;script
  &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"popular-js-framework.js"&lt;/span&gt;
  &lt;span class="na"&gt;integrity=&lt;/span&gt;&lt;span class="s"&gt;"sha256-def456..."&lt;/span&gt;
  &lt;span class="na"&gt;crossoriginstorage=&lt;/span&gt;&lt;span class="s"&gt;"*"&lt;/span&gt;
&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same idea is floated for JavaScript import attributes and a &lt;code&gt;cross-origin-storage()&lt;/code&gt; modifier in CSS &lt;code&gt;url()&lt;/code&gt;. These aren't defined in the COS spec itself; each one has to land in its own host language's spec (HTML, TC39, CSS), so treat them as direction, not API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is the Cross-Origin Storage API supported in browsers yet?
&lt;/h2&gt;

&lt;p&gt;No. Cross-Origin Storage is not implemented in any browser, there's no origin trial, and there's no flag to flip. Emscripten's own COS docs say it plainly: the API "has not yet shipped in any browser." It is not Baseline, and it isn't close.&lt;/p&gt;

&lt;p&gt;What exists is experimentation around the edges. There's a &lt;a href="https://github.com/web-ai-community/cross-origin-storage-extension" rel="noopener noreferrer"&gt;Chrome extension that polyfills the API&lt;/a&gt; so libraries can develop against it, and the AI-in-the-browser crowd is already wiring in opt-in support: &lt;a href="https://huggingface.co/blog/cross-origin-storage" rel="noopener noreferrer"&gt;Transformers.js&lt;/a&gt; gates it behind an &lt;code&gt;experimental_useCrossOriginStorage&lt;/code&gt; flag, with WebLLM and wllama experimenting too. That's the natural first constituency: a 33 GB model you download once and reuse across every site that runs it is a far better story than downloading it per origin.&lt;/p&gt;

&lt;p&gt;On the standards side it's genuinely early. Mozilla opened a &lt;a href="https://github.com/mozilla/standards-positions/issues/1427" rel="noopener noreferrer"&gt;standards-position issue&lt;/a&gt; on June 22, 2026, but hasn't taken a formal stance yet; I couldn't find a WebKit position or a W3C TAG review at all. So this is one browser's proposal with no committed implementers.&lt;/p&gt;

&lt;p&gt;A quick disambiguation while you're searching, because the names collide: Cross-Origin &lt;em&gt;Storage&lt;/em&gt; is not the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Storage_Access_API" rel="noopener noreferrer"&gt;Storage Access API&lt;/a&gt;. That one is about letting embedded third-party content reach its own cookies. COS is about sharing large content-addressed files. Different problem, confusingly adjacent name.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should you use the Cross-Origin Storage API?
&lt;/h2&gt;

&lt;p&gt;Not enough to write code against it, but enough to track it. The waste it targets only compounds: every site now pays full freight for its own copy of the same shared libraries and fonts since the cache got partitioned, and in-browser AI keeps pushing file sizes into the gigabytes, where it really starts to hurt. A content-addressed cache shared across origins is the obvious fix, and the interesting engineering is entirely in making it &lt;em&gt;safe&lt;/em&gt; rather than making it work.&lt;/p&gt;

&lt;p&gt;It also fits a pattern I keep noticing in these platform proposals, the same one behind &lt;a href="https://morello.dev/blog/webmcp-making-your-site-usable-by-ai-agents" rel="noopener noreferrer"&gt;WebMCP&lt;/a&gt; and the &lt;a href="https://morello.dev/blog/the-new-http-query-method" rel="noopener noreferrer"&gt;HTTP QUERY method&lt;/a&gt;: the web already had the raw capability, and the standards work is mostly about giving it a name and a safety model. COS is that for deduplicated storage. There's a read-side version of this too, which I covered in &lt;a href="https://morello.dev/blog/configuring-my-site-for-ai-discoverability" rel="noopener noreferrer"&gt;getting a site ready for AI readers&lt;/a&gt;: that post is about the content agents consume, this one is about the model weights they run. Whether it ships as-is, gets filed down by the standards process, or stalls, the shape of the answer looks right. I'd rather understand it now than the day a library flips it on by default.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>ai</category>
      <category>performance</category>
    </item>
    <item>
      <title>SolidJS 2.0 Async Data: A Deep Dive for React Devs</title>
      <dc:creator>Dennis Morello</dc:creator>
      <pubDate>Thu, 16 Jul 2026 07:40:00 +0000</pubDate>
      <link>https://dev.to/morellodev/solidjs-20-async-data-a-deep-dive-for-react-devs-1dpg</link>
      <guid>https://dev.to/morellodev/solidjs-20-async-data-a-deep-dive-for-react-devs-1dpg</guid>
      <description>&lt;p&gt;In &lt;a href="https://morello.dev/blog/solidjs-2-react-developers-first-look" rel="noopener noreferrer"&gt;my first look at SolidJS 2.0&lt;/a&gt; I said first-class async was the real story, then barely opened the box. This post opens it.&lt;/p&gt;

&lt;p&gt;Here's the short version. In Solid 2.0 a promise is just another value the reactive graph knows how to wait for. A computation can return one, anything that reads it suspends until it resolves, and a &lt;code&gt;&amp;lt;Loading&amp;gt;&lt;/code&gt; boundary shows a fallback only until the first real value lands. There's no &lt;code&gt;createResource&lt;/code&gt;, no manual &lt;code&gt;await&lt;/code&gt; in your components, and no re-render. If you write React, this is the part that behaves least like what you're used to, so it's worth slowing down on.&lt;/p&gt;

&lt;p&gt;Everything below is from the &lt;code&gt;2.0.0-beta.18&lt;/code&gt; line (&lt;code&gt;npm install solid-js@next&lt;/code&gt;). It's a beta, and some of these names will still move before stable, so I've flagged the parts that are rougher.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does async data fetching work in Solid 2.0?
&lt;/h2&gt;

&lt;p&gt;A derived computation can return a promise, and the reactive graph treats the pending state as "not ready" until it settles. &lt;code&gt;createResource&lt;/code&gt; is gone; a plain &lt;code&gt;createMemo&lt;/code&gt; that returns a promise does the job, as the &lt;a href="https://github.com/solidjs/solid/blob/next/documentation/solid-2.0/05-async-data.md" rel="noopener noreferrer"&gt;async RFC&lt;/a&gt; puts it: async is "a first-class capability of computations."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Solid 1.x&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;user&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createResource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fetchUser&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Solid 2.0: the memo returns a promise, the graph waits on it&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createMemo&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;fetchUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read &lt;code&gt;user()&lt;/code&gt; anywhere and you get the resolved value. While the promise is in flight the read throws a &lt;code&gt;NotReadyError&lt;/code&gt; internally, which propagates up the graph to the nearest boundary rather than crashing. You never catch it yourself; it's the mechanism that lets &lt;code&gt;&amp;lt;Loading&amp;gt;&lt;/code&gt; and error boundaries work without a dedicated suspense primitive.&lt;/p&gt;

&lt;p&gt;When &lt;code&gt;userId()&lt;/code&gt; changes, the memo re-runs, returns a new promise, and the graph waits again. Same accessor, no dependency array, no effect wired up to refetch.&lt;/p&gt;

&lt;p&gt;If you need the value imperatively outside a reactive scope (a route loader, an event handler), &lt;code&gt;resolve&lt;/code&gt; hands you a real promise that settles once the expression is ready:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;currentUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;resolve&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;user&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One correction to a thing you'll see repeated: &lt;strong&gt;there is no &lt;code&gt;createAsync&lt;/code&gt; in core 2.0.&lt;/strong&gt; &lt;code&gt;createAsync&lt;/code&gt; is a &lt;code&gt;@solidjs/router&lt;/code&gt; API, and its 2.0 shape isn't settled in the sources I could find, so don't reach for it as the core data primitive. The core primitive is the async &lt;code&gt;createMemo&lt;/code&gt; above.&lt;/p&gt;

&lt;h2&gt;
  
  
  What replaced &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt;? The &lt;code&gt;&amp;lt;Loading&amp;gt;&lt;/code&gt; boundary
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;&amp;lt;Loading&amp;gt;&lt;/code&gt; is the direct replacement for 1.x's &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt;, and the semantics are the reason the release was titled &lt;a href="https://github.com/solidjs/solid/releases/tag/v2.0.0-beta.0" rel="noopener noreferrer"&gt;"The &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; is Over."&lt;/a&gt; &lt;code&gt;&amp;lt;Loading&amp;gt;&lt;/code&gt; covers &lt;strong&gt;initial readiness only&lt;/strong&gt;. It shows its fallback while the subtree has nothing to render yet, and once real content is on screen it gets out of the way.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Loading&lt;/span&gt; &lt;span class="na"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Spinner&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Profile&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;user&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Loading&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the behavior React's Suspense makes you fight for. In React, when a value a Suspense boundary depends on changes, the boundary can rip the rendered content down and flash the fallback again unless you wrap the update in &lt;a href="https://react.dev/reference/react/useTransition" rel="noopener noreferrer"&gt;&lt;code&gt;useTransition&lt;/code&gt;&lt;/a&gt;. That's also why router-driven navigations, which wrap their updates in transitions by default, keep the stale content visible instead and need a &lt;a href="https://react.dev/reference/react/Suspense#resetting-suspense-boundaries-on-navigation" rel="noopener noreferrer"&gt;&lt;code&gt;key&lt;/code&gt;&lt;/a&gt; to reset the boundary. Solid inverts the default: after the first paint, a refetch holds the old content on screen. It won't kick you back to the spinner.&lt;/p&gt;

&lt;p&gt;When you do want the fallback to reappear on a specific change (say the whole record is being swapped, not refreshed), the new &lt;code&gt;on&lt;/code&gt; prop opts into that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Loading&lt;/span&gt; &lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Spinner&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Profile&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;user&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Loading&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now a change to &lt;code&gt;userId()&lt;/code&gt; while data is pending re-shows the fallback; other pending work doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you coordinate several boundaries? &lt;code&gt;&amp;lt;Reveal&amp;gt;&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;&amp;lt;Reveal&amp;gt;&lt;/code&gt; replaces 1.x's &lt;code&gt;&amp;lt;SuspenseList&amp;gt;&lt;/code&gt; and controls the order in which sibling &lt;code&gt;&amp;lt;Loading&amp;gt;&lt;/code&gt; boundaries reveal their content. It takes an &lt;code&gt;order&lt;/code&gt; prop of &lt;code&gt;"sequential"&lt;/code&gt; (the default), &lt;code&gt;"together"&lt;/code&gt;, or &lt;code&gt;"natural"&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;sequential&lt;/code&gt; reveals boundaries in DOM order; later ones stay on their fallbacks until every earlier one has resolved.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;together&lt;/code&gt; holds every fallback until the whole group is ready, then reveals at once.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;natural&lt;/code&gt; lets each boundary reveal as soon as its own data lands.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's also a &lt;code&gt;collapsed&lt;/code&gt; boolean, consulted only under &lt;code&gt;order="sequential"&lt;/code&gt;, that renders a single frontier fallback instead of one per boundary. The &lt;a href="https://github.com/solidjs/solid/blob/next/documentation/solid-2.0/03-control-flow.md" rel="noopener noreferrer"&gt;control-flow RFC&lt;/a&gt; is the only source documenting the fine-grained nesting rules here, so treat the exact &lt;code&gt;&amp;lt;Reveal&amp;gt;&lt;/code&gt; semantics as provisional until stable.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you show a refresh without a spinner? &lt;code&gt;isPending&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;isPending&lt;/code&gt; reports that a change to a specific read is in flight, so you can show a subtle indicator while the stale content stays put. It takes a thunk, and it actually performs the read, so where you place it matters.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;refreshing&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;isPending&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;user&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Loading&lt;/span&gt; &lt;span class="na"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Spinner&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Show&lt;/span&gt; &lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;refreshing&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;RefreshBar&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Show&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Profile&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;user&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Loading&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First load hits &lt;code&gt;&amp;lt;Loading&amp;gt;&lt;/code&gt; and shows the spinner. A later refetch keeps &lt;code&gt;&amp;lt;Profile&amp;gt;&lt;/code&gt; visible and flips &lt;code&gt;refreshing()&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt;, so you render a thin bar instead of tearing the page down. Stale-while-revalidate comes built into the primitive, so you don't assemble it yourself.&lt;/p&gt;

&lt;p&gt;The gotcha that will catch you: &lt;strong&gt;a bare &lt;code&gt;refresh()&lt;/code&gt; reads as not pending.&lt;/strong&gt; Re-asking the same question (a poll, a manual refresh, a confirming refetch after a mutation) is treated as silent by design. If you want that reload to register as pending, you declare it with &lt;code&gt;affects&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Silent: isPending stays false, no refresh UI&lt;/span&gt;
&lt;span class="nf"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Declared: now the refresh reads as pending&lt;/span&gt;
&lt;span class="nf"&gt;affects&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;affects&lt;/code&gt; is one of the newer, rougher corners of the API (it lives in a single RFC family and is likely to move), but the underlying rule is worth knowing now: pending is about a &lt;em&gt;changed input&lt;/em&gt;, not about work happening. There's also a &lt;code&gt;latest(fn)&lt;/code&gt; helper that peeks at the in-flight value during a transition and falls back to stale if the next value isn't ready, for when you want to show the incoming id before its data lands.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where do mutations live now? &lt;code&gt;action&lt;/code&gt; and optimistic updates
&lt;/h2&gt;

&lt;p&gt;Solid 2.0 gives writes a home in core with &lt;code&gt;action&lt;/code&gt;, and the shape is unusual: &lt;code&gt;action&lt;/code&gt; wraps a &lt;strong&gt;generator&lt;/strong&gt;. Each &lt;code&gt;yield&lt;/code&gt; is a point where the action awaits, which lets the reactive system track an optimistic value until the real one lands.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;todos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setTodos&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createOptimisticStore&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;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTodos&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;addTodo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;setTodos&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="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="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// optimistic: show it immediately&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addTodo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// await the server&lt;/span&gt;
  &lt;span class="nf"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;todos&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// reconcile with the source of truth&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;createOptimistic&lt;/code&gt; has the same surface as &lt;code&gt;createSignal&lt;/code&gt;, but its writes are optimistic: they can be overridden during a transition and revert when the transition completes. &lt;code&gt;createOptimisticStore&lt;/code&gt; is the store version. The RFCs document it as &lt;code&gt;createOptimisticStore(fnOrValue, seed, options?)&lt;/code&gt; while the &lt;a href="https://github.com/solidjs/solid/blob/next/documentation/solid-2.0/MIGRATION.md" rel="noopener noreferrer"&gt;migration guide&lt;/a&gt; shows a single-object form, so the exact arity is still settling; the two-argument derived form above is the one that's fully specified.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;refresh(x)&lt;/code&gt; asks a derived signal or store to recompute. It's imperative revalidation, not a piece of UI state, which is why the &lt;code&gt;affects&lt;/code&gt; dance above exists for the cases where you want a refresh to be visible. An async-generator form of &lt;code&gt;action&lt;/code&gt; also works, where a bare &lt;code&gt;yield;&lt;/code&gt; resumes the action in the same transition context after an &lt;code&gt;await&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you've used React 19's &lt;a href="https://react.dev/reference/react/useOptimistic" rel="noopener noreferrer"&gt;Actions and &lt;code&gt;useOptimistic&lt;/code&gt;&lt;/a&gt;, the intent is familiar. The difference is that Solid folds the optimistic write, the server call, and the revalidation into one generator instead of splitting them across a hook and an action.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why is batching deterministic now?
&lt;/h2&gt;

&lt;p&gt;Solid 2.0 batches updates on a microtask by default, so a setter queues the write and reads don't reflect it until the batch flushes. &lt;code&gt;batch()&lt;/code&gt; is removed; &lt;code&gt;flush()&lt;/code&gt; is how you apply pending updates synchronously when you need the result right away.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setCount&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createSignal&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="nf"&gt;setCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// still 0: queued on the microtask&lt;/span&gt;
&lt;span class="nf"&gt;flush&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// 1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I mentioned this in the first post as a gotcha; the reason it matters here is that it's what makes async reliable. Running the tracking (compute) half of every effect before any side-effecting half gives the graph a complete dependency picture before anything runs, which is exactly what &lt;code&gt;&amp;lt;Loading&amp;gt;&lt;/code&gt; and error boundaries need to decide what's ready. &lt;code&gt;flush(fn)&lt;/code&gt; also takes a callback and drains the writes inside it before returning, preserving the return value. Coming from React's synchronous batching it's an adjustment, but the model is predictable once async is threaded through everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed for transitions? They're built in
&lt;/h2&gt;

&lt;p&gt;This one's aimed squarely at React developers: &lt;strong&gt;&lt;code&gt;startTransition&lt;/code&gt; and &lt;code&gt;useTransition&lt;/code&gt; are gone.&lt;/strong&gt; Solid 2.0 treats transitions as a core scheduling concept, and multiple can be in flight at once. Pending UI is expressed through &lt;code&gt;&amp;lt;Loading&amp;gt;&lt;/code&gt; and &lt;code&gt;isPending&lt;/code&gt; rather than by wrapping updates. You don't opt in per call site; the framework does the bookkeeping.&lt;/p&gt;

&lt;p&gt;Here's the async surface mapped against React 19:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;React 19&lt;/th&gt;
&lt;th&gt;Solid 2.0&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fetch data&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;use()&lt;/code&gt; + Suspense, or a data library&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;createMemo&lt;/code&gt; returning a promise&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Loading UI&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;&amp;lt;Suspense fallback&amp;gt;&lt;/code&gt; (can re-trigger)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;&amp;lt;Loading&amp;gt;&lt;/code&gt; (initial readiness only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Refresh without a flash&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;useTransition&lt;/code&gt; + &lt;code&gt;isPending&lt;/code&gt;, opt-in per call&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;isPending(fn)&lt;/code&gt;, observe any read&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Optimistic update&lt;/td&gt;
&lt;td&gt;&lt;code&gt;useOptimistic&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;createOptimistic&lt;/code&gt; / &lt;code&gt;createOptimisticStore&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mutations&lt;/td&gt;
&lt;td&gt;Actions / form actions&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;action(function*)&lt;/code&gt; + &lt;code&gt;refresh&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Batching&lt;/td&gt;
&lt;td&gt;automatic sync batching&lt;/td&gt;
&lt;td&gt;microtask batching + &lt;code&gt;flush()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The pattern across that table: React exposes each capability as a hook you opt into, while Solid pushes the distinction between "nothing to show" and "refreshing what's shown" down into the primitives, so you get the good behavior by default. That reactivity model is the same shift toward &lt;a href="https://morello.dev/blog/five-things-you-might-not-know-about-javascript" rel="noopener noreferrer"&gt;signals happening across the whole ecosystem&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should you build on this yet?
&lt;/h2&gt;

&lt;p&gt;Not in production. It's &lt;code&gt;beta.18&lt;/code&gt;, the same early-but-real stage &lt;a href="https://morello.dev/blog/pnpm-v12-rust-rewrite" rel="noopener noreferrer"&gt;pnpm v12's Rust rewrite&lt;/a&gt; is in. The async core (async &lt;code&gt;createMemo&lt;/code&gt;, &lt;code&gt;&amp;lt;Loading&amp;gt;&lt;/code&gt;, microtask batching with &lt;code&gt;flush&lt;/code&gt;, generator &lt;code&gt;action&lt;/code&gt;s) is stable enough to learn on, but the edges (&lt;code&gt;affects&lt;/code&gt;, &lt;code&gt;createOptimisticStore&lt;/code&gt;'s arity, &lt;code&gt;&amp;lt;Reveal&amp;gt;&lt;/code&gt;'s nesting rules) are still moving. The announcement is worth reading in full in the &lt;a href="https://github.com/solidjs/solid/discussions/2596" rel="noopener noreferrer"&gt;beta.0 discussion&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;What I'd actually do, and what I'm doing: build a small data-heavy screen with &lt;code&gt;solid-js@next&lt;/code&gt;, wire up a &lt;code&gt;createMemo&lt;/code&gt; that fetches, drop a &lt;code&gt;&amp;lt;Loading&amp;gt;&lt;/code&gt; around it, and trigger a refetch. Watch the old content stay on screen while the new content loads. That single behavior is the clearest argument for why fine-grained reactivity was worth following all the way to async.&lt;/p&gt;

</description>
      <category>solidjs</category>
      <category>react</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>pnpm v12 Is Being Rewritten in Rust</title>
      <dc:creator>Dennis Morello</dc:creator>
      <pubDate>Wed, 15 Jul 2026 15:12:29 +0000</pubDate>
      <link>https://dev.to/morellodev/pnpm-v12-is-being-rewritten-in-rust-5036</link>
      <guid>https://dev.to/morellodev/pnpm-v12-is-being-rewritten-in-rust-5036</guid>
      <description>&lt;p&gt;Every package manager has a moment where you just wait. You add one dependency, or you pull a branch and run install, and there's a beat where the terminal sits there doing file I/O while you tab away. &lt;a href="https://pnpm.io" rel="noopener noreferrer"&gt;pnpm&lt;/a&gt; already made that beat shorter than most, staying ahead of npm and Yarn on most install scenarios thanks to its content-addressable store and hard-linked &lt;code&gt;node_modules&lt;/code&gt;. But it was still a Node.js program, paying the Node startup cost and pushing tens of thousands of file operations through a &lt;a href="https://morello.dev/blog/five-things-you-might-not-know-about-javascript" rel="noopener noreferrer"&gt;single JavaScript runtime&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;That is what changes in pnpm v12: it rewrites the install engine, the part that fetches and links packages, in &lt;a href="https://www.rust-lang.org" rel="noopener noreferrer"&gt;Rust&lt;/a&gt;, and leaves the CLI, lockfile, and &lt;code&gt;node_modules&lt;/code&gt; layout untouched. It's the same move &lt;a href="https://morello.dev/blog/typescript-7-is-here" rel="noopener noreferrer"&gt;TypeScript just made with Go&lt;/a&gt;, aimed at a different bottleneck. And like that one, the interesting detail is how little is supposed to change for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  What pnpm v12 actually is
&lt;/h2&gt;

&lt;p&gt;pnpm v12 is the same pnpm you already use, ported to Rust. pnpm has always been TypeScript running on Node.js, and the Rust work lives under the codename &lt;strong&gt;&lt;code&gt;pacquet&lt;/code&gt;&lt;/strong&gt;, whose repo describes it in one line: "The official pnpm rewrite in Rust." The key word there is &lt;em&gt;rewrite&lt;/em&gt;, not &lt;em&gt;reimagining&lt;/em&gt;. It's a port. The flags, defaults, error codes, lockfile format, and &lt;code&gt;node_modules&lt;/code&gt; layout are all meant to match pnpm exactly.&lt;/p&gt;

&lt;p&gt;Zoltan Kochan, pnpm's creator, &lt;a href="https://github.com/orgs/pnpm/discussions/11292" rel="noopener noreferrer"&gt;put it plainly&lt;/a&gt; when the v12 plan came together: "v12 should behave the same as v11, the biggest change will be the full rewrite to Rust." That's the frame to hold onto. This is a performance release wearing a major version number, not a redesign of how pnpm works.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pacquet&lt;/code&gt; started as a separate experimental repo and &lt;a href="https://x.com/zkochan/status/2054966567692099943" rel="noopener noreferrer"&gt;moved into the main pnpm monorepo&lt;/a&gt; in May 2026, so the TypeScript and Rust versions now evolve side by side. The eventual goal is for the Rust engine to be what you run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why did pnpm choose Rust?
&lt;/h2&gt;

&lt;p&gt;The motivation isn't abstract. Two things were capping how fast a Node-based pnpm could go.&lt;/p&gt;

&lt;p&gt;The first is startup cost. Every invocation booted a Node.js runtime before it did any work. pnpm v12 ships as native per-platform binaries with &lt;a href="https://pnpm.io/blog/releases/11.10" rel="noopener noreferrer"&gt;no Node.js launcher&lt;/a&gt;, so a command pays no runtime bootstrap cost at all. On a fast, cached install where the actual work is milliseconds, that bootstrap was a real fraction of the wall-clock time.&lt;/p&gt;

&lt;p&gt;The second is the file I/O itself. A cold install on a large monorepo is really a lot of filesystem operations: fetching tarballs, unpacking them into the store, and hard-linking thousands of files into place. Running all of that through a single JavaScript thread leaves throughput on the table. Rust handles that fan-out natively, without the runtime sitting in the middle of every syscall.&lt;/p&gt;

&lt;p&gt;There's a softer reason too, and it's the same one that's driving the broader trend: the tooling to do this kind of rewrite has gotten dramatically better, and AI-assisted porting made a 1:1 translation of a large codebase far more tractable than it would have been a few years ago.&lt;/p&gt;

&lt;h2&gt;
  
  
  How much faster is pnpm v12?
&lt;/h2&gt;

&lt;p&gt;pnpm publishes a &lt;a href="https://pnpm.io/benchmarks" rel="noopener noreferrer"&gt;benchmark suite&lt;/a&gt; comparing v11 against the Rust engine directly. As of this writing (July 2026), here's what it shows:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;pnpm v11&lt;/th&gt;
&lt;th&gt;pnpm (Rust)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Everything warm (repeat install)&lt;/td&gt;
&lt;td&gt;381 ms&lt;/td&gt;
&lt;td&gt;12 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;node_modules present, no cache&lt;/td&gt;
&lt;td&gt;460 ms&lt;/td&gt;
&lt;td&gt;40 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clean install (nothing cached)&lt;/td&gt;
&lt;td&gt;6.5 s&lt;/td&gt;
&lt;td&gt;2.2 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The warm case is everything already in place, so the install just re-verifies the tree, which is what happens on every CI job. That drops from 381ms to 12ms, roughly a 30x difference, and it's the number you'll feel most, because it runs constantly. The colder cases improve less dramatically but still meaningfully: a fully clean install with nothing cached goes from 6.5s to 2.2s, about 3x.&lt;/p&gt;

&lt;p&gt;Treat these as pnpm's own numbers on pnpm's own hardware, and expect your mileage to vary with disk speed and project size. But the direction is not subtle, and the team's claim is that the Rust engine is faster than v11 in every scenario they measured.&lt;/p&gt;

&lt;h2&gt;
  
  
  It's already rolling out, incrementally
&lt;/h2&gt;

&lt;p&gt;Here's the part that's easy to get wrong: pnpm v12 isn't a big-bang release. The Rust engine has been landing piece by piece through the v11 line, opt-in first.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;pnpm 11.2&lt;/strong&gt; let you opt into pacquet as the install backend for materialization only. Rust did the fetch and link, while pnpm still resolved dependencies and wrote the lockfile.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pnpm 11.7&lt;/strong&gt; went further: a standard install could be &lt;a href="https://pnpm.io/blog/releases/11.7" rel="noopener noreferrer"&gt;delegated to pacquet end-to-end&lt;/a&gt;, with resolution, lockfile write, and linking all in a single Rust pass.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pnpm 11.10&lt;/strong&gt; added &lt;code&gt;pnpm self-update next-12&lt;/code&gt;, so you can pull the v12 line and try the Rust binary directly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In v12, the Rust engine for fetching and linking becomes the default rather than an opt-in. The &lt;a href="https://github.com/pnpm/pnpm/issues/11633" rel="noopener noreferrer"&gt;official roadmap&lt;/a&gt; then works outward from there: first the headless frozen-lockfile installer, then full dependency resolution (&lt;code&gt;add&lt;/code&gt;, &lt;code&gt;update&lt;/code&gt;, &lt;code&gt;remove&lt;/code&gt;, catalogs, overrides, peers), and finally the remaining commands like &lt;code&gt;run&lt;/code&gt;, &lt;code&gt;store&lt;/code&gt;, and &lt;code&gt;publish&lt;/code&gt;. So even after v12 ships, parts of pnpm will still run through the TypeScript implementation for a while. The lockfile is what lets the two halves cooperate.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changes for you?
&lt;/h2&gt;

&lt;p&gt;The honest answer is: not much, on purpose. Same CLI, same lockfile, same &lt;code&gt;node_modules&lt;/code&gt;. The whole point of a 1:1 port is that your muscle memory and your CI scripts keep working.&lt;/p&gt;

&lt;p&gt;The breaking changes slated for v12 are &lt;a href="https://github.com/orgs/pnpm/discussions/11292" rel="noopener noreferrer"&gt;deliberately small&lt;/a&gt;: removing some old migration code for git-hosted tarballs, rejecting unscoped auth settings, and deprecating the &lt;code&gt;$&lt;/code&gt; syntax in &lt;code&gt;overrides&lt;/code&gt; in favor of catalogs. If none of those describe your setup, a v12 upgrade should be close to a no-op.&lt;/p&gt;

&lt;h3&gt;
  
  
  Will the upgrade break your CI?
&lt;/h3&gt;

&lt;p&gt;Probably not, but the one thing worth checking is feature parity during the transition. Because the Rust engine is being ported incrementally, some newer or niche capabilities may lag the TypeScript version at any given moment. If your install depends on something specific (a particular linker mode, lockfile-verification settings, or &lt;code&gt;pnpm deploy&lt;/code&gt;), test the bump in an isolated &lt;a href="https://morello.dev/blog/git-worktrees-are-underrated" rel="noopener noreferrer"&gt;git worktree&lt;/a&gt; before you flip it on in CI, the same sandbox I used while &lt;a href="https://morello.dev/blog/solidjs-2-async-data" rel="noopener noreferrer"&gt;exploring SolidJS 2.0's async data model&lt;/a&gt;. This is moving quickly, so the right list of gaps is whatever the roadmap says on the day you upgrade.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bigger picture
&lt;/h2&gt;

&lt;p&gt;pnpm is not doing this alone. The last few years have quietly rebuilt the JavaScript toolchain's foundations in native languages: &lt;a href="https://biomejs.dev" rel="noopener noreferrer"&gt;Biome&lt;/a&gt; for linting and formatting, &lt;a href="https://oxc.rs" rel="noopener noreferrer"&gt;Oxc&lt;/a&gt; for parsing and transforming, &lt;a href="https://rolldown.rs" rel="noopener noreferrer"&gt;Rolldown&lt;/a&gt; and Turbopack for bundling. TypeScript's compiler went to Go. Deno is Rust from the ground up. (Bun is the odd one out: it's written in Zig, not Rust, so don't lump it into the "everything's Rust now" story.)&lt;/p&gt;

&lt;p&gt;There's a competitive angle underneath it, too. Bun and Deno both ship as single fast binaries with package management built in, and a chunk of their appeal is exactly that no-runtime-bootstrap speed. A native-binary pnpm with no Node launcher closes that gap while keeping the thing that makes pnpm pnpm: the strict, content-addressable store and the non-flat &lt;code&gt;node_modules&lt;/code&gt; that catches phantom dependencies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should you upgrade to pnpm v12 yet?
&lt;/h2&gt;

&lt;p&gt;If you're waiting for a stable release: keep waiting, but not for long. As of mid-July 2026, v12 is in alpha. The latest tag is &lt;code&gt;v12.0.0-alpha.12&lt;/code&gt;, and the stable line is still v11.13.0. This is not something to point production CI at today.&lt;/p&gt;

&lt;p&gt;But if you want to feel the difference now, you already can. Run &lt;code&gt;pnpm self-update next-12&lt;/code&gt; on a branch, or opt into the pacquet backend on your current v11, and watch a warm install that used to take a beat return before you've finished reading the command. The type of release this is, where the language underneath gets an order of magnitude faster while everything on top stays exactly where you left it, is a rare and good one. pnpm was already the fast package manager. It's about to stop being one you ever wait on.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>rust</category>
      <category>node</category>
    </item>
    <item>
      <title>WebMCP: Making Your Site Usable by AI Agents</title>
      <dc:creator>Dennis Morello</dc:creator>
      <pubDate>Mon, 13 Jul 2026 13:19:29 +0000</pubDate>
      <link>https://dev.to/morellodev/webmcp-making-your-site-usable-by-ai-agents-5b28</link>
      <guid>https://dev.to/morellodev/webmcp-making-your-site-usable-by-ai-agents-5b28</guid>
      <description>&lt;p&gt;A while back I wrote about &lt;a href="https://morello.dev/blog/configuring-my-site-for-ai-discoverability" rel="noopener noreferrer"&gt;configuring this site for AI discoverability&lt;/a&gt;: raw Markdown mirrors, &lt;code&gt;llms.txt&lt;/code&gt;, an AI stance in &lt;code&gt;robots.txt&lt;/code&gt;. All of that is about making a site &lt;em&gt;readable&lt;/em&gt;. An agent fetches your prose cheaply, summarizes it, and moves on. The agent reads, but it can't do anything on your page.&lt;/p&gt;

&lt;p&gt;WebMCP is the other half. Instead of handing an agent your text to read, you hand it a set of typed functions to call: search your posts, filter a product list, submit a form. The page declares what it can do, and an AI agent running in the browser invokes those actions directly.&lt;/p&gt;

&lt;p&gt;It's early and experimental, Chromium-only behind a flag, and the spec changes almost weekly. But it's the first serious attempt at answering "how do I make my site &lt;em&gt;usable&lt;/em&gt; by agents, not just legible to them," so it's worth understanding now.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is WebMCP?
&lt;/h2&gt;

&lt;p&gt;WebMCP is a proposed web API that lets a page expose structured &lt;strong&gt;tools&lt;/strong&gt; to an AI agent: JavaScript functions, each with a natural-language description and a JSON Schema for its parameters, that an in-browser agent can discover and call. In &lt;a href="https://developer.chrome.com/docs/ai/webmcp" rel="noopener noreferrer"&gt;Chrome's own words&lt;/a&gt;, it's "a proposed web standard to help you build and expose structured tools for AI agents."&lt;/p&gt;

&lt;p&gt;The name is the giveaway. It's &lt;a href="https://modelcontextprotocol.io" rel="noopener noreferrer"&gt;Model Context Protocol&lt;/a&gt; adapted for the browser. A normal MCP server is a separate backend process that advertises tools to a model over a protocol. WebMCP takes that same tool model, name, description, input schema, and a result payload, and moves it into the page itself. Your page becomes, in effect, a client-side MCP server. The tools are defined &lt;em&gt;and&lt;/em&gt; executed in your own JavaScript context, reusing the session the user is already logged into. No extra backend, no separate auth handshake.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not just let the agent read the page?
&lt;/h2&gt;

&lt;p&gt;Because reading a page to &lt;em&gt;act&lt;/em&gt; on it is expensive and fragile. Today an agent that wants to click a button downloads your DOM, maybe screenshots the page, infers which element is the "add to cart" control, and computes where to click. That burns tokens, adds latency, and breaks the moment a layout shifts or an ad loads late. You're asking a model to reverse-engineer an interface that was built for eyes and a mouse.&lt;/p&gt;

&lt;p&gt;WebMCP replaces that guesswork with an explicit contract. The page says "here is a &lt;code&gt;search_posts&lt;/code&gt; tool, it takes a &lt;code&gt;tag&lt;/code&gt;, here's what it returns," and the agent calls it like a function. It's the difference between scraping a form and being handed its API.&lt;/p&gt;

&lt;p&gt;It also fills a gap the discoverability tools don't:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mechanism&lt;/th&gt;
&lt;th&gt;What it gives an agent&lt;/th&gt;
&lt;th&gt;Direction&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;llms.txt&lt;/code&gt; / Markdown mirrors&lt;/td&gt;
&lt;td&gt;Your content, cheaply&lt;/td&gt;
&lt;td&gt;Read-only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remote MCP server&lt;/td&gt;
&lt;td&gt;Tools, via a separate backend + its own auth&lt;/td&gt;
&lt;td&gt;Actionable, off-page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;WebMCP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Tools, in the page, on the user's session&lt;/td&gt;
&lt;td&gt;Actionable, in-page&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;llms.txt&lt;/code&gt; tells an agent what you've written. A remote MCP server exposes actions but lives on its own infrastructure with its own OAuth story. WebMCP is the in-page option: actions that run in the tab the user already has open, with the auth they already have.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does the API look like?
&lt;/h2&gt;

&lt;p&gt;You register tools on &lt;code&gt;document.modelContext&lt;/code&gt;. Each call defines one tool with four keys: a &lt;code&gt;name&lt;/code&gt;, a &lt;code&gt;description&lt;/code&gt; the agent reads to decide when to use it, an &lt;code&gt;inputSchema&lt;/code&gt; (JSON Schema) for the arguments, and an async &lt;code&gt;execute&lt;/code&gt; function that does the work.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;modelContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registerTool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;search_posts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Search blog posts by tag. Returns matching titles and URLs.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A single lowercase tag, e.g. 'webdev'&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="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tag&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="na"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;tag&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;posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;findPostsByTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`Found &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; posts tagged &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tag&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="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;That example returns a plain string, which the &lt;a href="https://developer.chrome.com/docs/ai/webmcp/imperative-api" rel="noopener noreferrer"&gt;imperative API docs&lt;/a&gt; accept. The spec actually types the return as &lt;code&gt;any&lt;/code&gt;, so a string is fine, but if you want to match what a regular MCP tool sends back you'd return a &lt;a href="https://modelcontextprotocol.io/specification/2025-06-18/server/tools" rel="noopener noreferrer"&gt;&lt;code&gt;{ content: [{ type: "text", text: "…" }] }&lt;/code&gt; object&lt;/a&gt;. WebMCP mirrors that convention without enforcing it. Tools are registered one at a time, and a &lt;code&gt;toolchange&lt;/code&gt; event fires whenever the set changes, so other frames can react when you add or remove a tool.&lt;/p&gt;

&lt;p&gt;One trap if you follow older tutorials: the entry point used to be &lt;code&gt;navigator.modelContext&lt;/code&gt;. That's &lt;strong&gt;deprecated as of Chrome 150&lt;/strong&gt; in favor of &lt;code&gt;document.modelContext&lt;/code&gt;. Write the new one; a lot of early write-ups still show the old surface.&lt;/p&gt;

&lt;p&gt;There's also a declarative flavor where you annotate existing &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt; elements and Chrome exposes them as tools automatically. Chrome documents it, but the spec itself hasn't defined it yet, its section is literally marked as a TODO pointing back to the explainer. Treat that one as a preview of a preview.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you try it today?
&lt;/h2&gt;

&lt;p&gt;In Chrome, behind an origin trial. Per the &lt;a href="https://groups.google.com/a/chromium.org/g/blink-dev/c/gmYffo5WOE8/m/OJxuQRP3AAAJ" rel="noopener noreferrer"&gt;Chromium "Intent to Experiment"&lt;/a&gt;, WebMCP runs as an origin trial from &lt;strong&gt;Chrome 149 through 156&lt;/strong&gt;, on desktop first, plus Android and WebView. You can &lt;a href="https://developer.chrome.com/blog/ai-webmcp-origin-trial" rel="noopener noreferrer"&gt;sign up for the trial&lt;/a&gt; to enable it for real users, or flip it on locally at &lt;code&gt;chrome://flags/#enable-webmcp-testing&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The cross-browser picture is the honest catch. In the same filing, Mozilla (Gecko) and WebKit are both recorded as &lt;strong&gt;"No signal."&lt;/strong&gt; And despite Microsoft co-authoring the spec, there's no shipping Edge support I could find. So right now this is a Chromium-only experiment. Don't build anything on it expecting Firefox or Safari to follow soon; they haven't said they will.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is WebMCP an actual standard?
&lt;/h2&gt;

&lt;p&gt;Not yet, and it's worth being precise about that. WebMCP is incubated in the &lt;a href="https://webmachinelearning.github.io/webmcp/" rel="noopener noreferrer"&gt;W3C Web Machine Learning Community Group&lt;/a&gt;, and the current draft is dated &lt;strong&gt;July 10, 2026&lt;/strong&gt;. A Community Group Report is explicitly &lt;em&gt;not&lt;/em&gt; a W3C Standard and not on the Recommendation track. It's a proposal with momentum, edited by engineers from Google and Microsoft, but it can still change shape or stall.&lt;/p&gt;

&lt;p&gt;The origin story is a good reminder of how early this is. WebMCP grew out of &lt;strong&gt;MCP-B&lt;/strong&gt;, a browser extension built by &lt;a href="https://github.com/MiguelsPizza" rel="noopener noreferrer"&gt;Alex Nahas&lt;/a&gt;. His insight was that the browser already solves the hard part of agent auth, you're logged in, so run MCP &lt;em&gt;inside&lt;/em&gt; the browser and inherit that session instead of standing up a server and an OAuth flow for every tool. That extension work converged into the W3C proposal and got renamed WebMCP. He built MCP-B in January 2025, and the draft is dated July 2026, so we're about eighteen months out from a browser extension becoming a draft spec. Worth keeping in mind before you plan a roadmap around it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about security?
&lt;/h2&gt;

&lt;p&gt;This is the part I'd read before shipping anything, because handing a language model a set of callable actions on your users' authenticated sessions is exactly as dangerous as it sounds. The &lt;a href="https://developer.chrome.com/docs/ai/webmcp/secure-tools" rel="noopener noreferrer"&gt;security model&lt;/a&gt; gives you a few knobs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;exposedTo&lt;/code&gt;&lt;/strong&gt; allowlists which origins can see a tool. Tools aren't exposed cross-origin by default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;untrustedContentHint&lt;/code&gt;&lt;/strong&gt; flags a tool's output as untrusted (user-generated or external) so the agent treats it with more suspicion, a prompt-injection signal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;readOnlyHint&lt;/code&gt;&lt;/strong&gt; marks a tool that doesn't change state, so an agent can skip a confirmation prompt. The docs still warn that even a read can leak information, so only expose to origins you trust.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;requestUserInteraction()&lt;/code&gt;&lt;/strong&gt; lets a tool pause mid-execution to ask the user to confirm.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But the docs are refreshingly blunt about the ceiling here: prompt injection is not solved. They acknowledge that repeatable prompt-injection attacks have succeeded against state-of-the-art models, and that because models are probabilistic, safety can't be guaranteed inside the model itself. Origin gating, the hints, and human confirmation are defense in depth, not a guarantee. If you expose a destructive action as a tool, assume it can be tricked into firing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should you add it to your site?
&lt;/h2&gt;

&lt;p&gt;Not to production, not yet. It's Chromium-only, gated behind an origin trial that expires at Chrome 156, the declarative API isn't specified, no other engine has signaled support, and the spec is moving week to week. Anything you build today you'll be rewriting.&lt;/p&gt;

&lt;p&gt;But it's worth prototyping, and worth understanding, because the shape of it is right. My &lt;a href="https://morello.dev/blog/configuring-my-site-for-ai-discoverability" rel="noopener noreferrer"&gt;discoverability post&lt;/a&gt; was about the read side: serve agents a version of your content they can consume cheaply. WebMCP is the write side. It's the first proposal that treats agent interaction as an explicit, typed contract the page controls, rather than something agents reverse-engineer from your DOM. Whether it ships as-is, gets filed down by the standards process, or stalls, the shape of the answer looks right. Same instinct that made &lt;a href="https://morello.dev/blog/the-new-http-query-method" rel="noopener noreferrer"&gt;the QUERY method&lt;/a&gt; and &lt;a href="https://morello.dev/blog/cross-origin-storage-api" rel="noopener noreferrer"&gt;Cross-Origin Storage&lt;/a&gt; satisfying: the platform finally growing a real name for something we'd been faking.&lt;/p&gt;

&lt;p&gt;Maybe it ships as WebMCP, maybe the standards process files it down into something else. Either way, sites are going to start declaring what agents can do, not just what they can read. I'd rather figure that out while it's a flag in Chrome than after it's a default everywhere.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>mcp</category>
    </item>
    <item>
      <title>Build-Time OG Images with Satori and Astro</title>
      <dc:creator>Dennis Morello</dc:creator>
      <pubDate>Sun, 12 Jul 2026 10:16:39 +0000</pubDate>
      <link>https://dev.to/morellodev/generating-og-images-at-build-time-with-satori-and-astro-4b4b</link>
      <guid>https://dev.to/morellodev/generating-og-images-at-build-time-with-satori-and-astro-4b4b</guid>
      <description>&lt;p&gt;Every page on this site ships with its own &lt;a href="https://ogp.me/" rel="noopener noreferrer"&gt;Open Graph&lt;/a&gt; image, the little card that shows up when you paste a link into Slack, X, or iMessage. None of them are drawn by hand. Each one is generated at build time by two tools working in sequence: &lt;a href="https://github.com/vercel/satori" rel="noopener noreferrer"&gt;Satori&lt;/a&gt; turns a chunk of JSX styled with a CSS subset into an SVG, and &lt;a href="https://github.com/yisibl/resvg-js" rel="noopener noreferrer"&gt;resvg-js&lt;/a&gt; rasterizes that SVG into a PNG. The result is a static &lt;code&gt;.png&lt;/code&gt; for the homepage, the blog index, every post, and every tag, all baked into &lt;code&gt;dist/&lt;/code&gt; and served straight from the edge with zero work at request time.&lt;/p&gt;

&lt;p&gt;I rebuilt this whole pipeline recently, and it turned into a nice case study in doing something simple well. Below is how it works, why it's built the way it is, and the handful of performance decisions that let it run for every page on every build without slowing anything down.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually generates the images
&lt;/h2&gt;

&lt;p&gt;The pipeline is two libraries, each doing one job.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Satori&lt;/strong&gt;, from Vercel, is described in its own repo as a library "to convert HTML and CSS to SVG." You hand it an object tree that looks like JSX and a set of fonts, and it lays everything out with a Flexbox engine and returns an SVG string. It's the same engine behind &lt;a href="https://vercel.com/docs/og-image-generation" rel="noopener noreferrer"&gt;Vercel's OG image generation&lt;/a&gt;, so it's built for exactly this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;resvg-js&lt;/strong&gt;, maintained by &lt;a href="https://github.com/yisibl" rel="noopener noreferrer"&gt;yisibl&lt;/a&gt;, is "a high-performance SVG renderer and toolkit, powered by Rust based resvg." Satori gives you vector output, but social platforms want a raster image, so resvg-js takes the SVG and renders a PNG. It's a thin Node binding over the Rust &lt;a href="https://github.com/linebender/resvg" rel="noopener noreferrer"&gt;resvg&lt;/a&gt; library, which is fast and has no runtime dependencies.&lt;/p&gt;

&lt;p&gt;The whole thing is about ten lines once the layout exists:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;svg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;satori&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OG_WIDTH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OG_HEIGHT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;fonts&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="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;JetBrains Mono&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fonts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;regular&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;normal&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="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;JetBrains Mono&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fonts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bold&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;700&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;normal&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="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resvg&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;Resvg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;svg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;font&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;loadSystemFonts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;fitTo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;width&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OG_WIDTH&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="nx"&gt;resvg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;asPng&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;loadSystemFonts: false&lt;/code&gt; line is doing more than it looks like. I'll come back to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The design: one card, deliberately plain
&lt;/h2&gt;

&lt;p&gt;There is exactly one card template. Everything on the site, from a post to the tags hub, renders through the same function with different props. That's a design decision, not laziness. A single template means every share preview looks like it came from the same place, and there's only one layout to keep correct.&lt;/p&gt;

&lt;p&gt;The layout is built from a few primitives. A &lt;code&gt;morello.dev&lt;/code&gt; wordmark sits top-left in the accent color, with an optional eyebrow (&lt;code&gt;blog&lt;/code&gt;, &lt;code&gt;tags&lt;/code&gt;) on the right. The title dominates the middle: 60px, bold, left-aligned, vertically centered, capped at a &lt;code&gt;maxWidth&lt;/code&gt; so long titles wrap instead of running to the edge. A byline anchors the bottom, either the post author or a page subtitle, introduced by a short accent-colored tick. That's the whole thing.&lt;/p&gt;

&lt;p&gt;The type is &lt;a href="https://www.jetbrains.com/lp/mono/" rel="noopener noreferrer"&gt;JetBrains Mono&lt;/a&gt; throughout, which matches the monospace-first identity of &lt;a href="https://morello.dev/blog/the-new-website" rel="noopener noreferrer"&gt;the rest of the site&lt;/a&gt;. The color scheme is Catppuccin Mocha: a near-black &lt;code&gt;#11111b&lt;/code&gt; background, &lt;code&gt;#cdd6f4&lt;/code&gt; text, and a &lt;code&gt;#89b4fa&lt;/code&gt; blue accent.&lt;/p&gt;

&lt;p&gt;Here's the part people find surprising. The site itself has &lt;a href="https://morello.dev/blog/the-new-website" rel="noopener noreferrer"&gt;16 themes you can switch between&lt;/a&gt;, but the OG cards are locked to Catppuccin Mocha and don't follow the visitor's choice. That's on purpose. A share card is a static artifact generated once at build time. The person who sees it on X has no relationship to the &lt;code&gt;localStorage&lt;/code&gt; theme of whoever pasted the link. Making the card theme-aware would mean generating 16 variants of every image to serve one that nobody specifically asked for. So the OG palette lives in its own tiny &lt;code&gt;colors.ts&lt;/code&gt; file, mirroring the default theme's tokens, completely detached from the runtime theming system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why generate at build time instead of on demand
&lt;/h2&gt;

&lt;p&gt;Because there's no reason to pay for it twice.&lt;/p&gt;

&lt;p&gt;A lot of OG setups render the image in a serverless function on the first request and cache it. That's the right call when your content is dynamic or unbounded. Mine isn't. It's a static site with a known, finite set of pages, so I generate every card during &lt;code&gt;astro build&lt;/code&gt; through &lt;code&gt;getStaticPaths&lt;/code&gt;:&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getStaticPaths&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GetStaticPaths&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nf"&gt;getCollection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;blog&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;getAllTags&lt;/span&gt;&lt;span class="p"&gt;()]);&lt;/span&gt;
  &lt;span class="c1"&gt;// ...one entry per static page, post, and tag&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Astro walks that list and writes a real PNG for each path into &lt;code&gt;dist/&lt;/code&gt;. At request time there's no function to invoke and no rendering step. &lt;a href="https://www.cloudflare.com/" rel="noopener noreferrer"&gt;Cloudflare&lt;/a&gt; serves the file from its asset cache like any other static file. The cost is paid once, on my machine or in CI, and never again. For a site that already ships as static assets, running a serverless renderer for images would be the odd one out.&lt;/p&gt;

&lt;p&gt;Paginated pages get a small optimization on top of this. &lt;code&gt;/blog/2&lt;/code&gt; and beyond don't need their own card, so they reuse the page-1 image (&lt;code&gt;/og/blog.png&lt;/code&gt;) instead of generating a near-identical copy per page. Same for tag pagination. That's fewer images to render and fewer bytes in &lt;code&gt;dist/&lt;/code&gt;, for previews that would look identical either way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The performance decisions that matter
&lt;/h2&gt;

&lt;p&gt;Generating a few dozen images per build sounds cheap, and it is, but the naive version was slow enough on my machine to be annoying. A few changes made it genuinely fast.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Skip the system-font scan.&lt;/strong&gt; This is the big one. By default, resvg-js scans the operating system's installed fonts so it can render &lt;code&gt;&amp;lt;text&amp;gt;&lt;/code&gt; elements. Its own type definitions say of &lt;code&gt;loadSystemFonts&lt;/code&gt;: "Default: true, if set to false, it will be faster." On a developer machine with a large font library (hello, macOS), that scan runs on every single &lt;code&gt;Resvg&lt;/code&gt; construction and dominates the render time.&lt;/p&gt;

&lt;p&gt;The trick is that we never need it. Satori, by default, "renders the text as &lt;code&gt;&amp;lt;path&amp;gt;&lt;/code&gt; in SVG, instead of &lt;code&gt;&amp;lt;text&amp;gt;&lt;/code&gt;" and "embeds the font path data as inlined information." Every glyph is already a vector outline by the time resvg-js sees the SVG, so there are no fonts left to resolve. Setting &lt;code&gt;loadSystemFonts: false&lt;/code&gt; was safe and cut per-image render time by roughly 20x locally. In CI, where the container has almost no fonts installed, the scan was cheap anyway, so this is pure upside.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Load fonts from the build, not a CDN.&lt;/strong&gt; Satori needs the actual font bytes passed in. Early on I fetched JetBrains Mono from a CDN at build time, which meant the build made a network call and broke offline. Now the fonts come from Astro's &lt;a href="https://docs.astro.build/en/reference/experimental-flags/fonts/" rel="noopener noreferrer"&gt;fonts API&lt;/a&gt; via &lt;code&gt;astro:assets&lt;/code&gt;, read straight off disk. The build makes no network call for them and pulls in nothing external.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Load the font once.&lt;/strong&gt; The font bytes are the same for every image, so re-reading them per card is wasted work. A small memoized loader reads each weight once and hands the same buffers to every render for the rest of the build:&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;loadFonts&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;regular&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;ArrayBuffer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;bold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;ArrayBuffer&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;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&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;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;??=&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nf"&gt;loadWeight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;400&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;loadWeight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;700&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;regular&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bold&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;regular&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bold&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="nx"&gt;cache&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;&lt;strong&gt;Use woff, not woff2.&lt;/strong&gt; This one is a gotcha rather than an optimization. Satori's README is blunt about it: "Satori currently supports three font formats: TTF, OTF and WOFF. Note that WOFF2 is not supported at the moment." So the OG font is a plain &lt;code&gt;.woff&lt;/code&gt; at weights 400 and 700, a separate entry from the variable &lt;code&gt;.woff2&lt;/code&gt; the site itself uses. Feed Satori a woff2 and it fails; the format that's best for the browser is the one Satori can't read.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Drop decorative texture.&lt;/strong&gt; The card used to have a faint dot-grid background. It looked fine, but every dot is geometry Satori has to lay out and resvg-js has to rasterize, multiplied across every image. I removed it in favor of a flat background. The card looks cleaner and each render does less work.&lt;/p&gt;

&lt;h2&gt;
  
  
  OG best practices worth stealing
&lt;/h2&gt;

&lt;p&gt;A few things I'd carry to any OG setup, whatever tools you use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Size the card 1200x630.&lt;/strong&gt; &lt;a href="https://developers.facebook.com/docs/sharing/webmasters/images/" rel="noopener noreferrer"&gt;Facebook's sharing docs&lt;/a&gt; recommend "images that are at least 1200 x 630 pixels for the best display on high resolution devices" and an aspect ratio "as close to 1.91:1 as possible." Worth noting: the &lt;a href="https://ogp.me/" rel="noopener noreferrer"&gt;Open Graph protocol itself&lt;/a&gt; never specifies a size, so this number comes from the platforms, not the spec. 1200x630 is the safe cross-platform default, and it's what &lt;code&gt;OG_WIDTH&lt;/code&gt; and &lt;code&gt;OG_HEIGHT&lt;/code&gt; are set to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Let the title wrap gracefully.&lt;/strong&gt; Satori supports &lt;code&gt;textWrap: balance&lt;/code&gt;, which evens out line lengths so a two-line title doesn't leave one word stranded. It does not honor &lt;code&gt;text-wrap: pretty&lt;/code&gt;, so &lt;code&gt;balance&lt;/code&gt; is the one to reach for.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Design within Satori's CSS subset.&lt;/strong&gt; Satori lays out with Flexbox, and &lt;code&gt;display: grid&lt;/code&gt; isn't supported. If you're used to reaching for grid, you'll rewrite it as nested flex containers. Knowing this up front saves you from debugging a layout that silently doesn't apply.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Always set &lt;code&gt;og:image:alt&lt;/code&gt;.&lt;/strong&gt; The card is an image like any other, and it deserves alt text for the same accessibility reasons. Astro's OG plumbing handles the meta tags, but the alt text is content you have to write.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the social palette static.&lt;/strong&gt; Even if your site has themes, pin the cards to one look. A share preview is generated once and seen by strangers; it should be consistent, not personalized.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;The pipeline that generates every social card on this site is two libraries and a couple hundred lines: Satori for HTML and CSS to SVG, resvg-js for SVG to PNG, wired into &lt;code&gt;getStaticPaths&lt;/code&gt; so it all happens at build time and never at runtime. The interesting parts weren't the generation itself but the constraints around it. One static template keeps every preview consistent. Generating at build time means the edge just serves files. And the render stays quick mostly because it skips the system-font scan resvg-js doesn't actually need here.&lt;/p&gt;

&lt;p&gt;If you want the wider context on how the rest of the site is put together, I wrote about &lt;a href="https://morello.dev/blog/the-new-website" rel="noopener noreferrer"&gt;rebuilding it with Astro and Tailwind&lt;/a&gt;, and about the &lt;a href="https://morello.dev/blog/configuring-my-site-for-ai-discoverability" rel="noopener noreferrer"&gt;discoverability plumbing&lt;/a&gt; that OG images are one small part of.&lt;/p&gt;

</description>
      <category>astro</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>performance</category>
    </item>
    <item>
      <title>shadcn/typeset vs Tailwind Typography for Markdown</title>
      <dc:creator>Dennis Morello</dc:creator>
      <pubDate>Sat, 11 Jul 2026 08:21:32 +0000</pubDate>
      <link>https://dev.to/morellodev/shadcntypeset-vs-tailwind-typography-styling-markdown-c82</link>
      <guid>https://dev.to/morellodev/shadcntypeset-vs-tailwind-typography-styling-markdown-c82</guid>
      <description>&lt;p&gt;You render some markdown, get back a pile of unstyled HTML, and then you style it. Headings, paragraphs, lists, tables, code. You do it for the blog. Then you do it again for the docs. Then a third time for the chat UI that streams model output token by token. Every context, the same elements, slightly different rules.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://x.com/shadcn" rel="noopener noreferrer"&gt;shadcn&lt;/a&gt; shipped a take on this on July 10, 2026, called &lt;a href="https://ui.shadcn.com/docs/typeset" rel="noopener noreferrer"&gt;shadcn/typeset&lt;/a&gt;. It's worth a look even if you don't use the rest of shadcn/ui, because it isn't a component and it isn't a CLI install. It's one CSS file you copy into your project and own outright.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is shadcn/typeset?
&lt;/h2&gt;

&lt;p&gt;Typeset is a styling system for rendered HTML and markdown, delivered as a single CSS file. You wrap your content in a &lt;code&gt;.typeset&lt;/code&gt; container and everything inside it gets styled: headings, paragraphs, lists, tables, code, blockquotes, links, footnotes, even MathML and &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"typeset typeset-docs"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;YourMarkdownRenderer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;YourMarkdownRenderer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;.typeset&lt;/code&gt; turns the styles on. &lt;code&gt;.typeset-docs&lt;/code&gt; is a preset, a small class you layer on top to tune the look for a specific context. You can define as many presets as you have contexts: one for the blog, one for the docs, one for chat.&lt;/p&gt;

&lt;p&gt;The thing to internalize up front: there's no npm package and no config layer. As the &lt;a href="https://ui.shadcn.com/docs/changelog/2026-07-typeset" rel="noopener noreferrer"&gt;changelog&lt;/a&gt; puts it, "the file lives in your project," nothing to update, nothing to work around. If a heading annoys you, you open the file and change the rule. That's the whole distribution model.&lt;/p&gt;

&lt;p&gt;This is a different thing from shadcn/ui's &lt;a href="https://ui.shadcn.com/docs/components/base/typography" rel="noopener noreferrer"&gt;Typography component&lt;/a&gt;, which gives you styled React heading and paragraph components to author by hand. Typeset styles HTML you didn't write, the output of a markdown renderer.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you install shadcn/typeset?
&lt;/h2&gt;

&lt;p&gt;You don't run a CLI. As of this release there's no &lt;code&gt;npx shadcn@latest add typeset&lt;/code&gt;, and expecting one will send you looking for a command that doesn't exist.&lt;/p&gt;

&lt;p&gt;Instead you build the file in the &lt;a href="https://ui.shadcn.com/typeset" rel="noopener noreferrer"&gt;interactive builder&lt;/a&gt;, pick your sizes and fonts, and copy the generated &lt;code&gt;typeset.css&lt;/code&gt; into your project. Then you import it after Tailwind:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s1"&gt;"tailwindcss"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s1"&gt;"./typeset.css"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Import order matters, and I'll come back to why in a moment. Once it's in, the wrapper from the first snippet is all you need.&lt;/p&gt;

&lt;h2&gt;
  
  
  Size, leading, and flow
&lt;/h2&gt;

&lt;p&gt;Typeset condenses typographic tuning into three CSS variables it calls "rhythm," and everything else derives from them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.typeset&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--typeset-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1em&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c"&gt;/* base font size */&lt;/span&gt;
  &lt;span class="py"&gt;--typeset-leading&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.75&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c"&gt;/* line-height */&lt;/span&gt;
  &lt;span class="py"&gt;--typeset-flow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.25em&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c"&gt;/* vertical space between blocks */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A preset is just those three variables set to different values under a class name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.typeset-chat&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--typeset-flow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1em&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--typeset-leading&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.typeset-docs&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--typeset-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;15px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--typeset-flow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.5em&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;Because it's all CSS custom properties, you can override a single value inline without touching the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;article&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"typeset [--typeset-flow:1.75em]"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;article&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The sizing is container-relative rather than a fixed &lt;code&gt;rem&lt;/code&gt; scale. Below &lt;code&gt;48rem&lt;/code&gt; the base size gets a &lt;code&gt;1.125x&lt;/code&gt; bump and then settles back to the raw value on larger screens, so text reads comfortably on a phone without a separate small-screen variant. Colors and borders pull from your app's theme tokens (&lt;code&gt;--color-foreground&lt;/code&gt;, &lt;code&gt;--color-muted-foreground&lt;/code&gt;, &lt;code&gt;--color-border&lt;/code&gt;, &lt;code&gt;--radius&lt;/code&gt;) with sensible fallbacks, so dark mode just works when your tokens flip. There's no second inverted palette to maintain.&lt;/p&gt;

&lt;p&gt;One deliberate omission: Typeset doesn't set a &lt;code&gt;max-width&lt;/code&gt;. Your layout owns the measure. The builder's Measure control adds a &lt;code&gt;max-width&lt;/code&gt; to the wrapper if you want it, but the stylesheet itself stays out of your layout's business.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why your Tailwind utilities win without !important
&lt;/h2&gt;

&lt;p&gt;Here's the detail I liked most. Typeset lives in the CSS &lt;code&gt;@components&lt;/code&gt; layer and uses zero-specificity &lt;code&gt;:where()&lt;/code&gt; selectors for every element rule:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@layer&lt;/span&gt; &lt;span class="n"&gt;components&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.typeset&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="err"&gt;&amp;amp;&lt;/span&gt; &lt;span class="err"&gt;:where(h1)&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.75em&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="nd"&gt;:where&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;margin-block-start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--typeset-flow&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="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The override comes from cascade layers, not specificity. Typeset's rules live in the &lt;code&gt;@components&lt;/code&gt; layer, and Tailwind's utilities live in the &lt;code&gt;@utilities&lt;/code&gt; layer, which is declared after it. When rules in two different layers conflict, the later layer wins outright, no matter how specific either selector is. So a utility beats a Typeset rule with no &lt;code&gt;!important&lt;/code&gt; and no special modifier syntax. Slap &lt;code&gt;.text-3xl&lt;/code&gt; on a heading and it just wins.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;:where()&lt;/code&gt; does a related but separate job: it holds every element rule at zero specificity, so an override that isn't already shielded by a later layer, like plain CSS you write yourself or a rule in the same layer, can still win without a specificity fight. That's also why import order matters. Importing Tailwind first establishes its layer order, which puts &lt;code&gt;@utilities&lt;/code&gt; after Typeset's &lt;code&gt;@components&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you want to pull a whole subtree out, there's an escape hatch: &lt;code&gt;.not-typeset&lt;/code&gt; (or the &lt;code&gt;data-not-typeset&lt;/code&gt; attribute), which cascades to descendants including any nested &lt;code&gt;.typeset&lt;/code&gt; container.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Card&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"not-typeset"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Untouched component.&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Card&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;shadcn is candid that both of these ideas, the &lt;code&gt;:where()&lt;/code&gt; guard and the opt-out class, are borrowed from &lt;a href="https://github.com/tailwindlabs/tailwindcss-typography" rel="noopener noreferrer"&gt;&lt;code&gt;@tailwindcss/typography&lt;/code&gt;&lt;/a&gt;, which has a &lt;code&gt;.not-prose&lt;/code&gt; of its own. Which brings us to the comparison you actually clicked for.&lt;/p&gt;

&lt;h2&gt;
  
  
  shadcn/typeset vs Tailwind Typography
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/tailwindlabs/tailwindcss-typography" rel="noopener noreferrer"&gt;Tailwind CSS Typography&lt;/a&gt; plugin, and the &lt;code&gt;.prose&lt;/code&gt; class it hands you, has been the default answer for styling rendered markdown for years, and it's still excellent at what it was built for. Typeset isn't a drop-in replacement or a wrapper around it. It's a different set of trade-offs. The docs lay them out directly:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Tailwind Typography (&lt;code&gt;.prose&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;Typeset&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sizing&lt;/td&gt;
&lt;td&gt;Fixed &lt;code&gt;rem&lt;/code&gt; scale, &lt;code&gt;.prose-sm&lt;/code&gt; to &lt;code&gt;.prose-2xl&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Relative to the container, any size&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dark mode&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.prose-invert&lt;/code&gt;, a second palette&lt;/td&gt;
&lt;td&gt;Your tokens flip, nothing to add&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Overrides&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;prose-a:&lt;/code&gt;, &lt;code&gt;prose-headings:&lt;/code&gt; modifier API&lt;/td&gt;
&lt;td&gt;Plain utilities and CSS win&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Streaming&lt;/td&gt;
&lt;td&gt;No append-stability contract&lt;/td&gt;
&lt;td&gt;Designed for stable appends&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Distribution&lt;/td&gt;
&lt;td&gt;npm plugin, generated CSS&lt;/td&gt;
&lt;td&gt;One CSS file you own&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The way I'd frame it: &lt;code&gt;.prose&lt;/code&gt; is a well-designed dependency you configure, and Typeset is a starting point you edit. If you like the modifier API and want a maintained plugin tracking Tailwind releases, &lt;code&gt;.prose&lt;/code&gt; is still a great pick. If you'd rather own the file and delete the rules you don't want, Typeset fits that instinct better. It's the same philosophy as the rest of shadcn/ui: copy the code in, make it yours.&lt;/p&gt;

&lt;h2&gt;
  
  
  What makes it streaming-safe?
&lt;/h2&gt;

&lt;p&gt;This is the part that made Typeset feel designed for 2026 rather than 2020. If you've built a chat UI that renders an LLM's markdown as it streams, you've watched the layout twitch as each token lands. Typeset treats that as a first-class constraint.&lt;/p&gt;

&lt;p&gt;The rule is that there are no forward-looking selectors in the layout. &lt;code&gt;:last-child&lt;/code&gt;, &lt;code&gt;:has()&lt;/code&gt;, and &lt;code&gt;:empty&lt;/code&gt; are deliberately kept out of spacing rules, because what they match changes as content appends, which restyles blocks that were already on screen. Spacing flows in one direction only, using &lt;code&gt;margin-block-start&lt;/code&gt; and never &lt;code&gt;margin-block-end&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="nd"&gt;:where&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;margin-block-start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--typeset-flow&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;margin-block-end&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&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;The same discipline shows up in tables: the row separators sit on the cells rather than on &lt;code&gt;tr:last-child&lt;/code&gt;, so appending a row never re-runs a &lt;code&gt;:last-child&lt;/code&gt; match and shifts the borders above it. When you add a new block to a Typeset container, nothing before it moves. That's a genuinely hard property to get right by hand, and it's baked into the stylesheet.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs you
&lt;/h2&gt;

&lt;p&gt;Two things are worth knowing before you commit.&lt;/p&gt;

&lt;p&gt;First, the stylesheet leans on modern CSS, though most of it is safe by now: &lt;code&gt;color-mix()&lt;/code&gt; and &lt;code&gt;oklch()&lt;/code&gt; colors have been widely supported since 2023, and logical properties like &lt;code&gt;margin-block-start&lt;/code&gt; work everywhere. The real outlier is &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/margin-trim" rel="noopener noreferrer"&gt;&lt;code&gt;margin-trim&lt;/code&gt;&lt;/a&gt;, which as of 2026 still ships only in Safari and other WebKit browsers (16.4 and up). There's no fallback for it, so in Chrome and Firefox that rule quietly does nothing. Worth knowing before you count on the margin trimming it's meant to handle.&lt;/p&gt;

&lt;p&gt;Second, wide tables wrap to fit by default. Horizontal scroll is opt-in through a &lt;code&gt;.typeset-scroll&lt;/code&gt; wrapper you have to add in your renderer's table component or a small rehype plugin. It won't happen automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should you use it?
&lt;/h2&gt;

&lt;p&gt;If you're already in the shadcn/ui world and you're styling rendered markdown, especially anything that streams, Typeset is an easy yes: it's free, it's one file, and the streaming stability alone justifies it. If you're outside that ecosystem and happy with &lt;code&gt;.prose&lt;/code&gt;, there's no migration to rush. It's an alternative with a clearer answer to a couple of specific problems, not an upgrade you're missing out on.&lt;/p&gt;

&lt;p&gt;What I appreciate is the framing. shadcn keeps betting that the right unit of distribution for UI is source you own rather than a dependency you configure, and Typeset applies that bet to typography. I made a similar call &lt;a href="https://morello.dev/blog/the-new-website" rel="noopener noreferrer"&gt;rebuilding this site&lt;/a&gt;: the long-form styling here is hand-written Tailwind wired to theme variables rather than a plugin, for exactly the reason Typeset exists. The same instinct runs through &lt;a href="https://morello.dev/blog/generating-og-images-with-satori-and-astro" rel="noopener noreferrer"&gt;generating OG images with Satori&lt;/a&gt;: own the design, skip the abstraction. Owning the CSS means when something looks off, you fix the rule instead of fighting the abstraction. Go &lt;a href="https://ui.shadcn.com/typeset" rel="noopener noreferrer"&gt;build one in the builder&lt;/a&gt; and read the file it hands you. It's short, and reading it teaches you more about typographic CSS than the docs do.&lt;/p&gt;

</description>
      <category>css</category>
      <category>tailwindcss</category>
      <category>webdev</category>
      <category>react</category>
    </item>
    <item>
      <title>TypeScript 7 Is Here: The Compiler Got Rewritten in Go</title>
      <dc:creator>Dennis Morello</dc:creator>
      <pubDate>Thu, 09 Jul 2026 10:43:20 +0000</pubDate>
      <link>https://dev.to/morellodev/typescript-7-is-here-the-compiler-got-rewritten-in-go-53cn</link>
      <guid>https://dev.to/morellodev/typescript-7-is-here-the-compiler-got-rewritten-in-go-53cn</guid>
      <description>&lt;p&gt;If you've worked in a large TypeScript codebase, you know the feeling. You save a file, tab away, and wait for the red squiggles to catch up. On a big monorepo, &lt;code&gt;tsc --noEmit&lt;/code&gt; in CI is the slow step everyone learned to stop watching. The type checker was written in TypeScript, running on a single JavaScript thread, and there's only so fast that can go.&lt;/p&gt;

&lt;p&gt;That's the thing that just changed. On &lt;a href="https://devblogs.microsoft.com/typescript/announcing-typescript-7-0/" rel="noopener noreferrer"&gt;July 8, 2026&lt;/a&gt;, Microsoft shipped &lt;strong&gt;TypeScript 7.0&lt;/strong&gt;: the same compiler you already use, rewritten from the ground up in &lt;a href="https://go.dev" rel="noopener noreferrer"&gt;the Go language&lt;/a&gt;. The headline number is builds that run 8 to 12 times faster, and after living on the preview for a while, that number holds up.&lt;/p&gt;

&lt;h2&gt;
  
  
  What TypeScript 7 actually is
&lt;/h2&gt;

&lt;p&gt;TypeScript 7 is a &lt;a href="https://devblogs.microsoft.com/typescript/typescript-native-port/" rel="noopener noreferrer"&gt;native port&lt;/a&gt; of the compiler and tooling. There's no new type system here, no new syntax. The team took the existing checker and methodically ported it to Go, keeping the logic structurally identical to the JavaScript version. The &lt;a href="https://devblogs.microsoft.com/typescript/announcing-typescript-7-0-rc/" rel="noopener noreferrer"&gt;RC announcement&lt;/a&gt; puts it plainly: the Go codebase "was methodically ported from our existing implementation rather than rewritten from scratch, and its type-checking logic is structurally identical to TypeScript 6.0."&lt;/p&gt;

&lt;p&gt;That's the part worth internalizing before anything else. Your types don't change. The inference you rely on, the errors you see, the edge cases you've memorized: all the same. Microsoft ran the two implementations against roughly 20,000 test cases. About 6,000 of those flag at least one error under TypeScript 6, and in &lt;a href="https://devblogs.microsoft.com/typescript/progress-on-typescript-7-december-2025/" rel="noopener noreferrer"&gt;all but 74&lt;/a&gt; of them, TypeScript 7 flags an error too. This is a performance release wearing a major version number, not a language release.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Go, of all things
&lt;/h2&gt;

&lt;p&gt;This was the question everyone asked when &lt;a href="https://devblogs.microsoft.com/typescript/typescript-native-port/" rel="noopener noreferrer"&gt;the port was announced&lt;/a&gt; back in March 2025. Why not Rust? Why not C#, Microsoft's own language?&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/microsoft/typescript-go/discussions/411" rel="noopener noreferrer"&gt;design notes&lt;/a&gt; give a practical answer. The compiler does an unusually large amount of graph work, walking trees up and down through polymorphic nodes, and Go makes that ergonomic. It gives you control over memory layout and allocation without forcing every line of the codebase to think about ownership. And because a batch &lt;code&gt;tsc&lt;/code&gt; run terminates when it's done, the compiler can lean on Go's garbage collector cheaply, or skip collection almost entirely, since the process exits and the OS reclaims everything anyway.&lt;/p&gt;

&lt;p&gt;There's also the boring reason, which is usually the real one: idiomatic Go looks a lot like the existing TypeScript codebase. Functions and data structures, not deep object hierarchies. That resemblance is what made porting hundreds of thousands of lines tractable instead of a multi-year rewrite. &lt;a href="https://en.wikipedia.org/wiki/Anders_Hejlsberg" rel="noopener noreferrer"&gt;Anders Hejlsberg&lt;/a&gt;, who has been on this since the start, has described Go as the lowest-level language that still offered native code, a garbage collector, and good concurrency without fighting the transition from TypeScript's coding style. None of that made Rust the wrong tool in general; it's having its own moment across JavaScript tooling, from bundlers to the &lt;a href="https://morello.dev/blog/pnpm-v12-rust-rewrite" rel="noopener noreferrer"&gt;Rust rewrite of pnpm&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How much faster is TypeScript 7?
&lt;/h2&gt;

&lt;p&gt;Here's what the speedup looks like on real projects, measured for the &lt;a href="https://devblogs.microsoft.com/typescript/announcing-typescript-7-0/" rel="noopener noreferrer"&gt;7.0 release&lt;/a&gt; against TypeScript 6:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Project&lt;/th&gt;
&lt;th&gt;TS 6&lt;/th&gt;
&lt;th&gt;TS 7&lt;/th&gt;
&lt;th&gt;Speedup&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;VS Code&lt;/td&gt;
&lt;td&gt;125.7s&lt;/td&gt;
&lt;td&gt;10.6s&lt;/td&gt;
&lt;td&gt;11.9x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sentry&lt;/td&gt;
&lt;td&gt;139.8s&lt;/td&gt;
&lt;td&gt;15.7s&lt;/td&gt;
&lt;td&gt;8.9x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bluesky&lt;/td&gt;
&lt;td&gt;24.3s&lt;/td&gt;
&lt;td&gt;2.8s&lt;/td&gt;
&lt;td&gt;8.7x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Playwright&lt;/td&gt;
&lt;td&gt;12.8s&lt;/td&gt;
&lt;td&gt;1.47s&lt;/td&gt;
&lt;td&gt;8.7x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tldraw&lt;/td&gt;
&lt;td&gt;11.2s&lt;/td&gt;
&lt;td&gt;1.46s&lt;/td&gt;
&lt;td&gt;7.7x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A full check of the VS Code codebase went from over two minutes to about ten seconds. Memory use dropped too, somewhere between 6% and 26% depending on the project.&lt;/p&gt;

&lt;p&gt;The build-time number is the one that gets quoted, but the one you'll feel every day is editor responsiveness. Loading the VS Code project in the editor used to take &lt;a href="https://devblogs.microsoft.com/typescript/typescript-native-port/" rel="noopener noreferrer"&gt;around 9.6 seconds&lt;/a&gt; before the language service was ready; on the native port it's about 1.2. That's the gap between "the squiggles show up when I need them" and "I've already tabbed away."&lt;/p&gt;

&lt;p&gt;Where does the speed come from? Two places. Native code is faster than &lt;a href="https://morello.dev/blog/five-things-you-might-not-know-about-javascript" rel="noopener noreferrer"&gt;JIT-compiled JavaScript&lt;/a&gt; for this kind of work, and Go lets the compiler use &lt;a href="https://devblogs.microsoft.com/typescript/announcing-typescript-7-0/" rel="noopener noreferrer"&gt;shared-memory multithreading&lt;/a&gt; that the single-threaded JavaScript version simply couldn't. Build mode now runs multi-threaded on a single project and compiles multiple projects in parallel, so the whole graph gets to use your cores instead of one of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to install TypeScript 7
&lt;/h2&gt;

&lt;p&gt;At GA, the compiler ships from the package you already have. Install the latest &lt;code&gt;typescript&lt;/code&gt; and the binary is still &lt;code&gt;tsc&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; typescript
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you were on the preview before GA, note that the names have settled. During the preview the package was &lt;code&gt;@typescript/native-preview&lt;/code&gt; and the binary was &lt;code&gt;tsgo&lt;/code&gt;, run like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx tsgo &lt;span class="nt"&gt;--project&lt;/span&gt; ./src/tsconfig.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From the RC onward that folds back into the normal &lt;code&gt;typescript&lt;/code&gt; package and the &lt;code&gt;tsc&lt;/code&gt; command you've always used, so most projects change a version number and nothing else. The &lt;a href="https://github.com/microsoft/typescript-go" rel="noopener noreferrer"&gt;typescript-go repo&lt;/a&gt; that housed the port is expected to merge back into the main &lt;code&gt;microsoft/TypeScript&lt;/code&gt; repository over time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 6.0 / 7.0 split
&lt;/h2&gt;

&lt;p&gt;The version jump from 5.x is deliberate, and it comes with a plan. TypeScript &lt;a href="https://devblogs.microsoft.com/typescript/announcing-typescript-6-0/" rel="noopener noreferrer"&gt;6.0&lt;/a&gt;, released in March 2026, is the &lt;a href="https://devblogs.microsoft.com/typescript/progress-on-typescript-7-december-2025/" rel="noopener noreferrer"&gt;final JavaScript-based version&lt;/a&gt;. There's no 6.1 on the roadmap, only patch releases for security fixes or serious regressions. It exists as the bridge: 6.0 shipped the new defaults and marked a set of things deprecated, giving you a release to clean up against while still running the old, familiar implementation.&lt;/p&gt;

&lt;p&gt;TypeScript 7.0 then adopts those 6.0 defaults and turns the deprecations into &lt;a href="https://devblogs.microsoft.com/typescript/announcing-typescript-7-0/" rel="noopener noreferrer"&gt;hard errors&lt;/a&gt;. A few compiler options are gone or changed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;target: es5&lt;/code&gt; is removed.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;baseUrl&lt;/code&gt; is no longer supported.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;types&lt;/code&gt; now defaults to &lt;code&gt;[]&lt;/code&gt; instead of pulling in everything under &lt;code&gt;node_modules/@types&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rootDir&lt;/code&gt; defaults to &lt;code&gt;./&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a jump straight to 7.0 surfaces too much at once, there's a side-by-side &lt;code&gt;@typescript/typescript6&lt;/code&gt; package so you can keep the old compiler available while you migrate.&lt;/p&gt;

&lt;h2&gt;
  
  
  What doesn't work in TypeScript 7 yet
&lt;/h2&gt;

&lt;p&gt;This is the part to be honest about, because it's where the release will actually bite.&lt;/p&gt;

&lt;p&gt;The editor story moved to a &lt;a href="https://microsoft.github.io/language-server-protocol/" rel="noopener noreferrer"&gt;Language Server Protocol&lt;/a&gt; architecture, which is a good long-term direction. But TypeScript 7 does not yet expose a stable programmatic API, and a lot of the ecosystem is built on that API. The concrete fallout: template type-checking for &lt;a href="https://vuejs.org" rel="noopener noreferrer"&gt;Vue&lt;/a&gt;, &lt;a href="https://svelte.dev" rel="noopener noreferrer"&gt;Svelte&lt;/a&gt;, &lt;a href="https://astro.build" rel="noopener noreferrer"&gt;Astro&lt;/a&gt;, MDX, and &lt;a href="https://angular.dev" rel="noopener noreferrer"&gt;Angular&lt;/a&gt; isn't supported on TypeScript 7 yet. Those tools reach into the compiler's internals to type-check the parts of your app that live outside &lt;code&gt;.ts&lt;/code&gt; files, and until the public API stabilizes, they can't do that against the Go implementation.&lt;/p&gt;

&lt;p&gt;So if you're building a Vue or Svelte app, or anything that leans on a framework's language tooling, the 10x number is real but you can't fully collect it yet. The compiler is ready; the layer your framework plugs into isn't. This is worth checking against your own stack before you upgrade, since it's the difference between a version bump and a broken editor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should you upgrade?
&lt;/h2&gt;

&lt;p&gt;If you've got a plain TypeScript project, a library, a Node service, a build script, the answer is easy: yes, and soon. The type checking is the same, the CI step gets dramatically faster, and the migration is mostly a version bump plus whatever 6.0 already warned you about. Test the bump in an isolated &lt;a href="https://morello.dev/blog/git-worktrees-are-underrated" rel="noopener noreferrer"&gt;git worktree&lt;/a&gt; so your main checkout keeps building while you shake out any surprises.&lt;/p&gt;

&lt;p&gt;If you're in a framework app that depends on template type-checking, wait and watch. Track the &lt;a href="https://github.com/microsoft/typescript-go" rel="noopener noreferrer"&gt;programmatic API work&lt;/a&gt;, see when your framework's tooling lands support, and upgrade then. There's no rush and no downside to letting it settle.&lt;/p&gt;

&lt;p&gt;Either way, this is the most significant thing to happen to TypeScript's infrastructure in years. The type system stayed still on purpose so that the thing underneath it could get an order of magnitude faster. It's the same kind of deliberate bet &lt;a href="https://morello.dev/blog/solidjs-2-react-developers-first-look" rel="noopener noreferrer"&gt;SolidJS 2.0 is making with async&lt;/a&gt;: keep the surface still, rebuild the engine.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>The HTTP QUERY Method: Safe Reads with a Body</title>
      <dc:creator>Dennis Morello</dc:creator>
      <pubDate>Tue, 07 Jul 2026 18:50:16 +0000</pubDate>
      <link>https://dev.to/morellodev/the-new-http-query-method-2ek5</link>
      <guid>https://dev.to/morellodev/the-new-http-query-method-2ek5</guid>
      <description>&lt;p&gt;If you've ever built a search endpoint, you've hit this wall. Your query has filters, sort orders, a nested set of facets, maybe a geo bounding box. It doesn't fit in a URL, and cramming it into query string params is ugly and fragile. So you reach for &lt;code&gt;POST /search&lt;/code&gt;, send the whole thing as a JSON body, and quietly accept that you've just lied about what the request does. It's not creating anything. It's a read. But POST is the only tool that lets you attach a body without fighting the platform.&lt;/p&gt;

&lt;p&gt;That gap finally got filled. In June 2026 the IETF published &lt;a href="https://www.rfc-editor.org/info/rfc10008/" rel="noopener noreferrer"&gt;RFC 10008&lt;/a&gt;, which defines the HTTP QUERY method: a new verb built for exactly this case.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two bad options
&lt;/h2&gt;

&lt;p&gt;Every read that needs structured input has been stuck choosing between GET and POST, and both are wrong in their own way.&lt;/p&gt;

&lt;p&gt;GET is the semantically correct choice. It's &lt;a href="https://www.rfc-editor.org/rfc/rfc9110#name-safe-methods" rel="noopener noreferrer"&gt;safe&lt;/a&gt; (the client isn't asking to change anything), it's &lt;a href="https://www.rfc-editor.org/rfc/rfc9110#name-idempotent-methods" rel="noopener noreferrer"&gt;idempotent&lt;/a&gt; (retrying it is fine), and it's cacheable. The problem is the body. RFC 9110 is explicit that &lt;a href="https://www.rfc-editor.org/rfc/rfc9110#name-get" rel="noopener noreferrer"&gt;content in a GET request has no defined semantics&lt;/a&gt;, and sending one may cause some implementations to reject the request. So your query has to live in the URI, where you run into unknown length limits across proxies and servers, encoding overhead, and the query landing in access logs and browser history.&lt;/p&gt;

&lt;p&gt;POST solves the body problem and creates a new one. It carries any payload you want, but it's neither safe nor idempotent by definition. Intermediaries won't cache it, clients won't retry it automatically after a dropped connection, and anything inspecting traffic has to assume the request might have side effects. You get the body, you lose everything that made the request honest.&lt;/p&gt;

&lt;p&gt;QUERY is the missing third option: a method that carries a body &lt;em&gt;and&lt;/em&gt; keeps the semantics of a read.&lt;/p&gt;

&lt;h2&gt;
  
  
  What QUERY actually is
&lt;/h2&gt;

&lt;p&gt;The spec, authored by Julian Reschke, &lt;a href="https://github.com/jasnell" rel="noopener noreferrer"&gt;James Snell&lt;/a&gt;, and Mike Bishop, describes it in one sentence:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A QUERY requests that the request target process the enclosed content in a safe and idempotent manner and then respond with the result of that processing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So a QUERY request looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="nf"&gt;QUERY&lt;/span&gt; &lt;span class="nn"&gt;/products&lt;/span&gt; &lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt;
&lt;span class="na"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;example.com&lt;/span&gt;
&lt;span class="na"&gt;Content-Type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;application/json&lt;/span&gt;
&lt;span class="na"&gt;Accept&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;application/json&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"filters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"keyboards"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"inStock"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sort"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"field"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"order"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"asc"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"page"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"size"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The three properties that matter:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's safe and idempotent.&lt;/strong&gt; The client isn't requesting a state change, and the request can be retried or repeated without worrying about partial effects. This is what POST could never promise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's cacheable.&lt;/strong&gt; A cache is allowed to store the response and use it to satisfy later QUERY requests. The catch is that the cache key has to include the request content, not just the URI, since the body is what distinguishes one query from another. Two QUERYs to the same URL with different bodies are different requests. In practice, getting a CDN to key on anything beyond the URI is its own fight, as I found out trying &lt;a href="https://morello.dev/blog/configuring-my-site-for-ai-discoverability" rel="noopener noreferrer"&gt;Accept-based content negotiation on Cloudflare&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The response isn't a representation of the URI.&lt;/strong&gt; Unlike GET, where the response &lt;em&gt;is&lt;/em&gt; the resource at that URL, a QUERY response is the result of running your query over some data scoped to the target. &lt;code&gt;GET /products&lt;/code&gt; returns the products resource; &lt;code&gt;QUERY /products&lt;/code&gt; returns whatever your query selected from it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The details that bite
&lt;/h2&gt;

&lt;p&gt;A few rules are worth knowing before you wire this up.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Content-Type&lt;/code&gt; is mandatory. The server MUST reject the request if the &lt;code&gt;Content-Type&lt;/code&gt; field is missing or inconsistent with the body. In practice you'll see a &lt;code&gt;400&lt;/code&gt; for a missing media type, a &lt;code&gt;415 Unsupported Media Type&lt;/code&gt; for one the server doesn't handle, and a &lt;code&gt;422 Unprocessable Content&lt;/code&gt; when the body parses fine but doesn't make sense as a query.&lt;/p&gt;

&lt;p&gt;There are two response headers that trip people up because they sound alike:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Content-Location&lt;/code&gt;&lt;/strong&gt; points at a resource representing the &lt;em&gt;results&lt;/em&gt; of this query. A client can GET that URL later to fetch the same results again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Location&lt;/code&gt;&lt;/strong&gt; points at a resource representing the &lt;em&gt;query itself&lt;/em&gt;. A client can GET that URL to re-run the query without resending the body, which is handy for turning a heavy query into a shareable link.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Results points to the answer; Location points to the question.&lt;/p&gt;

&lt;p&gt;The spec also nudges you away from HTTP range requests for pagination. Range semantics technically apply, but most query formats already have their own paging built in (think SQL's &lt;code&gt;FETCH FIRST&lt;/code&gt;), and that's the mechanism you should reach for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Can you use HTTP QUERY today?
&lt;/h2&gt;

&lt;p&gt;On the client, yes, already. The Fetch API and libraries like axios accept arbitrary method strings, so nothing is stopping you from sending one right now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/products&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="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;QUERY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;keyboards&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One gotcha before you do: pass the method in uppercase. &lt;a href="https://fetch.spec.whatwg.org/#concept-method-normalize" rel="noopener noreferrer"&gt;&lt;code&gt;fetch&lt;/code&gt; only normalizes the case&lt;/a&gt; of a fixed set of methods (&lt;code&gt;DELETE&lt;/code&gt;, &lt;code&gt;GET&lt;/code&gt;, &lt;code&gt;HEAD&lt;/code&gt;, &lt;code&gt;OPTIONS&lt;/code&gt;, &lt;code&gt;POST&lt;/code&gt;, &lt;code&gt;PUT&lt;/code&gt;), and QUERY isn't on it. Write &lt;code&gt;method: "query"&lt;/code&gt; and it goes out verbatim, which a case-sensitive server will reject.&lt;/p&gt;

&lt;p&gt;The friction is everywhere else in the stack. QUERY isn't a &lt;a href="https://fetch.spec.whatwg.org/#cors-safelisted-method" rel="noopener noreferrer"&gt;CORS-safelisted method&lt;/a&gt;, so cross-origin QUERY requests trigger a preflight &lt;code&gt;OPTIONS&lt;/code&gt;. Your server has to answer it with &lt;code&gt;Access-Control-Allow-Methods: QUERY&lt;/code&gt; or the browser blocks the real request. And anything that whitelists methods will reject QUERY until you tell it not to: reverse proxies, WAFs, API gateways, &lt;code&gt;limit_except&lt;/code&gt; blocks in nginx. The method passing through the wire is the easy part; the config that guards the wire is where you'll spend your time.&lt;/p&gt;

&lt;p&gt;Framework and server support is still landing. New HTTP methods don't come around often. The last one most people reached for was PATCH back in 2010, so the ecosystem moves slowly. Expect native routing helpers and middleware to fill in over the next couple of years rather than overnight.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should you rush to switch?
&lt;/h2&gt;

&lt;p&gt;Probably not, and there's no need to. Your &lt;code&gt;POST /search&lt;/code&gt; endpoints work and aren't going anywhere. QUERY is the more correct tool, not an urgent migration.&lt;/p&gt;

&lt;p&gt;What it does give you is a real answer to a question we've been hacking around for years. It's the same pattern the &lt;a href="https://morello.dev/blog/cross-origin-storage-api" rel="noopener noreferrer"&gt;Cross-Origin Storage API&lt;/a&gt; and &lt;a href="https://morello.dev/blog/webmcp-making-your-site-usable-by-ai-agents" rel="noopener noreferrer"&gt;WebMCP&lt;/a&gt; got right: the platform finally naming something we'd been faking. When you're designing a new read endpoint that needs a structured body, you now have a method that says exactly what it means: this is a safe, repeatable, cacheable read, and here's the query in the body where it belongs. That's worth reaching for on the next thing you build, even if the old endpoints stay put.&lt;/p&gt;

</description>
      <category>http</category>
      <category>webdev</category>
      <category>api</category>
      <category>backend</category>
    </item>
    <item>
      <title>SolidJS 2.0 for React Developers: A First Look</title>
      <dc:creator>Dennis Morello</dc:creator>
      <pubDate>Wed, 01 Jul 2026 22:00:00 +0000</pubDate>
      <link>https://dev.to/morellodev/solidjs-20-a-react-developers-first-look-at-signals-and-async-51bn</link>
      <guid>https://dev.to/morellodev/solidjs-20-a-react-developers-first-look-at-signals-and-async-51bn</guid>
      <description>&lt;p&gt;I've shipped React for the better part of a decade. Hooks, Suspense, Server Components, the whole tour. So when the &lt;strong&gt;SolidJS 2.0&lt;/strong&gt; beta showed up in my feed with the tagline &lt;a href="https://github.com/solidjs/solid/releases/tag/v2.0.0-beta.0" rel="noopener noreferrer"&gt;"The &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; is Over,"&lt;/a&gt; I rolled my eyes a little and then clicked anyway.&lt;/p&gt;

&lt;p&gt;I'm glad I did. Solid has been the framework React people admire from a distance for years, and 2.0 is the release that finally made me install it and poke at the reactivity model instead of just reading about it. This post is my honest first look: what's new, what a React developer will recognize, and where Solid quietly does something React can't.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is SolidJS 2.0?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.solidjs.com" rel="noopener noreferrer"&gt;SolidJS&lt;/a&gt; is a UI library with React-like JSX and a completely different engine underneath. Instead of re-rendering components and diffing a virtual DOM, it uses &lt;a href="https://docs.solidjs.com/concepts/intro-to-reactivity" rel="noopener noreferrer"&gt;&lt;strong&gt;fine-grained reactivity&lt;/strong&gt;&lt;/a&gt;: your component runs once, and updates flow directly to the exact DOM nodes that depend on a piece of state.&lt;/p&gt;

&lt;p&gt;The 2.0 line is currently in beta. The first public build, &lt;code&gt;v2.0.0-beta.0&lt;/code&gt;, landed on March 3, 2026, and the team skipped the alpha phase entirely because the milestones planned for it stopped feeling worth their own release. You can try it today:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm add solid-js@next
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The headline of the whole release is async. Solid's reactive graph now understands promises natively, and a lot of the API changes fall out of that one decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fine-grained reactivity, from a React brain
&lt;/h2&gt;

&lt;p&gt;Here's the mental model shift, and it's the thing to internalize before anything else makes sense.&lt;/p&gt;

&lt;p&gt;In React, state changes re-run your component function. React then builds a new virtual DOM tree and diffs it against the old one to figure out what actually changed. &lt;code&gt;useMemo&lt;/code&gt;, &lt;code&gt;useCallback&lt;/code&gt;, and dependency arrays exist to keep that re-render machine from doing too much work. The &lt;a href="https://react.dev/learn/react-compiler" rel="noopener noreferrer"&gt;React Compiler&lt;/a&gt; automates a lot of that memoization now, but the underlying model is the same: the component re-runs and React diffs the result.&lt;/p&gt;

&lt;p&gt;In Solid, the component body is setup code. It runs a single time. When you read a signal in your JSX, Solid records that specific dependency and wires it straight to the DOM. Change the signal later and only that text node or attribute updates. No component re-render, and no dependency array to keep honest.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Counter&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;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setCount&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createSignal&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="c1"&gt;// This whole function runs ONCE. Only the text node updates on click.&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&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;setCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tell for a React dev: &lt;code&gt;count&lt;/code&gt; is a function, not a value. You call &lt;code&gt;count()&lt;/code&gt; to read it. That call is what subscribes the surrounding computation to changes. Once it clicks, the absence of a dependency array stops feeling like something is missing and starts feeling like a bug class that no longer exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why async is the real story of SolidJS 2.0
&lt;/h2&gt;

&lt;p&gt;This is where 2.0 earns its version bump. In Solid 1.x, async data meant &lt;code&gt;createResource&lt;/code&gt; and wrapping things in &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt;. In 2.0, a derived computation can just return a promise, and the graph handles suspending and resuming for you.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Solid 1.x&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;data&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createResource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fetchUser&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Solid 2.0: the promise flows through the reactive graph&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createMemo&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;fetchUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Suspense gets replaced by &lt;code&gt;&amp;lt;Loading&amp;gt;&lt;/code&gt;, and the semantics are the part I actually care about. &lt;code&gt;&amp;lt;Loading&amp;gt;&lt;/code&gt; is scoped to &lt;em&gt;initial&lt;/em&gt; readiness. It shows a fallback while the subtree can't render anything yet, and then it stays out of your way. When &lt;code&gt;userId()&lt;/code&gt; changes and the data refetches, the UI doesn't tear itself down and flash a spinner. It holds the old content until the new content is ready.&lt;/p&gt;

&lt;p&gt;To show that a refresh is in flight, you reach for &lt;code&gt;isPending&lt;/code&gt;, which reports on pending reactive work without unmounting anything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createMemo&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;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listUsers&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;refreshing&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;isPending&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;users&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="p"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Show&lt;/span&gt; &lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;refreshing&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;RefreshIndicator&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Show&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Loading&lt;/span&gt; &lt;span class="na"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Spinner&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;UserList&lt;/span&gt; &lt;span class="na"&gt;users&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;users&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Loading&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&amp;gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;React 19 gets you most of the way here: put the update in a transition with &lt;a href="https://react.dev/reference/react/useTransition" rel="noopener noreferrer"&gt;&lt;code&gt;useTransition&lt;/code&gt;&lt;/a&gt;, and Suspense holds the old content on screen instead of flashing its fallback, with &lt;code&gt;isPending&lt;/code&gt; reporting the refresh. The difference is that React makes you opt into that at each call site, while Solid bakes the distinction between "we have nothing to show yet" and "we're refreshing what's already on screen" into the primitives themselves. I take this apart properly in a &lt;a href="https://morello.dev/blog/solidjs-2-async-data" rel="noopener noreferrer"&gt;follow-up deep dive on Solid 2.0's async data&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do mutations work in SolidJS 2.0?
&lt;/h2&gt;

&lt;p&gt;For a long time React had no blessed way to do writes, so you rolled your own or reached for a data library. React 19 changed that with &lt;a href="https://react.dev/blog/2024/12/05/react-19" rel="noopener noreferrer"&gt;Actions&lt;/a&gt; and the &lt;a href="https://react.dev/reference/react/useOptimistic" rel="noopener noreferrer"&gt;&lt;code&gt;useOptimistic&lt;/code&gt;&lt;/a&gt; hook, so this is a spot where the two frameworks have converged. Solid 2.0's version is &lt;code&gt;action&lt;/code&gt; for mutations, plus &lt;code&gt;createOptimistic&lt;/code&gt; and &lt;code&gt;createOptimisticStore&lt;/code&gt; for optimistic updates, with &lt;code&gt;refresh&lt;/code&gt; to revalidate afterward.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setMessages&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createOptimisticStore&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;chatServer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loadMessages&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;sendMessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="o"&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="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;setMessages&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;m&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;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&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="c1"&gt;// optimistic: show it instantly&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nx"&gt;chatServer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendMessage&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="c1"&gt;// await the server&lt;/span&gt;
  &lt;span class="nf"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// reconcile with the source of truth&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The generator function is doing real work here. Each &lt;code&gt;yield&lt;/code&gt; is a point where the action awaits, and the reactive system tracks the optimistic state until the real value lands and reconciles. Solid has been circling this for a while: &lt;code&gt;@solidjs/router&lt;/code&gt; already shipped &lt;a href="https://docs.solidjs.com/solid-router/reference/data-apis/create-async" rel="noopener noreferrer"&gt;&lt;code&gt;createAsync&lt;/code&gt;&lt;/a&gt; and &lt;code&gt;action&lt;/code&gt; for data loading and mutations. 2.0 takes that thinking and builds async and actions into the core reactive graph, so the patterns aren't router-specific anymore.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deterministic batching, and a gotcha
&lt;/h2&gt;

&lt;p&gt;One behavioral change will trip you up if you're not ready for it. Writes are batched on a microtask, and reads don't reflect a write until the batch flushes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setCount&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createSignal&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="nf"&gt;setCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// still 0: the update is queued on the microtask&lt;/span&gt;
&lt;span class="nf"&gt;flush&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// apply queued updates synchronously&lt;/span&gt;
&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// now 1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;flush()&lt;/code&gt; forces the queue to apply right away when you need to read the result immediately, like focusing an input after a state change. Coming from React's synchronous batching it takes a moment to adjust, but the model is predictable, and that matters once async is woven through everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  The renames a React developer should know
&lt;/h2&gt;

&lt;p&gt;Solid 2.0 is a major version, so it cleans house. Since this is beta, some of these identifiers could still shift before stable, but the direction is set, and the &lt;a href="https://github.com/solidjs/solid/blob/next/documentation/solid-2.0/MIGRATION.md" rel="noopener noreferrer"&gt;migration guide&lt;/a&gt; tracks where things stand today. The changes that matter most coming from React:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; → &lt;code&gt;&amp;lt;Loading&amp;gt;&lt;/code&gt;&lt;/strong&gt;, with the initial-readiness semantics above. &lt;code&gt;&amp;lt;SuspenseList&amp;gt;&lt;/code&gt; becomes &lt;code&gt;&amp;lt;Reveal&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;createEffect&lt;/code&gt; is split&lt;/strong&gt; into a compute phase (what to track) and an apply phase (what to run with the result). Instead of reading signals inside one callback, you pass the two separately:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;  &lt;span class="c1"&gt;// Solid 1.x&lt;/span&gt;
  &lt;span class="nf"&gt;createEffect&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;apiCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;signalB&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;

  &lt;span class="c1"&gt;// Solid 2.0&lt;/span&gt;
  &lt;span class="nf"&gt;createEffect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;signalB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;apiCall&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;createEffect&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="nf"&gt;signalA&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nf"&gt;signalB&lt;/span&gt;&lt;span class="p"&gt;()],&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;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;apiCall&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="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you've ever shipped a bug because a value was in a &lt;code&gt;useEffect&lt;/code&gt; body but missing from the dependency array, separating the two is a direct answer to that. And in practice you reach for &lt;code&gt;createEffect&lt;/code&gt; far less than you did in 1.x, since async and derived state now cover a lot of what used to send you to an effect.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;onMount&lt;/code&gt; → &lt;code&gt;onSettled&lt;/code&gt;&lt;/strong&gt; (the closest replacement rather than a straight rename), reflecting the async-aware lifecycle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;&amp;lt;Index&amp;gt;&lt;/code&gt; → &lt;code&gt;&amp;lt;For keyed={false}&amp;gt;&lt;/code&gt;&lt;/strong&gt;; in that non-keyed mode the row is passed as an accessor, matching the old &lt;code&gt;&amp;lt;Index&amp;gt;&lt;/code&gt; stability model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Store setters are draft-first by default.&lt;/strong&gt; You mutate a draft instead of threading a path. This is 1.x's &lt;code&gt;produce&lt;/code&gt; behavior promoted to the default, so you no longer wrap the callback, with a &lt;code&gt;storePath&lt;/code&gt; helper as the opt-in escape hatch for the old path style:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;  &lt;span class="c1"&gt;// 2.0 default: mutate the draft&lt;/span&gt;
  &lt;span class="nf"&gt;setStore&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="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;todos&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;done&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="c1"&gt;// Legacy path style, opt-in&lt;/span&gt;
  &lt;span class="nf"&gt;setStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;storePath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;todos&lt;/span&gt;&lt;span class="dl"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;done&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;classList&lt;/code&gt; is folded into &lt;code&gt;class&lt;/code&gt;&lt;/strong&gt;, which now takes strings, arrays, and objects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;use:&lt;/code&gt; directives are removed&lt;/strong&gt; in favor of &lt;code&gt;ref&lt;/code&gt; directive factories.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context is simpler.&lt;/strong&gt; The context is directly usable as the provider, so &lt;code&gt;Context.Provider&lt;/code&gt; is gone. A context created without a default is typed as its value instead of &lt;code&gt;T | undefined&lt;/code&gt;, and &lt;code&gt;useContext&lt;/code&gt; throws &lt;code&gt;ContextNotFoundError&lt;/code&gt; when there's no provider above it; a context created with a default still returns that default:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Theme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createContext&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// no default&lt;/span&gt;
  &lt;span class="c1"&gt;// 2.0: the context is its own provider&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Theme&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"dark"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Theme&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Signals are heading toward the standard
&lt;/h2&gt;

&lt;p&gt;The word "signals" is everywhere now, and it's worth knowing why. There's a &lt;a href="https://github.com/tc39/proposal-signals" rel="noopener noreferrer"&gt;TC39 Signals proposal&lt;/a&gt; at Stage 1, with design input from the people behind Angular, Vue, Svelte, Preact, Qwik, MobX, and Solid, among others. Solid's creator Ryan Carniato has written about fine-grained reactivity for years (his &lt;a href="https://dev.to/this-is-learning/the-evolution-of-signals-in-javascript-8ob"&gt;evolution of signals in JavaScript&lt;/a&gt; piece is a good entry point), and Solid is one of the frameworks feeding into the proposal.&lt;/p&gt;

&lt;p&gt;Worth being precise here, because it's easy to overstate: Solid 2.0 does not ship the TC39 API as its core. The proposal is synchronous, and Solid 2.0's whole thing is &lt;em&gt;async&lt;/em&gt; reactivity, which goes further than what's currently on the table. Solid influences and tracks the standard rather than implementing it. But the fact that the primitive Solid has bet on for years is now a serious effort to fold it into &lt;a href="https://morello.dev/blog/five-things-you-might-not-know-about-javascript" rel="noopener noreferrer"&gt;the JavaScript language itself&lt;/a&gt; tells you the industry is drifting toward the model React chose not to adopt. It's the same trajectory &lt;a href="https://morello.dev/blog/typescript-7-is-here" rel="noopener noreferrer"&gt;TypeScript's Go rewrite&lt;/a&gt; just traced: a bet on a different model that looked niche until it wasn't. Svelte 5 already rebuilt its reactivity around signals with its &lt;a href="https://svelte.dev/blog/runes" rel="noopener noreferrer"&gt;runes&lt;/a&gt; API, and it's unlikely to be the last framework to make the jump.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should you drop React? No. But watch this closely
&lt;/h2&gt;

&lt;p&gt;I'm not migrating my day job to Solid this quarter, and I don't think you should either. It's a beta, the ecosystem is smaller than React's, and API names are still moving. That's the honest read.&lt;/p&gt;

&lt;p&gt;But 2.0 is the most interesting frontend release I've looked at in a while, precisely because it isn't chasing React. It takes fine-grained reactivity seriously and follows the idea all the way through to async, and the result is a set of primitives that make the hard parts of React (stale closures, dependency arrays, Suspense re-triggering, figuring out where mutations should live) feel like problems you can just stop having.&lt;/p&gt;

&lt;p&gt;If you write React and you've never actually run Solid, this is the release to spend a weekend on. Spin up a project with &lt;code&gt;solid-js@next&lt;/code&gt;, build something small with &lt;code&gt;createSignal&lt;/code&gt; and &lt;code&gt;&amp;lt;Loading&amp;gt;&lt;/code&gt;, and pay attention to how much of your usual React vigilance you can put down.&lt;/p&gt;

&lt;p&gt;The beta is live at &lt;a href="https://github.com/solidjs/solid/releases/tag/v2.0.0-beta.0" rel="noopener noreferrer"&gt;github.com/solidjs/solid&lt;/a&gt;, and the &lt;a href="https://github.com/solidjs/solid/discussions/2596" rel="noopener noreferrer"&gt;announcement discussion&lt;/a&gt; is worth reading in full if any of this piqued your interest.&lt;/p&gt;

</description>
      <category>solidjs</category>
      <category>react</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
