<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Dennis Morello - Lead Frontend Engineer &amp; Open Source Maintainer</title><description>Dennis Morello, Lead Frontend Engineer from Milan. 10+ years building accessible web interfaces and open-source libraries like React Awesome Reveal.</description><link>https://morello.dev</link><language>en-us</language><atom:link href="https://morello.dev/rss.xml" rel="self" type="application/rss+xml"/><item><title>WebSkill: Agent Skills in the Browser</title><link>https://morello.dev/blog/webskill-agent-skills-in-the-browser</link><guid isPermaLink="true">https://morello.dev/blog/webskill-agent-skills-in-the-browser</guid><description>WebSkill (webskill.ai) is a draft proposal to run Agent Skills in the browser, using OPFS and Web Workers. How it works and where the claims are thin.</description><pubDate>Tue, 29 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;WebSkill is a draft proposal, published in July 2026, to run &lt;a href=&quot;https://agentskills.io/specification&quot;&gt;Agent Skills&lt;/a&gt; entirely inside the browser instead of on a server. A skill is stored in the page’s Origin Private File System (OPFS), its scripts run in a Web Worker, and an LLM picks and calls it without a backend in the loop. The project lives at &lt;a href=&quot;https://webskill.ai/&quot;&gt;webskill.ai&lt;/a&gt;, the editor is Chunhui Mo of Huawei, and it is very early.&lt;/p&gt;
&lt;p&gt;I went in expecting a spec. What I found is a proposal, a static demo, an npm SDK, and a few ideas that are worth borrowing even if &lt;code&gt;navigator.webskill&lt;/code&gt; never ships. Here’s what it is, based on the &lt;a href=&quot;https://github.com/kevinmoch/web-skill&quot;&gt;project’s README&lt;/a&gt; and docs, and where I’d hold back.&lt;/p&gt;
&lt;h2 id=&quot;what-is-webskill&quot;&gt;What is WebSkill?&lt;/h2&gt;
&lt;p&gt;WebSkill is a runtime for Agent Skills that lives in the frontend. In the README’s words, it is “a frontend-native skill running directly in the browser” and “a declarative contract.” The goal is to skip the usual setup, where a skill sits on a Node.js server or in a container sandbox and your data travels there and back. Not to be confused with WebXSkill, a Microsoft Research paper on &lt;a href=&quot;https://arxiv.org/abs/2604.13318&quot;&gt;skill learning for web agents&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;An Agent Skill, if you haven’t met one (the &lt;a href=&quot;/blog/hermes-agent-for-beginners&quot;&gt;skills Hermes Agent writes for itself&lt;/a&gt; use this same format), is a folder with a &lt;code&gt;SKILL.md&lt;/code&gt; file: YAML frontmatter (a &lt;code&gt;name&lt;/code&gt; up to 64 characters and a &lt;code&gt;description&lt;/code&gt; up to 1024) plus Markdown instructions, and optional &lt;code&gt;scripts/&lt;/code&gt;, &lt;code&gt;references/&lt;/code&gt; and &lt;code&gt;assets/&lt;/code&gt; directories. The &lt;a href=&quot;https://agentskills.io/specification&quot;&gt;Agent Skills spec&lt;/a&gt; describes three loading tiers so the model doesn’t pay for everything up front: about 100 tokens of metadata per skill at startup, the full &lt;code&gt;SKILL.md&lt;/code&gt; body when a skill activates, and other files only when needed. WebSkill keeps that structure and says it implements the protocol in TypeScript.&lt;/p&gt;
&lt;p&gt;The differences are in where things live:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Traditional skill&lt;/th&gt;
&lt;th&gt;WebSkill&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Runs on&lt;/td&gt;
&lt;td&gt;Node.js or a cloud server&lt;/td&gt;
&lt;td&gt;A Web Worker in the browser&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stored in&lt;/td&gt;
&lt;td&gt;The server’s filesystem&lt;/td&gt;
&lt;td&gt;OPFS, per origin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scripts&lt;/td&gt;
&lt;td&gt;Python, Bash, JavaScript&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.ts&lt;/code&gt; or &lt;code&gt;.js&lt;/code&gt; only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Isolation&lt;/td&gt;
&lt;td&gt;Container sandbox&lt;/td&gt;
&lt;td&gt;Worker thread&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shipped with&lt;/td&gt;
&lt;td&gt;A server deployment&lt;/td&gt;
&lt;td&gt;The web app itself&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scope&lt;/td&gt;
&lt;td&gt;Shared by every user&lt;/td&gt;
&lt;td&gt;Local to one user’s browser&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&quot;how-do-agent-skills-run-in-the-browser-with-webskill&quot;&gt;How do Agent Skills run in the browser with WebSkill?&lt;/h2&gt;
&lt;p&gt;Three pieces do the work, going by the README’s architecture diagram and its sandbox section. An on-device LLM routes the request and picks a skill. The skill’s script executes in a Worker. And when the script needs to touch the page, it calls a &lt;a href=&quot;/blog/webmcp-making-your-site-usable-by-ai-agents&quot;&gt;WebMCP&lt;/a&gt; tool.&lt;/p&gt;
&lt;p&gt;Script files have three rules. They must be &lt;code&gt;.ts&lt;/code&gt; or &lt;code&gt;.js&lt;/code&gt;, export a function named &lt;code&gt;run&lt;/code&gt;, and provide an &lt;code&gt;inputSchema&lt;/code&gt; so the model knows what arguments to pass. You can write that schema three ways: as plain JSON Schema, inferred from a TypeScript &lt;code&gt;interface&lt;/code&gt;, or inferred from JSDoc. The return value has to follow the MCP result shape, an object with a &lt;code&gt;content&lt;/code&gt; array.&lt;/p&gt;
&lt;p&gt;Skills call tools with two prefixes in &lt;code&gt;SKILL.md&lt;/code&gt;: &lt;code&gt;endpoint:toolName&lt;/code&gt; for a tool served through the standard MCP TypeScript SDK over a &lt;code&gt;MessageChannel&lt;/code&gt;, and &lt;code&gt;mcp#toolName&lt;/code&gt; for tools registered through the browser’s WebMCP API. The README’s WebMCP examples use &lt;code&gt;navigator.modelContext&lt;/code&gt;; Chrome’s current &lt;a href=&quot;https://developer.chrome.com/docs/ai/webmcp/imperative-api&quot;&gt;imperative API docs&lt;/a&gt; use &lt;code&gt;document.modelContext&lt;/code&gt; throughout, so expect that snippet to need updating.&lt;/p&gt;
&lt;p&gt;There is also a second mode. Instead of persistent skills in OPFS, a page can declare &lt;strong&gt;page-level skills&lt;/strong&gt; through MCP: the main thread acts as an MCP server (&lt;code&gt;registerPrompt&lt;/code&gt; for the instructions, &lt;code&gt;registerTool&lt;/code&gt; for scripts, &lt;code&gt;registerResource&lt;/code&gt; for reference files), and the Worker connects as a client. Open a product page and the assistant gains an inventory-check skill; navigate away and it disappears. The README argues this beats progressive disclosure once you have many skills, since only the skills for the current page are in play.&lt;/p&gt;
&lt;p&gt;When the model lacks a parameter, WebSkill doesn’t ask in plain chat. It emits a JSON Schema and a “Generative UI” layer renders a form. A &lt;code&gt;UIBridge&lt;/code&gt; adapter keeps the runtime independent of the renderer, and the README names &lt;code&gt;json-render-react&lt;/code&gt;, Vercel AI SDK, OpenUI and A2UI as candidates.&lt;/p&gt;
&lt;h2 id=&quot;what-is-navigatorwebskill&quot;&gt;What is &lt;code&gt;navigator.webskill&lt;/code&gt;?&lt;/h2&gt;
&lt;p&gt;It is the proposed browser API, and it exists only as a Web IDL draft in the README. The proposal adds a read-only property to &lt;code&gt;Navigator&lt;/code&gt; with three groups of methods:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Discovery:&lt;/strong&gt; &lt;code&gt;discover&lt;/code&gt; (list skills in a directory), &lt;code&gt;read&lt;/code&gt; (load one), &lt;code&gt;validate&lt;/code&gt; (check a skill and return a &lt;code&gt;ValidationReport&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Runtime:&lt;/strong&gt; &lt;code&gt;run(prompt)&lt;/code&gt;, which takes natural language and returns a &lt;code&gt;RuntimeRun&lt;/code&gt; with status and trace.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Manager:&lt;/strong&gt; &lt;code&gt;install&lt;/code&gt; (from a URL such as a Git repo) and &lt;code&gt;uninstall&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The usage example is short:&lt;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; ws&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; navigator&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;.webskills;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; catalog&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; ws&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;.discover&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;&apos;/skills&apos;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; run&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; ws&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;.run&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;&apos;Use calculator to compute 2+3&apos;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;console&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;.log&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;run&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;.status&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; run&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;trace&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;length&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;await&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; ws&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;.install&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;&apos;https://github.com/me/skills.git&apos;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice the property is &lt;code&gt;navigator.webskill&lt;/code&gt; in the prose and &lt;code&gt;navigator.webskills&lt;/code&gt; in the code. That’s a small slip, but it tells you how settled the draft is. The README also says the ambition is standardization; I found no browser vendor response or standards-group adoption while researching, so for now treat the API as an idea.&lt;/p&gt;
&lt;p&gt;What you can use today is the SDK: &lt;a href=&quot;https://www.npmjs.com/package/@webskill/sdk&quot;&gt;&lt;code&gt;@webskill/sdk&lt;/code&gt;&lt;/a&gt; is MIT-licensed, described as a “browser/Node agent skill runtime,” and sat at 0.25.0 when I checked, first published in July 2026. Version 0.x means expect churn.&lt;/p&gt;
&lt;h2 id=&quot;is-the-privacy-story-real&quot;&gt;Is the privacy story real?&lt;/h2&gt;
&lt;p&gt;Partly, and the README hedges it itself. The pitch is that skill files sit in &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system&quot;&gt;OPFS&lt;/a&gt;, which is private to the origin and, per MDN, “not visible to the user like the regular file system.” That’s Baseline Widely available, with support in every major engine since March 2023, so it’s a safe foundation. Running scripts in a Worker is also sound as far as it goes: MDN says a worker can’t “directly affect the parent page,” including the DOM.&lt;/p&gt;
&lt;p&gt;But the README goes further than the platform does. It credits OPFS with “blocking all unauthorized network requests,” but OPFS is a storage API, and MDN states that workers “can make network requests using the &lt;code&gt;fetch()&lt;/code&gt; or &lt;code&gt;XMLHttpRequest&lt;/code&gt; APIs.” A Worker keeps a script off your DOM and global variables. It does not, by itself, stop a script from sending data somewhere.&lt;/p&gt;
&lt;p&gt;The enforcement is in the WebSkill runtime instead. The product docs describe a network policy that defaults to “Deny all,” with “Allow all” and “Whitelist” as the other options, and the SDK’s worker bootstrap enforces it by replacing &lt;code&gt;fetch&lt;/code&gt;, &lt;code&gt;XMLHttpRequest&lt;/code&gt;, &lt;code&gt;WebSocket&lt;/code&gt; and &lt;code&gt;EventSource&lt;/code&gt; inside the worker. That’s a userland patch, not a platform boundary, and I haven’t seen anyone outside the project test it.&lt;/p&gt;
&lt;p&gt;The comparison table promises “zero outward data transmission” with no conditions. An earlier section admits the catch in a parenthetical: “assuming an on-device model is used.” The product docs list OpenAI-compatible, Anthropic and Google providers next to a “Chrome built-in (experimental)” option that “runs entirely on this device through the browser Prompt API.” Pick a hosted model and your page content goes to that vendor like it would anywhere else.&lt;/p&gt;
&lt;p&gt;The on-device route is real, since the &lt;a href=&quot;https://developer.chrome.com/docs/ai/prompt-api&quot;&gt;Prompt API&lt;/a&gt; is stable in desktop Chrome 148 (Edge has it in developer preview). It needs at least 22 GB of free space plus either more than 4 GB of VRAM or 16 GB of RAM and 4 CPU cores. And WebSkill’s own docs add the catch: the built-in model “cannot call tools, so skills that run scripts will not work with it.” Today, the setup that keeps data local is the one that can’t run skills.&lt;/p&gt;
&lt;p&gt;The README does add one safeguard I like: sensitive DOM actions and file reads or writes trigger a native authorization prompt, so a human clicks to approve. WebMCP has the same instinct in &lt;a href=&quot;https://developer.chrome.com/docs/ai/webmcp/secure-tools&quot;&gt;&lt;code&gt;consequentialHint&lt;/code&gt;&lt;/a&gt;, which lets the agent or browser ask for user confirmation before a high-stakes tool runs.&lt;/p&gt;
&lt;h2 id=&quot;should-you-use-webskill&quot;&gt;Should you use WebSkill?&lt;/h2&gt;
&lt;p&gt;Not in production, and not as a bet on a browser API. There’s a proposal with one named editor, a static demo the README says “doesn’t involve real AI execution,” and an SDK at 0.25.0. The repo had 8 stars when I looked, and it was created in April 2026.&lt;/p&gt;
&lt;p&gt;I would still read it, for two reasons. First, the layering is a sensible answer to a real problem. In July I wrote about &lt;a href=&quot;/blog/webmcp-making-your-site-usable-by-ai-agents&quot;&gt;WebMCP&lt;/a&gt;, which gives an agent typed tools on a page. The catch I skipped over is that hundreds of tool descriptions in one context is its own problem. WebSkill’s answer, a skill layer that decides which tools matter and loads them on demand, fills the gap between “the page has tools” and “the model knows when to use them.”&lt;/p&gt;
&lt;p&gt;Second, page-scoped skills are a neat idea on their own. Skills that appear with the page and vanish when you leave it fit how browsers already work, and they need no install step. My earlier post on &lt;a href=&quot;/blog/configuring-my-site-for-ai-discoverability&quot;&gt;making a site legible to AI&lt;/a&gt; covered the read side and WebMCP covered actions; skills would give an agent the know-how to combine the two.&lt;/p&gt;
&lt;p&gt;If you want to try it, the demo at &lt;a href=&quot;https://webskill.ai/demo&quot;&gt;webskill.ai/demo&lt;/a&gt; walks through a sample project-management app, and the SDK is one &lt;code&gt;pnpm add @webskill/sdk&lt;/code&gt; away. I’d wait for the spec to settle, and for someone other than the author to test the sandbox claims, before building on it.&lt;/p&gt;
</content:encoded><category>ai</category><category>webdev</category><category>javascript</category><category>mcp</category><author>Dennis Morello</author></item><item><title>htmx 4: What&apos;s New, What Breaks, Why It&apos;s Not latest</title><link>https://morello.dev/blog/htmx-4</link><guid isPermaLink="true">https://morello.dev/blog/htmx-4</guid><description>htmx 4.0 is out: fetch() internals, explicit inheritance, no more history cache. What breaks, how to migrate, and why 2.x stays latest on npm until early 2027.</description><pubDate>Fri, 28 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;htmx 4.0.0 &lt;a href=&quot;https://four.htmx.org/announcements/2026-08-28-htmx-4.0.0-is-released&quot;&gt;shipped today, August 28, 2026&lt;/a&gt;. The ajax core now runs on &lt;code&gt;fetch()&lt;/code&gt; instead of &lt;code&gt;XMLHttpRequest&lt;/code&gt;, attribute inheritance is explicit, the back button issues a real request instead of restoring a cached snapshot, and error responses can be routed by status code. It is a major release that breaks real things, on purpose. And it comes with an unusual instruction: nobody has to move. On npm, 2.x keeps the &lt;code&gt;latest&lt;/code&gt; dist-tag until early 2027, 4.0 lives under &lt;code&gt;next&lt;/code&gt;, and htmx 2 stays supported indefinitely.&lt;/p&gt;
&lt;h2 id=&quot;tldr&quot;&gt;TL;DR&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Every request htmx makes now goes through &lt;code&gt;fetch()&lt;/code&gt;; &lt;code&gt;XMLHttpRequest&lt;/code&gt; is out, along with the XHR-specific events.&lt;/li&gt;
&lt;li&gt;Attribute inheritance is explicit: attributes like &lt;code&gt;hx-confirm&lt;/code&gt; reach descendants only when you add the &lt;code&gt;:inherited&lt;/code&gt; modifier.&lt;/li&gt;
&lt;li&gt;The history DOM cache is gone. The back button re-fetches the page from the server instead of restoring a stored snapshot.&lt;/li&gt;
&lt;li&gt;Error responses swap by default, and the new &lt;code&gt;hx-status&lt;/code&gt; attribute routes 4xx and 5xx responses by status code to their own targets.&lt;/li&gt;
&lt;li&gt;On npm, htmx 2.x keeps the &lt;code&gt;latest&lt;/code&gt; dist-tag until early 2027, and 4.0 lives under the &lt;code&gt;next&lt;/code&gt; dist-tag, so nobody upgrades by accident.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;what-changed-in-htmx-4&quot;&gt;What changed in htmx 4&lt;/h2&gt;
&lt;p&gt;htmx 4 is a ground-up rework of the library’s internals and a cleanup of its accumulated defaults. Carson Gross, htmx’s creator, announced it in an essay called &lt;a href=&quot;https://htmx.org/essays/the-fetchening/&quot;&gt;The fetch()ening&lt;/a&gt; in November 2025, together with the first alpha. A string of betas followed through 2026, and the &lt;a href=&quot;https://github.com/bigskysoftware/htmx/releases&quot;&gt;final release landed today&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The big majors I covered this year, &lt;a href=&quot;/blog/astro-7&quot;&gt;Astro 7&lt;/a&gt; and &lt;a href=&quot;/blog/pnpm-v12-rust-rewrite&quot;&gt;pnpm v12&lt;/a&gt;, rewrote their engines precisely so that nothing would change for you: same surface, faster internals. htmx 4 is the other kind of major. Moving to &lt;code&gt;fetch()&lt;/code&gt; gave the project a reason to revisit design decisions that had been locked in for years, and the team took it. Implicit inheritance, the history cache, silently ignored error responses: all gone. That is why the release strategy matters as much as the feature list, and I will get to it.&lt;/p&gt;
&lt;h2 id=&quot;the-fetch-rewrite&quot;&gt;The fetch() rewrite&lt;/h2&gt;
&lt;p&gt;Every request htmx makes now goes through &lt;code&gt;fetch()&lt;/code&gt; rather than &lt;code&gt;XMLHttpRequest&lt;/code&gt;. You do not call either API yourself when you write htmx, so on a quiet day you will not notice. The visible consequences are at the edges:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;XHR-specific events are gone.&lt;/strong&gt; &lt;code&gt;htmx:xhr:loadstart&lt;/code&gt;, &lt;code&gt;htmx:xhr:progress&lt;/code&gt;, and &lt;code&gt;htmx:xhr:abort&lt;/code&gt; have no &lt;code&gt;fetch()&lt;/code&gt; equivalent and were removed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Events follow a new &lt;code&gt;htmx:phase:action&lt;/code&gt; naming scheme.&lt;/strong&gt; If you listen to htmx lifecycle events in JavaScript, every listener needs a rename.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A default request timeout exists now.&lt;/strong&gt; Requests time out after 60 seconds; in 2.x they could hang forever. The config option moved from &lt;code&gt;timeout&lt;/code&gt; to &lt;code&gt;defaultTimeout&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The event renames look like this:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;htmx 2.x&lt;/th&gt;
&lt;th&gt;htmx 4&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;htmx:beforeRequest&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;htmx:before:request&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;htmx:afterRequest&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;htmx:after:request&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;htmx:beforeSwap&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;htmx:before:swap&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;htmx:afterSwap&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;htmx:after:swap&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;htmx:configRequest&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;htmx:config:request&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;htmx:sendError&lt;/code&gt;, &lt;code&gt;htmx:swapError&lt;/code&gt;, &lt;code&gt;htmx:targetError&lt;/code&gt;, &lt;code&gt;htmx:timeout&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;htmx:error&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;That last row is a real simplification: four error events collapse into one &lt;code&gt;htmx:error&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Extensions changed even more. The announcement puts it plainly: “Switching to &lt;code&gt;fetch()&lt;/code&gt; internally let us rethink how extensions can and should work.” The old &lt;code&gt;htmx.defineExtension()&lt;/code&gt; API is replaced by &lt;code&gt;htmx.registerExtension()&lt;/code&gt;, and the &lt;code&gt;hx-ext&lt;/code&gt; attribute is gone entirely: loading an extension’s script is enough. There is a dedicated &lt;a href=&quot;https://four.htmx.org/docs/extension-htmx-4-migration-guide&quot;&gt;extension migration guide&lt;/a&gt; for authors. The payoff is streaming: new first-party extensions stream HTML into the page over server-sent events (&lt;code&gt;hx-sse&lt;/code&gt;), WebSockets (&lt;code&gt;hx-ws&lt;/code&gt;), and &lt;code&gt;multipart/mixed&lt;/code&gt; responses (&lt;code&gt;hx-multipart&lt;/code&gt;).&lt;/p&gt;
&lt;h2 id=&quot;attribute-inheritance-is-now-explicit&quot;&gt;Attribute inheritance is now explicit&lt;/h2&gt;
&lt;p&gt;In htmx 2, attributes like &lt;code&gt;hx-confirm&lt;/code&gt; and &lt;code&gt;hx-target&lt;/code&gt; silently inherited from parent elements. Convenient until it wasn’t: a &lt;code&gt;hx-confirm&lt;/code&gt; on a container would attach confirmation dialogs to descendants you forgot about, and &lt;code&gt;hx-disinherit&lt;/code&gt; existed purely to fight this. In htmx 4, inheritance only happens when you ask for it with the &lt;code&gt;:inherited&lt;/code&gt; modifier:&lt;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;html&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;div&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; hx-confirm:inherited&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;&quot;Are you sure?&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  &amp;lt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;button&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; hx-delete&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;&quot;/account&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;gt;Delete my account&amp;lt;/&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;button&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;div&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Without the modifier, the attribute applies only to the element it sits on. &lt;code&gt;hx-inherit&lt;/code&gt; and &lt;code&gt;hx-disinherit&lt;/code&gt; are both removed, since explicit inheritance makes them pointless. If your codebase leans heavily on the old behavior, setting &lt;code&gt;implicitInheritance&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt; in the htmx config restores it while you migrate.&lt;/p&gt;
&lt;p&gt;Of everything in this release, this is the change I would have asked for. Implicit inheritance was the closest thing htmx had to action at a distance, and debugging it meant walking up the DOM tree in your head.&lt;/p&gt;
&lt;h2 id=&quot;the-back-button-is-a-real-request-now&quot;&gt;The back button is a real request now&lt;/h2&gt;
&lt;p&gt;htmx 2 handled history by snapshotting the DOM into browser storage (localStorage originally, sessionStorage &lt;a href=&quot;https://github.com/bigskysoftware/htmx/releases/tag/v2.0.5&quot;&gt;since 2.0.5&lt;/a&gt;) and restoring the snapshot on back navigation. That cache is exactly the kind of client-side state that drifts from reality, and &lt;a href=&quot;https://www.infoworld.com/article/4150864/htmx-4-0-hypermedia-finds-a-new-gear.html&quot;&gt;InfoWorld’s coverage&lt;/a&gt; of the 4.0 line put it bluntly: “It turned out to be brittle.”&lt;/p&gt;
&lt;p&gt;htmx 4 drops the cache entirely. Per the &lt;a href=&quot;https://four.htmx.org/announcements/2026-08-28-htmx-4.0.0-is-released&quot;&gt;announcement&lt;/a&gt;, on back navigation htmx re-fetches the page from the server and swaps it into &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt;. Your back button now shows what the server says the page is, not what the page happened to look like when you left it. The &lt;code&gt;hx-history&lt;/code&gt; attribute and &lt;code&gt;historyCacheSize&lt;/code&gt; config are gone with it. If you genuinely need snapshot behavior, an &lt;code&gt;hx-history-cache&lt;/code&gt; extension brings it back as an opt-in.&lt;/p&gt;
&lt;h2 id=&quot;route-error-responses-with-hx-status&quot;&gt;Route error responses with hx-status&lt;/h2&gt;
&lt;p&gt;htmx 2 refused to swap 400 and 500 responses by default, which meant server-rendered validation errors needed workarounds. htmx 4 flips the default: 4xx and 5xx responses swap like any other (only 204 and 304 are no-swap by default), and a new &lt;code&gt;hx-status&lt;/code&gt; attribute routes specific status codes to their own target and swap behavior:&lt;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;html&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;form&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; hx-post&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;&quot;/signup&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;      hx-status:422&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;&quot;target:#validation-errors&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;      hx-status:5xx&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;&quot;target:#server-error&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;      hx-status:503&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;&quot;swap:none&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Matching goes from specific to general: an exact code like &lt;code&gt;503&lt;/code&gt; wins over a single-digit wildcard like &lt;code&gt;50x&lt;/code&gt;, which wins over a range wildcard like &lt;code&gt;5xx&lt;/code&gt;. A 422 with the form’s field errors rendered server-side can now land in the right spot with one attribute, which is what hypermedia apps do all day. If you depended on the old ignore-errors behavior, setting the &lt;code&gt;noSwap&lt;/code&gt; config to &lt;code&gt;[204, 304, &apos;4xx&apos;, &apos;5xx&apos;]&lt;/code&gt; restores it.&lt;/p&gt;
&lt;h2 id=&quot;what-else-is-new-in-htmx-4&quot;&gt;What else is new in htmx 4&lt;/h2&gt;
&lt;p&gt;The &lt;a href=&quot;https://four.htmx.org/docs/whats-new-in-htmx-4&quot;&gt;what’s new page&lt;/a&gt; lists more than I can cover, but a few stand out. Morph swaps are built in: &lt;code&gt;innerMorph&lt;/code&gt; and &lt;code&gt;outerMorph&lt;/code&gt; use the idiomorph algorithm and need no extension. New swap styles &lt;code&gt;textContent&lt;/code&gt; and &lt;code&gt;delete&lt;/code&gt; join the existing set. A new &lt;code&gt;&amp;lt;hx-partial&amp;gt;&lt;/code&gt; element lets one response update several targets, with each partial carrying its own target and swap. View Transitions support is there but disabled by default; flip the &lt;code&gt;transitions&lt;/code&gt; config option to turn it on.&lt;/p&gt;
&lt;p&gt;The one that caught my eye: a new &lt;code&gt;hx-query&lt;/code&gt; attribute issues an HTTP QUERY request, the &lt;a href=&quot;/blog/the-new-http-query-method&quot;&gt;safe-reads-with-a-body method&lt;/a&gt; I wrote about in July after RFC 10008 landed. Watching a brand-new HTTP verb show up as a first-class attribute in a hypermedia library within months is the spec pipeline working the way it should.&lt;/p&gt;
&lt;h2 id=&quot;the-breaking-changes-in-htmx-4&quot;&gt;The breaking changes in htmx 4&lt;/h2&gt;
&lt;p&gt;The &lt;a href=&quot;https://four.htmx.org/docs&quot;&gt;migration guide&lt;/a&gt; is thorough, and the headline breaking changes are the three above: explicit inheritance, error responses swapping by default, and server round-trip history. Beyond those:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Two attributes trade names, and order matters.&lt;/strong&gt; &lt;code&gt;hx-disable&lt;/code&gt; becomes &lt;code&gt;hx-ignore&lt;/code&gt;, and &lt;code&gt;hx-disabled-elt&lt;/code&gt; becomes &lt;code&gt;hx-disable&lt;/code&gt;. The old name gets reused, so the guide says to rename &lt;code&gt;hx-disable&lt;/code&gt; to &lt;code&gt;hx-ignore&lt;/code&gt; first or you will migrate one attribute into the other.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Removed attributes:&lt;/strong&gt; &lt;code&gt;hx-ext&lt;/code&gt;, &lt;code&gt;hx-inherit&lt;/code&gt;, &lt;code&gt;hx-disinherit&lt;/code&gt;, &lt;code&gt;hx-history&lt;/code&gt;, &lt;code&gt;hx-vars&lt;/code&gt; (use &lt;code&gt;hx-vals&lt;/code&gt; with the &lt;code&gt;js:&lt;/code&gt; prefix), &lt;code&gt;hx-params&lt;/code&gt; (use the &lt;code&gt;htmx:config:request&lt;/code&gt; event), &lt;code&gt;hx-request&lt;/code&gt;, and &lt;code&gt;hx-prompt&lt;/code&gt; (now an extension).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Config options were renamed or culled.&lt;/strong&gt; &lt;code&gt;timeout&lt;/code&gt; is &lt;code&gt;defaultTimeout&lt;/code&gt;, &lt;code&gt;defaultSwapStyle&lt;/code&gt; is &lt;code&gt;defaultSwap&lt;/code&gt;, &lt;code&gt;globalViewTransitions&lt;/code&gt; is &lt;code&gt;transitions&lt;/code&gt;. A long list of options is simply gone, including &lt;code&gt;historyCacheSize&lt;/code&gt;, &lt;code&gt;defaultSwapDelay&lt;/code&gt;, and the settling-class options.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Validation events are removed.&lt;/strong&gt; &lt;code&gt;htmx:validation:validate&lt;/code&gt;, &lt;code&gt;failed&lt;/code&gt;, and &lt;code&gt;halted&lt;/code&gt; are out; native browser validation is the path forward.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Every event listener needs the new names.&lt;/strong&gt; See the table above.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;htmx ships a scanner that finds most of this for you:&lt;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;sh&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;npx&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string)&quot;&gt; htmx.org@4.0.0&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string)&quot;&gt; upgrade-check&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string)&quot;&gt; --&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string)&quot;&gt; ./path/to/project/root&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It walks your templates and prints deprecated patterns with file and line numbers. Run it before deciding how big your migration actually is; for a small app the answer may be an afternoon.&lt;/p&gt;
&lt;h2 id=&quot;why-40-is-not-latest-on-npm&quot;&gt;Why 4.0 is not latest on npm&lt;/h2&gt;
&lt;p&gt;Here is the part I find genuinely rare. As of today, the &lt;a href=&quot;https://www.npmjs.com/package/htmx.org&quot;&gt;htmx.org package&lt;/a&gt; has &lt;code&gt;latest&lt;/code&gt; pointing at 2.0.10 and 4.0.0 published under &lt;code&gt;next&lt;/code&gt;. That is deliberate, and the announcement spells out why:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;we are not marking 4.0 as &lt;code&gt;latest&lt;/code&gt; in NPM because we do not want to force-upgrade users who are relying on non-versioned CDN URLs for htmx. Instead, 2.x will remain &lt;code&gt;latest&lt;/code&gt; and the 4.0 line will remain &lt;code&gt;next&lt;/code&gt; until some point in early 2027.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Plenty of production sites load htmx from a CDN URL that resolves whatever &lt;code&gt;latest&lt;/code&gt; is. Flipping the tag on release day would silently ship breaking changes to all of them. So the tag waits several more months while 4.0 is, by every other measure, released and stable today.&lt;/p&gt;
&lt;p&gt;The announcement frames the whole release this way: “we have made explicit choices that we feel will put htmx-based applications in a good spot for being 100-year web services.” You can roll your eyes at the century talk, but the mechanics back it up. htmx 2 “will continue to be supported indefinitely so don’t feel any pressure to upgrade,” per the same announcement. Compare that with the upgrade treadmill most of the ecosystem runs on, where staying one major behind makes you a straggler within months. htmx is telling you the old version is fine, forever, and meaning it.&lt;/p&gt;
&lt;h2 id=&quot;should-you-upgrade-to-htmx-4&quot;&gt;Should you upgrade to htmx 4?&lt;/h2&gt;
&lt;p&gt;For a new project, start on 4. The defaults are better: explicit inheritance, error responses you can actually handle in hypermedia, history that cannot go stale. Install it with &lt;code&gt;npm install htmx.org@next&lt;/code&gt; or pin the 4.0.0 CDN URL, since &lt;code&gt;latest&lt;/code&gt; will hand you 2.x until early 2027.&lt;/p&gt;
&lt;p&gt;For an existing app, there is no clock. Run the &lt;code&gt;upgrade-check&lt;/code&gt; scanner to size the work, and if the report is short, do the migration in an isolated &lt;a href=&quot;/blog/git-worktrees-are-underrated&quot;&gt;git worktree&lt;/a&gt; and shake out the event renames. If the report is long, htmx 2 remains supported and maintained, and the team has said so in writing.&lt;/p&gt;
&lt;p&gt;The technical changes are good. The release discipline is the story I will remember: a project that shipped a breaking major and then deliberately kept it off the &lt;code&gt;latest&lt;/code&gt; tag so nobody gets upgraded by accident. That is what taking backwards compatibility seriously looks like.&lt;/p&gt;
</content:encoded><category>webdev</category><category>javascript</category><category>htmx</category><category>opensource</category><author>Dennis Morello</author></item><item><title>Why I Replaced Tailwind CSS With Vanilla CSS</title><link>https://morello.dev/blog/replacing-tailwind-with-vanilla-css</link><guid isPermaLink="true">https://morello.dev/blog/replacing-tailwind-with-vanilla-css</guid><description>I pulled Tailwind CSS out of my Astro site and rewrote it in vanilla CSS. Here&apos;s why modern CSS made the framework optional in 2026, and how I did it.</description><pubDate>Thu, 06 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I removed Tailwind CSS from this site and rewrote every style by hand in vanilla CSS. The reason is short: modern CSS now does natively what I was pulling Tailwind in to get. Cascade layers, custom properties, nesting, and &lt;code&gt;:has()&lt;/code&gt; all ship in every current browser, and Tailwind v4 is built on those exact primitives. Once the browser speaks the language directly, a build step that generates it for you reads as overhead rather than leverage. When I &lt;a href=&quot;/blog/the-new-website&quot;&gt;rebuilt this site in December 2025&lt;/a&gt; I reached for &lt;a href=&quot;https://tailwindcss.com&quot;&gt;Tailwind v4&lt;/a&gt; on instinct. This month I took it back out.&lt;/p&gt;
&lt;h2 id=&quot;tldr&quot;&gt;TL;DR&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;The features I used Tailwind for (design tokens, nesting, scoping, a consistent scale) are now native CSS or handled by Astro’s scoped styles.&lt;/li&gt;
&lt;li&gt;Tailwind v4 is itself built on native cascade layers, custom properties, and &lt;code&gt;color-mix()&lt;/code&gt;, with &lt;a href=&quot;https://lightningcss.dev&quot;&gt;Lightning CSS&lt;/a&gt; doing the build. I was shipping an engine to generate CSS the browser already understands.&lt;/li&gt;
&lt;li&gt;The migration dropped three Tailwind packages, moved global styles into native &lt;code&gt;@layer&lt;/code&gt;s, and put every component’s styles in Astro’s scoped &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; blocks.&lt;/li&gt;
&lt;li&gt;The site stayed &lt;a href=&quot;https://web.dev/baseline&quot;&gt;Baseline Widely available&lt;/a&gt; the whole way.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;why-replace-tailwind-at-all&quot;&gt;Why replace Tailwind at all?&lt;/h2&gt;
&lt;p&gt;Because the job Tailwind did for me is now built into the platform. When I list what I actually reached for it to solve, every item has a native answer today:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What I used Tailwind for&lt;/th&gt;
&lt;th&gt;The vanilla replacement&lt;/th&gt;
&lt;th&gt;Baseline Widely since&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Style layering and predictable overrides&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@layer&lt;/code&gt; cascade layers&lt;/td&gt;
&lt;td&gt;Sept 2024&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Design tokens and theming&lt;/td&gt;
&lt;td&gt;Custom properties (&lt;code&gt;--x&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;long-standing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nesting without a preprocessor&lt;/td&gt;
&lt;td&gt;Native CSS nesting (&lt;code&gt;&amp;amp;&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;June 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Parent and state selectors&lt;/td&gt;
&lt;td&gt;&lt;code&gt;:has()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;June 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Component-scoped responsive rules&lt;/td&gt;
&lt;td&gt;Container queries and Astro scoped styles&lt;/td&gt;
&lt;td&gt;Aug 2025&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The dates are the point. &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_nesting&quot;&gt;CSS nesting&lt;/a&gt; and &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/:has&quot;&gt;&lt;code&gt;:has()&lt;/code&gt;&lt;/a&gt; only crossed into Baseline Widely availability around June 2026, roughly two months before I did this. &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/@layer&quot;&gt;Cascade layers&lt;/a&gt; got there in September 2024, &lt;code&gt;color-mix()&lt;/code&gt; in November 2025, size container queries in August 2025. The Chrome team’s &lt;a href=&quot;https://developer.chrome.com/blog/css-wrapped-2025&quot;&gt;CSS Wrapped 2025&lt;/a&gt; counted 22 new CSS features landing in Chrome in a single year. The gap Tailwind was filling closed while I wasn’t looking, and 2026 is the year it actually finished closing.&lt;/p&gt;
&lt;p&gt;None of this makes Tailwind bad. It makes it optional for what I’m doing here: a single-author content site where nobody but me touches the CSS.&lt;/p&gt;
&lt;h2 id=&quot;tailwind-v4-already-runs-on-the-css-i-switched-to&quot;&gt;Tailwind v4 already runs on the CSS I switched to&lt;/h2&gt;
&lt;p&gt;The thing that tipped me over was reading how &lt;a href=&quot;https://tailwindcss.com/blog/tailwindcss-v4&quot;&gt;Tailwind v4&lt;/a&gt; works under the hood. Its January 2025 rewrite is explicit that it is built on modern CSS. The announcement lists “native cascade layers,” “registered custom properties,” “&lt;code&gt;color-mix()&lt;/code&gt;,” and logical properties as the foundation. The &lt;a href=&quot;https://tailwindcss.com/blog/tailwindcss-v4-alpha&quot;&gt;earlier post introducing its Oxide engine&lt;/a&gt; is blunter about the plumbing: the engine is “up to 10x faster,” and “the only thing the new engine depends on is Lightning CSS.”&lt;/p&gt;
&lt;p&gt;Read that back with a migration in mind. Tailwind v4 generates native cascade layers, sets custom properties, and runs the output through Lightning CSS for prefixing and minification. That is the same stack I now write directly. I was running an engine whose job was to emit CSS features my target browsers already implement. For an app with dozens of engineers and a shared design system, that engine buys you consistency and guardrails worth paying for. For this site, it was a generator sitting between me and CSS I could just write.&lt;/p&gt;
&lt;p&gt;There is a small irony worth stating plainly. Tailwind v4 leans on &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/@property&quot;&gt;registered custom properties&lt;/a&gt; (&lt;code&gt;@property&lt;/code&gt;), which won’t reach Baseline Widely availability until around January 2027, so Tailwind ships its own fallbacks for it. My hand-written CSS holds a stricter bar than the framework I removed.&lt;/p&gt;
&lt;h2 id=&quot;what-the-migration-actually-looked-like&quot;&gt;What the migration actually looked like&lt;/h2&gt;
&lt;p&gt;The site is &lt;a href=&quot;https://astro.build&quot;&gt;Astro&lt;/a&gt;, so it splits cleanly in two: global styles in &lt;code&gt;src/styles&lt;/code&gt;, and per-component styles in each &lt;code&gt;.astro&lt;/code&gt; file’s scoped &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; block. I rewrote both.&lt;/p&gt;
&lt;h3 id=&quot;cascade-layers-replace-the-tailwind-import&quot;&gt;Cascade layers replace the Tailwind import&lt;/h3&gt;
&lt;p&gt;The old entry point started with Tailwind’s import, its typography plugin, and a &lt;code&gt;@custom-variant&lt;/code&gt; line for every palette so theme selectors would compile:&lt;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;css&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;@import&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt; &quot;tailwindcss&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;@plugin&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; &quot;@tailwindcss/typography&quot;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;@custom-variant&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; tokyo-night (&amp;amp;:where([data-theme=&quot;tokyo-night&quot;]));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;@custom-variant&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; dracula (&amp;amp;:where([data-theme=&quot;dracula&quot;]));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-comment)&quot;&gt;/* ...thirteen more palettes... */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;@custom-variant&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; dark (&amp;amp;:where([data-mode=&quot;dark&quot;], [data-mode=&quot;dark&quot;] *));&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Fifteen &lt;code&gt;@custom-variant&lt;/code&gt; declarations, one per palette, plus one for dark mode, all to teach the framework about selectors the browser already understands. The replacement is one line of real CSS that declares the cascade order, then a plain file per layer:&lt;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;css&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;@layer&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; reset, tokens, base, primitives, prose, utilities;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ordering the layers by hand turned out to be the feature I most underrated in Tailwind. Layers win by declaration order regardless of selector specificity, so the reset can be dead simple, tokens can sit under everything, and utilities win last without a single &lt;code&gt;!important&lt;/code&gt;. I get the exact override behavior Tailwind gave me, spelled out in one readable statement.&lt;/p&gt;
&lt;h3 id=&quot;design-tokens-are-just-custom-properties&quot;&gt;Design tokens are just custom properties&lt;/h3&gt;
&lt;p&gt;Theming didn’t really change, which is the tell. Under Tailwind the palettes were already CSS variables swapped by a &lt;code&gt;data-theme&lt;/code&gt; attribute; the framework just wrapped them in &lt;code&gt;@theme&lt;/code&gt; and &lt;code&gt;@custom-variant&lt;/code&gt;. Stripping that wrapper left plain rules:&lt;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;css&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;:root&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;data-theme&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;&quot;catppuccin-mocha&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;] {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  --bg&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; #11111b&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  --ink&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; #cdd6f4&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  --accent&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; #89b4fa&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-comment)&quot;&gt;  /* ...the rest of the reduced token set... */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Sixteen of those blocks, one per theme, in a &lt;code&gt;tokens&lt;/code&gt; layer. The switcher sets &lt;code&gt;data-theme&lt;/code&gt; on &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt;, the variables cascade in, and syntax highlighting tracks them through more variables. No config object, no plugin API, just the custom properties that were doing the work all along.&lt;/p&gt;
&lt;h3 id=&quot;components-from-class-lists-to-scoped-styles&quot;&gt;Components: from class lists to scoped styles&lt;/h3&gt;
&lt;p&gt;This is where the day-to-day difference shows. Here is the card component before, as a Tailwind class list:&lt;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;astro&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;article&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;  class:list&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;{[&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;    &quot;relative rounded border bg-card p-4 sm:p-6&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;    &quot;has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-ring&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;    className&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  ]}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  {&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;...&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;props}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  &amp;lt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;slot&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; /&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;article&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And after, as scoped CSS in the same file:&lt;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;astro&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;article&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; class&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;{className} {&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;...&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;props}&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  &amp;lt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;slot&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; /&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;article&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;style&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;  article&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;    position&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; relative&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;    border-radius&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; var&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;(--radius)&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;    border-width&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;px&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;    background-color&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; var&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;(--bg-elevated)&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;    padding&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;rem&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;    &amp;amp;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;has(:focus-visible&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;      outline&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; 2&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;px&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; solid &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;var&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;(--accent)&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;      outline-offset&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; 2&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;px&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;  @media&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;width&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt; &amp;gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; 40&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;rem&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;    article&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;      padding&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; 1.5&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;rem&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;style&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The markup goes quiet and the intent moves into CSS that reads like CSS. Native nesting holds the &lt;code&gt;:has()&lt;/code&gt; state rule next to the base rule, and a normal media query handles the responsive padding that &lt;code&gt;sm:p-6&lt;/code&gt; used to encode. &lt;a href=&quot;https://docs.astro.build/en/guides/styling/#scoped-styles&quot;&gt;Astro scopes these styles&lt;/a&gt; through a generated &lt;code&gt;data-astro-cid&lt;/code&gt; attribute, so a bare &lt;code&gt;article&lt;/code&gt; selector is enough and the element needs no class of its own. And because scoped styles compile unlayered, they beat the global layers without any specificity games. That last property is what made the migration safe to do component by component: a scoped rule always wins over a global primitive, so I could move one file at a time without the two systems fighting.&lt;/p&gt;
&lt;h3 id=&quot;lightning-css-does-the-vendor-work&quot;&gt;Lightning CSS does the vendor work&lt;/h3&gt;
&lt;p&gt;The reflex worry with dropping a framework is that you inherit its chores: prefixing, downleveling new syntax, minifying. I inherited none of them, because &lt;a href=&quot;https://lightningcss.dev&quot;&gt;Lightning CSS&lt;/a&gt; was already in the pipeline. It’s a Rust parser, transformer, and minifier by &lt;a href=&quot;https://x.com/devongovett&quot;&gt;Devon Govett&lt;/a&gt;, and it is the same tool Tailwind v4’s Oxide engine depends on. Astro’s Rust compiler &lt;a href=&quot;/blog/astro-7&quot;&gt;uses it too&lt;/a&gt;, so turning it on for my own CSS was one line:&lt;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;vite&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  css&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; { transformer&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt; &quot;lightningcss&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; }&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  build&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;    cssTarget&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;&quot;chrome111&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt; &quot;edge111&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt; &quot;firefox114&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt; &quot;safari16.4&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt; &quot;ios16.4&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;]&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  }&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It adds the handful of &lt;code&gt;-webkit-&lt;/code&gt; prefixes my target browsers still need, lowers nesting for the stragglers, and minifies, all against a Baseline target set. The &lt;code&gt;package.json&lt;/code&gt; diff was the satisfying part: out went &lt;code&gt;tailwindcss&lt;/code&gt;, &lt;code&gt;@tailwindcss/vite&lt;/code&gt;, and &lt;code&gt;@tailwindcss/typography&lt;/code&gt;; in went a single &lt;code&gt;lightningcss&lt;/code&gt; dev dependency that was effectively already there. The long-form article styling followed the same instinct I wrote about in &lt;a href=&quot;/blog/shadcn-typeset-vs-tailwind-prose&quot;&gt;shadcn/typeset vs Tailwind Typography&lt;/a&gt;: own the file, delete the rules you don’t want.&lt;/p&gt;
&lt;h2 id=&quot;did-the-site-stay-baseline-compliant&quot;&gt;Did the site stay Baseline compliant?&lt;/h2&gt;
&lt;p&gt;Yes, and that was a hard constraint, not a nice-to-have. Every feature this site ships has to be &lt;a href=&quot;/blog/baseline-2026-web-platform-apis&quot;&gt;Baseline Widely available&lt;/a&gt;, interoperable across the core browsers for at least 30 months. That’s exactly why the timing worked: the two features I lean on hardest, native nesting and &lt;code&gt;:has()&lt;/code&gt;, only reached Widely availability around June 2026. If I’d tried this a year earlier they’d have been Baseline Newly at best, and the card component above would have needed an &lt;code&gt;@supports&lt;/code&gt; gate with a full fallback. Doing it now, they’re safe to use unguarded. The one place I still gate behind &lt;code&gt;@supports&lt;/code&gt; is the anchor-positioned reading indicator in the table of contents, which is genuinely still Newly available.&lt;/p&gt;
&lt;h2 id=&quot;do-you-still-need-tailwind-in-2026&quot;&gt;Do you still need Tailwind in 2026?&lt;/h2&gt;
&lt;p&gt;Only if someone other than you owns the CSS. That is the honest split, and it decides whether replacing Tailwind with vanilla CSS is worth it for you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A team with a shared design system:&lt;/strong&gt; probably keep Tailwind. The constraints that feel like overhead on a solo site are the whole point when twenty people style the same product. Consistency you don’t have to enforce by review is worth a build step.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A solo project or a content site:&lt;/strong&gt; the trade-off has flipped. You get design tokens from custom properties, override control from cascade layers, nesting and state selectors natively, and scoping from your framework’s components. The framework’s value is real but no longer necessary.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I’m not claiming Tailwind lost. &lt;a href=&quot;https://kevinpowell.co&quot;&gt;Kevin Powell&lt;/a&gt; has spent years arguing that learning CSS itself pays off, and the platform finally caught up to make that cheap. Tailwind v4 is a genuinely good piece of engineering, built on the same modern CSS that made me comfortable leaving it. For this site, hand-written CSS wired to theme variables is less code, one fewer dependency, and output I can read top to bottom. That’s the same own-the-source call I keep making here, and this time the browser did most of the work for me.&lt;/p&gt;
</content:encoded><category>css</category><category>tailwindcss</category><category>webdev</category><category>astro</category><author>Dennis Morello</author></item><item><title>Bun Rust Rewrite: 64 Claude Agents, 535k Lines of Zig</title><link>https://morello.dev/blog/bun-14-rust-rewrite</link><guid isPermaLink="true">https://morello.dev/blog/bun-14-rust-rewrite</guid><description>Bun 1.4&apos;s Rust rewrite is now stable: 64 Claude agents ported 535k lines of Zig in 11 days. What it fixes, what it overclaims, who&apos;s right: Sumner or Kelley.</description><pubDate>Sat, 01 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In mid-July, in the &lt;a href=&quot;/blog/pnpm-v12-rust-rewrite&quot;&gt;post about pnpm rewriting its install engine in Rust&lt;/a&gt;, I called Bun “the odd one out: it’s written in Zig, not Rust.” That line is wrong, and the awkward part is that it was wrong the week I wrote it. Jarred Sumner announced Bun’s Zig-to-Rust rewrite on July 8; the pnpm post went up July 15, so the claim was stale at publication. I’ve since corrected that post so it acknowledges the rewrite and links here, so if you click through, that line now reads in the past tense with a pointer back here. &lt;a href=&quot;https://bun.sh&quot;&gt;Bun&lt;/a&gt; rewrote itself from Zig to Rust, and stable Bun 1.4 shipped on August 20: the first release where the runtime, bundler, and test runner are all Rust, ported from 535,496 lines of Zig in 11 days. &lt;a href=&quot;https://x.com/jarredsumner&quot;&gt;Jarred Sumner&lt;/a&gt; didn’t do it by hand. He ran about 50 Claude Code workflows, a few at a time, and let them loose on half a million lines of Zig.&lt;/p&gt;
&lt;p&gt;This is the part of the JavaScript toolchain story I want to get right, because it’s the one most likely to be flattened into a press release. The headline is real: Bun 1.4 fixes 128 bugs, ships a roughly 20% smaller binary, and runs a bit faster. The headline is also doing some work the underlying numbers don’t fully support, and there is a genuine, public argument between Sumner and &lt;a href=&quot;https://andrewkelley.me&quot;&gt;Andrew Kelley&lt;/a&gt;, the creator of Zig, over what actually happened. Both of them are partly right.&lt;/p&gt;
&lt;h2 id=&quot;tldr&quot;&gt;TL;DR&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Bun ported its entire runtime from Zig to Rust in 11 days with 64 Claude agents, and the rewrite shipped as stable Bun 1.4 on August 20.&lt;/li&gt;
&lt;li&gt;The numbers that hold up are the leak fixes. An in-process build loop that ballooned to 6.7 GB in 1.3.14 levels off at 609 MB in 1.4, and the use-after-free and double-free bugs that dogged the Zig runtime are the class of failure safe Rust’s &lt;code&gt;Drop&lt;/code&gt; and borrow checker turn into compile errors.&lt;/li&gt;
&lt;li&gt;What the announcement overclaims is the framing. The 2 to 5% speed gain and the roughly 20% smaller binary come from cross-language LTO, ICU trimming, and linker work that could have shipped in Zig, not from Rust as a language.&lt;/li&gt;
&lt;li&gt;Bun 1.4 went stable on August 20, but treat the upgrade like any runtime major: on a throwaway branch, run &lt;code&gt;bun upgrade&lt;/code&gt;, hammer the 2,000-build leak scenario against whatever you ship, and watch memory hold flat before you trust it in production.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;what-the-bun-rust-rewrite-actually-is&quot;&gt;What the Bun Rust rewrite actually is&lt;/h2&gt;
&lt;p&gt;Bun 1.4 is a mechanical port of the whole runtime from Zig to Rust, done one &lt;code&gt;.zig&lt;/code&gt; file at a time, holding behavior as close to 1.3.14 as the port allows. The last Zig release is &lt;a href=&quot;https://github.com/oven-sh/bun/releases/tag/bun-v1.3.14&quot;&gt;Bun v1.3.14&lt;/a&gt;, tagged May 13, 2026. The first Rust release is v1.4.0, &lt;a href=&quot;https://bun.com/1.4&quot;&gt;stable since August 20&lt;/a&gt;: run &lt;code&gt;bun upgrade&lt;/code&gt; to pull it.&lt;/p&gt;
&lt;p&gt;“Mechanical port” is the load-bearing phrase. This is not a greenfield rewrite where Claude gets to redesign Bun. The Zig and Rust shown side by side in Sumner’s &lt;a href=&quot;https://bun.com/blog/bun-in-rust&quot;&gt;announcement&lt;/a&gt; are deliberately near-identical: same function names, same scoping, same comments. The plan is to refactor toward idiomatic Rust &lt;em&gt;after&lt;/em&gt; 1.4 ships, not during the port. That choice is what makes an 11-day timeline plausible, and it’s what keeps the existing TypeScript test suite meaningful as the source of truth. The tests were never tied to the implementation language, so a faithful port should pass the same assertions.&lt;/p&gt;
&lt;p&gt;The scope is everything Bun is: the JavaScript, TypeScript, and CSS transpiler, minifier, and bundler; the npm-compatible package manager; the Jest-like test runner; the Node.js API surface (&lt;code&gt;fs&lt;/code&gt;, &lt;code&gt;net&lt;/code&gt;, &lt;code&gt;tls&lt;/code&gt;, &lt;code&gt;http&lt;/code&gt;, &lt;code&gt;http2&lt;/code&gt;, &lt;code&gt;node:zlib&lt;/code&gt;); and the HTTP server. About 780,000 lines of Rust now sit where 535,496 lines of Zig used to. The “million lines” Kelley keeps citing is the diff: the announcement counts the port as adding a million-plus lines of new Rust. The 780,000 is the resulting codebase, the denominator for the unsafe-percentage figure. Both numbers are right; one is what went in, the other is what’s there.&lt;/p&gt;
&lt;h2 id=&quot;why-rewrite-bun-from-zig-to-rust&quot;&gt;Why rewrite Bun from Zig to Rust?&lt;/h2&gt;
&lt;p&gt;The honest case isn’t speed. It’s stability. Sumner opens the &lt;a href=&quot;https://bun.com/blog/bun-in-rust&quot;&gt;announcement&lt;/a&gt; with a list of bugs fixed in 1.3.14, bugs the Zig runtime shipped until that release, and it’s grim reading for a runtime people run in production:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;heap-use-after-free in &lt;code&gt;node:zlib&lt;/code&gt; when &lt;code&gt;.reset()&lt;/code&gt; fires during an async &lt;code&gt;.write()&lt;/code&gt; still in flight on the threadpool&lt;/li&gt;
&lt;li&gt;use-after-free in &lt;code&gt;node:http2&lt;/code&gt; when a reentrant JS callback triggers a hashmap rehash that invalidates internal stream pointers&lt;/li&gt;
&lt;li&gt;use-after-free in &lt;code&gt;UDPSocket.send()&lt;/code&gt; where a &lt;code&gt;valueOf()&lt;/code&gt; callback detaches the &lt;code&gt;ArrayBuffer&lt;/code&gt; between payload capture and the actual send&lt;/li&gt;
&lt;li&gt;a &lt;code&gt;tlsSocket.setSession()&lt;/code&gt; leak of about 6.5 KB per call, every call, from a missing &lt;code&gt;SSL_SESSION_free&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fs.watch()&lt;/code&gt; watchers never collected after &lt;code&gt;.close()&lt;/code&gt;, pinned as GC roots by a reference-count underflow&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These are the canonical memory-safety failures: use-after-free, double-free, leaks on error paths. In safe Rust most of them become compile errors, and the rest get &lt;code&gt;Drop&lt;/code&gt; running cleanup exactly once when a value goes out of scope. The argument is that compiler errors are a better feedback loop than a style guide enforced by review, and for this class of bug, lifetimes of garbage-collected values mixed with manually managed ones, it’s hard to argue with. Zig’s &lt;code&gt;defer&lt;/code&gt;/&lt;code&gt;errdefer&lt;/code&gt; model puts cleanup at every call site and trusts the author to get it right every time. Bun, at half a million lines, did not get it right every time, and nobody reasonable expected it to.&lt;/p&gt;
&lt;p&gt;This is the part I’d already half-gotten wrong. When I lumped Bun into the “everything’s Rust now” trend in the &lt;a href=&quot;/blog/pnpm-v12-rust-rewrite&quot;&gt;pnpm v12 post&lt;/a&gt; and flagged it as the Zig holdout, I treated language choice as a settled, one-way decision. Sumner says explicitly that it used to be one, and that AI-assisted porting is what made it reversible at this scale. Take that claim seriously even if you’re skeptical of the rest.&lt;/p&gt;
&lt;h2 id=&quot;how-do-you-port-535000-lines-in-11-days&quot;&gt;How do you port 535,000 lines in 11 days?&lt;/h2&gt;
&lt;p&gt;You don’t ask one agent to “rewrite Bun.” Sumner’s post is most useful as an engineering breakdown of the loops, and the loops are ordinary:&lt;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-comment)&quot;&gt;// Pseudocode, not real code:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;let&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; task;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;while&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; ((task &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; todoList&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;.pop&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;())) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; result&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; task&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; feedback&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; Promise&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;.all&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;([&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;review&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(result)&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; review&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(result)]);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;  await&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; apply&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(feedback&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; result);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each workflow is one implementer plus two or more adversarial reviewers plus a fixer, with the reviewers running in separate context windows so the Claude that wrote the code isn’t the Claude grading it. At peak, four workflows ran at once, each with 16 Claudes, for about 64 concurrent agents across four &lt;a href=&quot;/blog/git-worktrees-are-underrated&quot;&gt;git worktrees&lt;/a&gt;. They started on May 3 and merged May 14, across 6,778 commits and 1,448 &lt;code&gt;.zig&lt;/code&gt; files moved to &lt;code&gt;.rs&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The part worth copying is the prep work. Before any porting, Sumner spent about three hours with Claude producing a &lt;code&gt;PORTING.md&lt;/code&gt; that mapped Zig patterns to Rust patterns, then ran a workflow that traced every struct field’s lifetime across the codebase and wrote the results to a &lt;code&gt;LIFETIMES.tsv&lt;/code&gt;. Three files were ported first as a trial, reviewed against the guides, then the full sweep ran. The false starts are instructive: two Claudes ran &lt;code&gt;git stash&lt;/code&gt; and &lt;code&gt;git reset --hard&lt;/code&gt; over each other’s work, so the workflow rule became “no &lt;code&gt;git&lt;/code&gt; except committing one specific file, no &lt;code&gt;cargo&lt;/code&gt;, no slow commands.” Claude also tried to “fix” compile errors by stubbing functions out, so a rule was added: if a workaround needs a paragraph-long comment, the code is wrong, fix the code.&lt;/p&gt;
&lt;p&gt;The token bill for all of this is roughly 5.9 billion uncached input tokens, 690 million output tokens, and 72 billion cached reads, about $165,000 at API pricing, on what was then a pre-release build of Claude Fable 5, the Mythos-class model Anthropic shipped a month later. That’s the part the search summaries will quote and the part that most obscures what happened. The money bought an engineered loop with human supervision, not a prompt and a prayer.&lt;/p&gt;
&lt;h2 id=&quot;do-the-numbers-hold-up&quot;&gt;Do the numbers hold up?&lt;/h2&gt;
&lt;p&gt;Here’s the skeptic’s reading. The &lt;a href=&quot;https://bun.com/blog/bun-in-rust&quot;&gt;announcement&lt;/a&gt; lists 128 bugs fixed versus 1.3.14, 19 known regressions (all fixed), zero tests skipped or deleted, and 1,386,826 &lt;code&gt;expect()&lt;/code&gt; calls on Debian. The leak numbers are the most convincing: every in-process &lt;code&gt;Bun.build()&lt;/code&gt; leaked about 3 MB in 1.3.14, so 2,000 builds ran the process to 6,745 MB. In 1.4 it levels off at 609 MB.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;text-align: right&quot;&gt;Builds&lt;/th&gt;
&lt;th style=&quot;text-align: right&quot;&gt;Bun 1.3.14&lt;/th&gt;
&lt;th style=&quot;text-align: right&quot;&gt;Bun 1.4&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: right&quot;&gt;500&lt;/td&gt;
&lt;td style=&quot;text-align: right&quot;&gt;1,914 MB&lt;/td&gt;
&lt;td style=&quot;text-align: right&quot;&gt;526 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: right&quot;&gt;1,000&lt;/td&gt;
&lt;td style=&quot;text-align: right&quot;&gt;3,506 MB&lt;/td&gt;
&lt;td style=&quot;text-align: right&quot;&gt;586 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: right&quot;&gt;1,500&lt;/td&gt;
&lt;td style=&quot;text-align: right&quot;&gt;5,097 MB&lt;/td&gt;
&lt;td style=&quot;text-align: right&quot;&gt;608 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: right&quot;&gt;2,000&lt;/td&gt;
&lt;td style=&quot;text-align: right&quot;&gt;6,745 MB&lt;/td&gt;
&lt;td style=&quot;text-align: right&quot;&gt;609 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;If you run a dev server that bundles on every request, that’s the difference between an OOM restart at 3 AM and a stable process. A previous Zig attempt at this fix &lt;a href=&quot;https://bun.com/blog/bun-in-rust&quot;&gt;never merged&lt;/a&gt; because, Sumner says, the lack of &lt;code&gt;Drop&lt;/code&gt; made it too risky to trust. That’s the strongest single argument for the rewrite.&lt;/p&gt;
&lt;p&gt;The speed and size numbers want more scrutiny. Bun 1.4 is 2 to 5% faster on HTTP and app workloads: &lt;code&gt;Bun.serve&lt;/code&gt; goes 169.6k to 177.7k req/s (up 4.8%), and &lt;code&gt;next build&lt;/code&gt; goes 13.62s to 13.03s. The binary shrinks roughly 20%, from 94 to 76 MB on Windows and 88 to 70 MB on Linux. But the announcement itself attributes the throughput to cross-language LTO between C/C++ and Rust, and attributes the binary shrink partly to ICU trimming and Identical Code Folding. None of that is Rust-only. The initial Rust-only binary reduction came from dropping Zig’s heavy &lt;code&gt;comptime&lt;/code&gt; use; the rest came from linker work that could have shipped in Zig.&lt;/p&gt;
&lt;p&gt;That’s where Andrew Kelley’s pushback lands hardest. The short version: a meaningful slice of the “Bun is better in Rust” win was engineering work Bun could have shipped without changing languages, and the post doesn’t separate the two clearly.&lt;/p&gt;
&lt;h2 id=&quot;the-sumner-vs-kelley-fight-both-sides&quot;&gt;The Sumner vs. Kelley fight, both sides&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://andrewkelley.me/post/my-thoughts-bun-rust-rewrite.html&quot;&gt;Andrew Kelley’s response&lt;/a&gt; is not a calm post, and it’s not framed as a technical critique. It opens on Sumner’s “beginner energy” and builds to Kelley calling him “a stinky manager. Poor communication, unrealistic expectations, low empathy, no experience.” But buried in the personal framing are four technical objections that hold up on their own:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The test-suite contradiction. The argument for merging a million lines of largely unreviewed code is that the test suite catches everything. But the same post opens with a long list of bugs in the Zig version the test suite didn’t catch. Kelley’s question: if it wasn’t sufficient to catch Zig bugs, why is it sufficient to declare one million lines of unreviewed AI-authored Rust clean?&lt;/li&gt;
&lt;li&gt;The style-guide sleight of hand. The post frames the choice as “style guide vs. language feature,” which skips the main way projects actually eliminate bugs, which is putting engineers on it. Kelley points at TigerBeetle, another Zig project, as the team that did the work Bun didn’t.&lt;/li&gt;
&lt;li&gt;The fuzzing claim. The announcement implies diligent Zig-side fuzzing; Kelley says Bun told the Zig Software Foundation directly that they weren’t fuzzing anything. The 24/7 coverage-guided fuzzing the post now brags about is new and Rust-side.&lt;/li&gt;
&lt;li&gt;The omitted build speed. The Zig compiler, roughly 600,000 lines, builds clean in 16s and recompiles in 90ms with incremental enabled, Kelley writes. Bun’s post doesn’t give post-rewrite build numbers, and for a mechanical port of a codebase this size that’s a conspicuous gap.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Each of those is fair. The first is the sharpest. Sumner’s “0 tests deleted, 1.4M expect() calls, adversarial review, human in the loop” defense addresses process, not the logical gap. A suite that missed a &lt;code&gt;setSession&lt;/code&gt; leak bleeding 6.5 KB a call for years is not a suite you can then cite as proof a million lines are correct. The suite is necessary and it caught a lot, the 128 fixes prove that, but “the test suite caught regressions” and “the test suite proves correctness” are different claims, and the announcement slides between them.&lt;/p&gt;
&lt;p&gt;Sumner’s side holds up too, and it’s the one that matters more for users. The Zig bug list isn’t marketing; those are real crashes and real leaks in a runtime with 22 million monthly CLI downloads that now backs Claude Code. The &lt;code&gt;Drop&lt;/code&gt; argument is the real argument, and Kelley doesn’t really answer it. He shifts to “you should have put in engineering hours instead,” which is true and also not a refutation of the fact that Zig’s manual-lifetime model was bleeding. You can fault how Bun got to Rust and still accept that being in Rust, with &lt;code&gt;Drop&lt;/code&gt; and a borrow checker and Miri, materially lowers the chance of the next &lt;code&gt;setSession&lt;/code&gt; leak.&lt;/p&gt;
&lt;p&gt;Where I land: the rewrite is net good for Bun’s users, the AI-authorship process is more disciplined than its critics assume, and the announcement overclaims by bundling genuinely Rust-driven gains (leaks, use-after-free) with engineering work that didn’t need a rewrite (LTO, ICU, &lt;code&gt;comptime&lt;/code&gt; cleanup). Read the two posts as correcting each other and you get closer to the truth than either one alone.&lt;/p&gt;
&lt;h2 id=&quot;what-about-the-unsafe-code-and-undefined-behavior&quot;&gt;What about the &lt;code&gt;unsafe&lt;/code&gt; code and undefined behavior?&lt;/h2&gt;
&lt;p&gt;The thing people will actually worry about with AI-authored Rust is &lt;code&gt;unsafe&lt;/code&gt;. The announcement gives the number: about 4% of the Rust sits in &lt;code&gt;unsafe&lt;/code&gt; blocks, roughly 13,000 &lt;code&gt;unsafe&lt;/code&gt; keyword uses across the ~780,000-line codebase, and 78% of those blocks are a single line, a pointer from JavaScriptCore or one call into a C library. That’s lower than I expected, and it’s the floor, not a ceiling. Bun embeds JSC, uWebSockets, BoringSSL, and SQLite, so &lt;code&gt;unsafe&lt;/code&gt; will never hit zero. The plan is to push it down as the mechanical port gets refactored toward idiomatic Rust.&lt;/p&gt;
&lt;p&gt;The worry has a concrete footprint. Issue &lt;a href=&quot;https://github.com/oven-sh/bun/issues/30719&quot;&gt;#30719&lt;/a&gt;, filed May 14 by AwesomeQubic, is titled “PathString::slice dangling reference UB - add Miri to CI”: a &lt;code&gt;core::slice::from_raw_parts&lt;/code&gt; call constructing a dangling &lt;code&gt;&amp;amp;[u8]&lt;/code&gt;. Miri was not in Bun’s CI when the bug was filed; the reporter ran it locally to find it. The issue is closed, fixed by &lt;a href=&quot;https://github.com/oven-sh/bun/pull/30876&quot;&gt;#30876&lt;/a&gt; (“Add cargo-miri support and fix HiveArray aliasing UB”, merged May 17), with &lt;a href=&quot;https://github.com/oven-sh/bun/pull/30728&quot;&gt;#30728&lt;/a&gt; an earlier attempt that stayed open until it was closed on August 13. The reporter’s parting line, “Please consider not vibe coding rust as AIs are not good at writing Rust and also hire a real rust dev,” is the tweet-sized version of the whole debate. The bug is real, the kind of UB that turns up in any large &lt;code&gt;unsafe&lt;/code&gt;-touching codebase, and Miri is exactly the tool meant to catch it. What’s worth being precise about is who supplied the mechanism: the reporter ran it on a build Bun hadn’t instrumented yet, not Bun’s CI. The announcement now says Miri runs on a growing chunk of code in CI, which is the state after #30876 landed. So it’s still the system working, not failing, but it’s the community catching the bug and Bun turning the catch into infrastructure. And it’s the proof that “AI wrote it” doesn’t free you from reviewing &lt;code&gt;unsafe&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;where-this-leaves-the-javascript-toolchain&quot;&gt;Where this leaves the JavaScript toolchain&lt;/h2&gt;
&lt;p&gt;The pattern is established enough that I’d stop calling it a trend and start calling it the default. &lt;a href=&quot;/blog/typescript-7-is-here&quot;&gt;TypeScript&lt;/a&gt; went to Go for build speed. &lt;a href=&quot;/blog/pnpm-v12-rust-rewrite&quot;&gt;pnpm&lt;/a&gt; is going to Rust for install speed. &lt;a href=&quot;/blog/astro-7&quot;&gt;Astro 7&lt;/a&gt; rebuilt its compiler in Rust. Deno is Rust from the ground up. Bun just went to Rust for stability. The reasons differ, but the move is the same: the JavaScript world’s foundation is being rewritten in languages with a real memory model, and the engines running the JS itself, V8 and JavaScriptCore with their &lt;a href=&quot;/blog/five-things-you-might-not-know-about-javascript&quot;&gt;tiered JIT pipelines&lt;/a&gt;, are the one layer staying put.&lt;/p&gt;
&lt;p&gt;What’s new in the Bun story is who’s doing it and who owns it. Anthropic &lt;a href=&quot;https://bun.com/blog/bun-joins-anthropic&quot;&gt;acquired Bun in December 2025&lt;/a&gt;. Claude Code ships as a Bun executable to millions of users, and the rewrite ran on Claude Fable 5 a month before Anthropic shipped it. Prisma launched a Prisma Compute beta on the Rust rewrite; Alexey Orlenko is quoted in the &lt;a href=&quot;https://bun.com/blog/bun-in-rust&quot;&gt;announcement&lt;/a&gt; on the memory leaks and the connection pool “that couldn’t recover after a VM was paused and resumed,” handled “perfectly” by the Rust build. That’s the financial and structural context the announcement’s lead leaves out: this is Anthropic making its own tooling’s runtime safer, with Anthropic’s model. The independence question is real, even if the code stays MIT.&lt;/p&gt;
&lt;p&gt;The subtler shift is what “an engineer can do in a year” means. Sumner’s closing line, that one engineer can do a lot more today than a year ago, is the part that’s broader than Bun. If a faithful port of half a million lines, supervised, costs $165k and 11 days, the bottleneck for a class of large rewrites stops being the typing and starts being taste: knowing which port to do, how to scope it, and how to review the output. Bun’s loop is a template people will copy, and the teams that internalize it first get to attempt things that used to require freezing development for a year.&lt;/p&gt;
&lt;h2 id=&quot;should-you-run-bun-14-today&quot;&gt;Should you run Bun 1.4 today?&lt;/h2&gt;
&lt;p&gt;If you’re on Bun in production on 1.3.x, the answer is now yes, with the caution you’d give any runtime major. Bun v1.4.0 landed as a stable release on August 20, tagged &lt;code&gt;Latest&lt;/code&gt; in the &lt;a href=&quot;https://github.com/oven-sh/bun/releases.atom&quot;&gt;release feed&lt;/a&gt;; v1.3.14 from May 13 is the last of the Zig line, and the rewrite is no longer a canary channel you have to opt into.&lt;/p&gt;
&lt;p&gt;To upgrade:&lt;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;sh&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;bun&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string)&quot;&gt; upgrade&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The honest test isn’t the benchmark. Run the leaky-build scenario from the announcement, 2,000 &lt;code&gt;Bun.build()&lt;/code&gt; calls in one process, against whatever you ship, and watch memory. If it holds flat the way the announcement’s table shows, the rewrite did the thing you actually care about. If it doesn’t, file the bug, because the whole premise is that this class of issue is now catchable and fixable instead of permanent.&lt;/p&gt;
&lt;p&gt;I wrote that Bun was the Zig holdout back in mid-July, and the line was stale the week it went up: Sumner had already announced the rewrite. The less comfortable lesson, for anyone writing about this stack, is that “X is written in Y” is now a claim with a short half-life, short enough that being a week behind the news is enough to be wrong. Bun’s Rust rewrite is the cleanest example I’ve seen of AI-assisted porting at a scale that used to take a year, and the open questions about it, how much of the win was the language versus the engineering hours, whether machine-authored &lt;code&gt;unsafe&lt;/code&gt; can be trusted, who owns the runtime your agent runs on, are the ones the rest of the JavaScript toolchain is going to answer next, one rewrite at a time.&lt;/p&gt;
</content:encoded><category>rust</category><category>javascript</category><category>ai</category><category>webdev</category><author>Dennis Morello</author></item><item><title>Baseline 2026: 4 APIs That Replaced My Dependencies</title><link>https://morello.dev/blog/baseline-2026-web-platform-apis</link><guid isPermaLink="true">https://morello.dev/blog/baseline-2026-web-platform-apis</guid><description>Navigation API, container style queries, :open, and Math.sumPrecise() all became Baseline in 2026. The ones that replaced actual dependencies in my projects.</description><pubDate>Wed, 22 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&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 id=&quot;the-navigation-api-one-navigate-event-instead-of-three-different-apis&quot;&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=&quot;https://web.dev/blog/baseline-navigation-api&quot;&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;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-comment)&quot;&gt;// Before: three separate concerns, manually wired&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;window&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;.addEventListener&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;&quot;popstate&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; (e) &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;  renderRoute&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;window&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;location&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;.pathname);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;document&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;.addEventListener&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;&quot;click&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; (e) &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; link&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; e&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;target&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;.closest&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;&quot;a[data-route]&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;  if&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; (link) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;    e&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;.preventDefault&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;    history&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;.pushState&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;null&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt; &quot;&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; link&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;.href);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;    renderRoute&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;new&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; URL&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;link&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;.href).pathname);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-comment)&quot;&gt;// Initial render&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;renderRoute&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;window&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;location&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;.pathname);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And here is the Navigation API equivalent:&lt;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-comment)&quot;&gt;// After: one centralized event&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;navigation&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;.addEventListener&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;&quot;navigate&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; (e) &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; url&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; URL&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;e&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;destination&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;.url);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;  e&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;.intercept&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;    handler&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;      renderRoute&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;url&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;.pathname);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;    }&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&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=&quot;https://github.com/remix-run/react-router/discussions/11046&quot;&gt;React Router’s&lt;/a&gt; and &lt;a href=&quot;https://github.com/TanStack/router/discussions/821&quot;&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=&quot;https://www.infoq.com/news/2026/05/navigation-api-browser&quot;&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=&quot;https://html5doctor.com/interview-with-ian-hickson-html-editor&quot;&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 id=&quot;container-style-queries-theming-without-js-class-toggling&quot;&gt;Container style queries: theming without JS class toggling&lt;/h2&gt;
&lt;p&gt;Container style queries for custom properties &lt;a href=&quot;https://web.dev/blog/web-platform-05-2026&quot;&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;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;jsx&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-comment)&quot;&gt;// Before: JS class toggling for a card variant&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;function&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; Card&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;({ variant &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt; &quot;default&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; }) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;  return&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; (&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;    &amp;lt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;div&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; className&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;`card card--&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;${&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;variant&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;`&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;}&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;      &amp;lt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;h3&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;gt;{title}&amp;lt;/&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;h3&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;      &amp;lt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;gt;{excerpt}&amp;lt;/&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;    &amp;lt;/&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;div&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  );&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;css&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;.card--featured&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; { &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-comment)&quot;&gt;/* special styles */&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;.card--compact&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; { &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-comment)&quot;&gt;/* compact styles */&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; }&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And with container style queries:&lt;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;css&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;.sidebar&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;  container-name&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; sidebar&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-comment)&quot;&gt;  /* no container-type needed for style queries */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;.card&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-comment)&quot;&gt;  /* default styles */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;@container&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; sidebar style(--variant: featured) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;  .card&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-comment)&quot;&gt;    /* featured layout */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;@container&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; sidebar style(--compact: true) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;  .card&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-comment)&quot;&gt;    /* compact layout */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&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=&quot;https://github.com/mdn/content/issues/44701&quot;&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 id=&quot;open-state-tracking-you-never-write&quot;&gt;&lt;code&gt;:open&lt;/code&gt;: state tracking you never write&lt;/h2&gt;
&lt;p&gt;The &lt;a href=&quot;https://web.dev/blog/web-platform-05-2026&quot;&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=&quot;color&quot;&amp;gt;&lt;/code&gt; / &lt;code&gt;&amp;lt;input type=&quot;date&quot;&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;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;css&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-comment)&quot;&gt;/* Before: attribute selector, limited */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;details&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;open&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt; summary&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;  border-radius&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; 4&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;px&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; 4&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;px&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; 0 0&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-comment)&quot;&gt;// Before: manual state tracking for a dialog&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;dialog&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;.addEventListener&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;&quot;toggle&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; () &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;  document&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;body&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;classList&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;.toggle&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;&quot;dialog-open&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; dialog&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;.open);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With &lt;code&gt;:open&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;css&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;details&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;:open &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt; summary&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;  border-radius&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; 4&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;px&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; 4&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;px&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; 0 0&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-comment)&quot;&gt;/* Style the page when any dialog is open */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;html&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;:has&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;dialog&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;:open) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;  overflow&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; hidden&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-comment)&quot;&gt;/* Style a label when its associated select is open */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;label&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;:has&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;select&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;:open) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;  color&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; var&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;(--accent)&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&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 id=&quot;mathsumprecise-the-wait-reduce-is-wrong-moment&quot;&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=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/sumPrecise&quot;&gt;became Baseline Newly available in April 2026&lt;/a&gt;, part of ES2026 after &lt;a href=&quot;https://socket.dev/blog/tc39-advances-11-proposals-for-math-precision-binary-apis-and-more&quot;&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;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; numbers&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;1e20&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; 0.1&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt; -&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;1e20&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;numbers&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;.reduce&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;((a&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; b) &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; a &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; b&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;);  &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-comment)&quot;&gt;// 0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;Math&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;.sumPrecise&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(numbers);              &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-comment)&quot;&gt;// 0.1&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&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=&quot;https://github.com/tc39/proposal-math-sum&quot;&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=&quot;https://github.com/microsoft/TypeScript/issues/63427&quot;&gt;issue #63427&lt;/a&gt;), so you may need a manual declaration depending on your &lt;a href=&quot;/blog/typescript-7-is-here&quot;&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=&quot;/blog/five-things-you-might-not-know-about-javascript&quot;&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 id=&quot;baseline-newly-available-vs-widely-available-what-the-difference-means&quot;&gt;Baseline Newly available vs Widely available: what the difference means&lt;/h2&gt;
&lt;p&gt;Baseline Newly available and Baseline Widely available describe the same support bar at two different ages. A feature becomes Newly available the day the last core browser ships it, and Widely available after that support has held for 30 months. All four APIs here are Newly, not Widely, and that gap is what decides whether you still write a fallback:&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;&quot;navigation&quot; in window&lt;/code&gt;, &lt;code&gt;CSS.supports(&quot;selector(:open)&quot;)&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 id=&quot;the-web-platform-is-eating-libraries-from-the-edges-in&quot;&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=&quot;/blog/the-new-http-query-method&quot;&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;
</content:encoded><category>webdev</category><category>javascript</category><category>css</category><author>Dennis Morello</author></item><item><title>Astro 7: What&apos;s New, What&apos;s Faster, and What Breaks</title><link>https://morello.dev/blog/astro-7</link><guid isPermaLink="true">https://morello.dev/blog/astro-7</guid><description>Astro 7 ships a Rust compiler, Sätteri markdown, Vite 8 with Rolldown, and advanced routing. Builds 15-61% faster. What changed, what breaks, how to upgrade.</description><pubDate>Tue, 21 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Astro 7, released &lt;a href=&quot;https://astro.build/blog/astro-7&quot;&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 id=&quot;what-astro-7-actually-is&quot;&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=&quot;https://astro.build/blog/astro-7&quot;&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=&quot;/blog/typescript-7-is-here&quot;&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 id=&quot;vite-8-and-rolldown&quot;&gt;Vite 8 and Rolldown&lt;/h2&gt;
&lt;p&gt;Astro 7 upgrades to &lt;a href=&quot;https://astro.build/blog/astro-7&quot;&gt;Vite 8&lt;/a&gt;, which ships &lt;a href=&quot;https://rolldown.rs&quot;&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 id=&quot;astro-7s-new-rust-compiler&quot;&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=&quot;https://oxc.rs&quot;&gt;oxc&lt;/a&gt; and &lt;a href=&quot;https://lightningcss.dev&quot;&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 id=&quot;sätteri-markdown-and-mdx-in-rust&quot;&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=&quot;https://github.com/pulldown-cmark/pulldown-cmark&quot;&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=&quot;https://docs.astro.build/en/guides/upgrade-to/v7/&quot;&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 id=&quot;queued-rendering-is-now-the-default&quot;&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 id=&quot;advanced-routing-with-srcfetchts&quot;&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=&quot;https://developers.cloudflare.com/workers/runtime-apis/handlers/fetch/&quot;&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=&quot;https://developers.cloudflare.com/workers/&quot;&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 id=&quot;route-caching-and-cdn-providers&quot;&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 id=&quot;what-breaks-when-upgrading-to-astro-7&quot;&gt;What breaks when upgrading to Astro 7&lt;/h2&gt;
&lt;p&gt;The &lt;a href=&quot;https://docs.astro.build/en/guides/upgrade-to/v7/&quot;&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 id=&quot;should-you-upgrade-to-astro-7&quot;&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=&quot;/blog/git-worktrees-are-underrated&quot;&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=&quot;/blog/typescript-7-is-here&quot;&gt;TypeScript went to Go&lt;/a&gt;, &lt;a href=&quot;/blog/pnpm-v12-rust-rewrite&quot;&gt;pnpm went to Rust&lt;/a&gt;, &lt;a href=&quot;/blog/bun-14-rust-rewrite&quot;&gt;Bun rewrote its runtime from Zig 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;
</content:encoded><category>astro</category><category>webdev</category><category>javascript</category><category>rust</category><author>Dennis Morello</author></item><item><title>Hermes Agent: A Beginner&apos;s Tutorial</title><link>https://morello.dev/blog/hermes-agent-for-beginners</link><guid isPermaLink="true">https://morello.dev/blog/hermes-agent-for-beginners</guid><description>A beginner tutorial for Hermes Agent, the open-source AI agent that learns from experience. Install it, connect a model, and write your first skill.</description><pubDate>Sat, 18 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&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=&quot;https://github.com/NousResearch/hermes-agent&quot;&gt;open-source autonomous agent&lt;/a&gt; built by &lt;a href=&quot;https://nousresearch.com&quot;&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=&quot;https://hermes-agent.nousresearch.com/docs&quot;&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 id=&quot;what-hermes-agent-actually-is&quot;&gt;What Hermes Agent actually is&lt;/h2&gt;
&lt;p&gt;Hermes Agent is an autonomous agent with a &lt;a href=&quot;https://hermes-agent.nousresearch.com/docs&quot;&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=&quot;https://hermes-agent.nousresearch.com/docs/integrations/providers/&quot;&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=&quot;https://portal.nousresearch.com&quot;&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 id=&quot;how-to-install-it&quot;&gt;How to install it&lt;/h2&gt;
&lt;p&gt;The installer handles everything. On macOS, Linux, WSL2, or Android via Termux:&lt;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;curl&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string)&quot;&gt; -fsSL&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string)&quot;&gt; https://hermes-agent.nousresearch.com/install.sh&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt; |&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; bash&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On Windows, in PowerShell:&lt;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;powershell&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;iex (irm https:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;//&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;hermes-agent.nousresearch.com&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;install.ps1)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&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=&quot;https://hermes-agent.nousresearch.com/docs/getting-started/installation&quot;&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;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;hermes&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string)&quot;&gt; setup&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string)&quot;&gt; --portal&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&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=&quot;https://hermes-agent.nousresearch.com/docs/getting-started/installation&quot;&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 id=&quot;first-thing-to-try-write-a-skill-from-experience&quot;&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=&quot;https://hermes-agent.nousresearch.com/docs/skills&quot;&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 id=&quot;second-thing-hook-up-a-messaging-platform&quot;&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;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;hermes&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string)&quot;&gt; gateway&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string)&quot;&gt; setup&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It walks you through connecting Telegram, Discord, Slack, WhatsApp, Signal, or any of the &lt;a href=&quot;https://hermes-agent.nousresearch.com/docs&quot;&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 id=&quot;third-thing-spawn-subagents-for-parallel-work&quot;&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 id=&quot;what-makes-it-different-from-copilot-cursor-and-claude-code&quot;&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;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;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=&quot;/blog/configuring-my-site-for-ai-discoverability&quot;&gt;making sites readable by AI agents&lt;/a&gt; and &lt;a href=&quot;/blog/webmcp-making-your-site-usable-by-ai-agents&quot;&gt;making them usable by agents with WebMCP&lt;/a&gt;. Hermes is the other side of that equation: the agent that can actually consume and act on that content. And when every site needs its own copy of a model to run agents like this, the &lt;a href=&quot;/blog/cross-origin-storage-api&quot;&gt;Cross-Origin Storage API&lt;/a&gt; is the platform fix for the storage problem that creates. If you’d rather have skills that run in the page than on a server, &lt;a href=&quot;/blog/webskill-agent-skills-in-the-browser&quot;&gt;WebSkill&lt;/a&gt; is a draft proposal for Agent Skills in the browser. Install Hermes, teach it one thing, and watch it get better.&lt;/p&gt;
</content:encoded><category>ai</category><category>webdev</category><category>programming</category><category>opensource</category><author>Dennis Morello</author></item><item><title>Cross-Origin Storage API: Stop Downloading Twice</title><link>https://morello.dev/blog/cross-origin-storage-api</link><guid isPermaLink="true">https://morello.dev/blog/cross-origin-storage-api</guid><description>The Cross-Origin Storage API lets sites share one cached copy of a file (a JS library, font, or AI model) across origins by SHA-256 hash. Here&apos;s how it works.</description><pubDate>Fri, 17 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&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=&quot;https://developer.chrome.com/blog/http-cache-partitioning&quot;&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=&quot;https://addyosmani.com/blog/double-keyed-caching/&quot;&gt;mostly stopped making sense&lt;/a&gt;. The &lt;a href=&quot;https://wicg.github.io/cross-origin-storage/&quot;&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 id=&quot;what-is-the-cross-origin-storage-api&quot;&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;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;requestFileHandle&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(hash&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; options)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&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=&quot;https://developer.mozilla.org/en-US/docs/Web/API/File_System_API&quot;&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=&quot;https://blog.tomayac.com/&quot;&gt;Thomas Steiner&lt;/a&gt; and François Beaufort, together with &lt;a href=&quot;https://christianliebel.com/&quot;&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 id=&quot;why-cant-the-cache-api-or-indexeddb-share-files-across-origins&quot;&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;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;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=&quot;https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system&quot;&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 id=&quot;how-does-requestfilehandle-read-and-store-files&quot;&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;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; hash&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  algorithm&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt; &quot;SHA-256&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  value&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt; &quot;8f434346648f6b96df89dda901c5176b10a6d83961dd3c1ac88b59b2dc327aa4&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;};&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; handle&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; navigator&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;crossOriginStorage&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;.requestFileHandle&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(hash);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; file&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; handle&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;.getFile&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;();&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&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;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; handle&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; navigator&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;crossOriginStorage&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;.requestFileHandle&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(hash&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  create&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; true&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  origins&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;&quot;https://example.com&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt; &quot;https://example.org&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;]&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; writable&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; handle&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;.createWritable&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;await&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; writable&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;.write&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(fileBlob);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;await&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; writable&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;.close&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;();&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&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;&quot;*&quot;&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 id=&quot;how-does-cross-origin-storage-avoid-becoming-a-supercookie&quot;&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;&quot;*&quot;&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=&quot;https://github.com/tomayac/public-hash-list&quot;&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 id=&quot;how-does-the-declarative-crossoriginstorage-attribute-work&quot;&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=&quot;https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity&quot;&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;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;html&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;script&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;  src&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;&quot;popular-js-framework.js&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;  integrity&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;&quot;sha256-def456...&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;  crossoriginstorage&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;&quot;*&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;script&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&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 id=&quot;is-the-cross-origin-storage-api-supported-in-browsers-yet&quot;&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=&quot;https://github.com/web-ai-community/cross-origin-storage-extension&quot;&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=&quot;https://huggingface.co/blog/cross-origin-storage&quot;&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=&quot;https://github.com/mozilla/standards-positions/issues/1427&quot;&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=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Storage_Access_API&quot;&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 id=&quot;should-you-use-the-cross-origin-storage-api&quot;&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=&quot;/blog/webmcp-making-your-site-usable-by-ai-agents&quot;&gt;WebMCP&lt;/a&gt; and the &lt;a href=&quot;/blog/the-new-http-query-method&quot;&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=&quot;/blog/configuring-my-site-for-ai-discoverability&quot;&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;
</content:encoded><category>webdev</category><category>javascript</category><category>ai</category><category>performance</category><author>Dennis Morello</author></item><item><title>SolidJS 2.0 Async Data: A Deep Dive for React Devs</title><link>https://morello.dev/blog/solidjs-2-async-data</link><guid isPermaLink="true">https://morello.dev/blog/solidjs-2-async-data</guid><description>How SolidJS 2.0 handles async data: promises in the reactive graph, the Loading boundary, isPending, and generator actions, explained for React developers.</description><pubDate>Thu, 16 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In &lt;a href=&quot;/blog/solidjs-2-react-developers-first-look&quot;&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 id=&quot;how-does-async-data-fetching-work-in-solid-20&quot;&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=&quot;https://github.com/solidjs/solid/blob/next/documentation/solid-2.0/05-async-data.md&quot;&gt;async RFC&lt;/a&gt; puts it: async is “a first-class capability of computations.”&lt;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;jsx&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-comment)&quot;&gt;// Solid 1.x&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;user&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; createResource&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(userId&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; fetchUser);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-comment)&quot;&gt;// Solid 2.0: the memo returns a promise, the graph waits on it&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; user&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; createMemo&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(() &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; fetchUser&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;userId&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;()));&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&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;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; currentUser&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; resolve&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(() &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; user&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;());&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&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 id=&quot;what-replaced-suspense-the-loading-boundary&quot;&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=&quot;https://github.com/solidjs/solid/releases/tag/v2.0.0-beta.0&quot;&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;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;jsx&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;Loading&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; fallback&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;{&amp;lt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;Spinner&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; /&amp;gt;}&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  &amp;lt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;Profile&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; user&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;user&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;()} /&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;Loading&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&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=&quot;https://react.dev/reference/react/useTransition&quot;&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=&quot;https://react.dev/reference/react/Suspense#resetting-suspense-boundaries-on-navigation&quot;&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;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;jsx&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;Loading&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; on&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;userId&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;()} &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;fallback&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;{&amp;lt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;Spinner&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; /&amp;gt;}&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  &amp;lt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;Profile&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; user&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;user&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;()} /&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;Loading&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&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 id=&quot;how-do-you-coordinate-several-boundaries-reveal&quot;&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;&quot;sequential&quot;&lt;/code&gt; (the default), &lt;code&gt;&quot;together&quot;&lt;/code&gt;, or &lt;code&gt;&quot;natural&quot;&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=&quot;sequential&quot;&lt;/code&gt;, that renders a single frontier fallback instead of one per boundary. The &lt;a href=&quot;https://github.com/solidjs/solid/blob/next/documentation/solid-2.0/03-control-flow.md&quot;&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 id=&quot;how-do-you-show-a-refresh-without-a-spinner-ispending&quot;&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;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;jsx&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; refreshing&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; () &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; isPending&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(() &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; user&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;());&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;Loading&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; fallback&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;{&amp;lt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;Spinner&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; /&amp;gt;}&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  &amp;lt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;Show&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; when&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;refreshing&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;()}&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;    &amp;lt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;RefreshBar&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; /&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  &amp;lt;/&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;Show&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  &amp;lt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;Profile&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; user&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;user&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;()} /&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;Loading&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&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;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-comment)&quot;&gt;// Silent: isPending stays false, no refresh UI&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;refresh&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(user);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-comment)&quot;&gt;// Declared: now the refresh reads as pending&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;affects&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(user);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;refresh&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(user);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&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 id=&quot;where-do-mutations-live-now-action-and-optimistic-updates&quot;&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;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;todos&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; setTodos&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; createOptimisticStore&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(() &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; api&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;.getTodos&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;()&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; []);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; addTodo&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; action&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;function*&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; (todo) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;  setTodos&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;((t) &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;    t&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;.push&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(todo); &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-comment)&quot;&gt;// optimistic: show it immediately&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;  yield&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; api&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;.addTodo&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(todo); &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-comment)&quot;&gt;// await the server&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;  refresh&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(todos); &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-comment)&quot;&gt;// reconcile with the source of truth&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&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=&quot;https://github.com/solidjs/solid/blob/next/documentation/solid-2.0/MIGRATION.md&quot;&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=&quot;https://react.dev/reference/react/useOptimistic&quot;&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 id=&quot;why-is-batching-deterministic-now&quot;&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;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;count&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; setCount&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; createSignal&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;setCount&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;count&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(); &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-comment)&quot;&gt;// still 0: queued on the microtask&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;flush&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;count&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(); &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-comment)&quot;&gt;// 1&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&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 id=&quot;what-changed-for-transitions-theyre-built-in&quot;&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;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;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=&quot;/blog/five-things-you-might-not-know-about-javascript&quot;&gt;signals happening across the whole ecosystem&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;should-you-build-on-this-yet&quot;&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=&quot;/blog/pnpm-v12-rust-rewrite&quot;&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=&quot;https://github.com/solidjs/solid/discussions/2596&quot;&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;
</content:encoded><category>solidjs</category><category>react</category><category>javascript</category><category>webdev</category><author>Dennis Morello</author></item><item><title>pnpm v12 Ships the Rust Rewrite</title><link>https://morello.dev/blog/pnpm-v12-rust-rewrite</link><guid isPermaLink="true">https://morello.dev/blog/pnpm-v12-rust-rewrite</guid><description>pnpm v12 is a 1:1 rewrite of its install engine in Rust, with no Node.js launcher and warm installs up to 30x faster. Now stable, so you can upgrade today.</description><pubDate>Wed, 15 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&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=&quot;https://pnpm.io&quot;&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=&quot;/blog/five-things-you-might-not-know-about-javascript&quot;&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=&quot;https://www.rust-lang.org&quot;&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=&quot;/blog/typescript-7-is-here&quot;&gt;TypeScript just made with Go&lt;/a&gt;, aimed at a different bottleneck. And like that one, the interesting detail is how little actually changes for you.&lt;/p&gt;
&lt;h2 id=&quot;what-pnpm-v12-actually-is&quot;&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=&quot;https://github.com/orgs/pnpm/discussions/11292&quot;&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=&quot;https://x.com/zkochan/status/2054966567692099943&quot;&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 id=&quot;why-did-pnpm-choose-rust&quot;&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=&quot;https://pnpm.io/blog/releases/11.10&quot;&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 id=&quot;how-much-faster-is-pnpm-v12&quot;&gt;How much faster is pnpm v12?&lt;/h2&gt;
&lt;p&gt;pnpm publishes a &lt;a href=&quot;https://pnpm.io/benchmarks&quot;&gt;benchmark suite&lt;/a&gt; comparing v11 against the Rust engine directly, and it re-runs weekly, so the exact figures move. As of late August 2026, here’s what it shows:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th style=&quot;text-align: right&quot;&gt;pnpm v11&lt;/th&gt;
&lt;th style=&quot;text-align: right&quot;&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 style=&quot;text-align: right&quot;&gt;472 ms&lt;/td&gt;
&lt;td style=&quot;text-align: right&quot;&gt;15 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;node_modules present, no cache&lt;/td&gt;
&lt;td style=&quot;text-align: right&quot;&gt;580 ms&lt;/td&gt;
&lt;td style=&quot;text-align: right&quot;&gt;67 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clean install (nothing cached)&lt;/td&gt;
&lt;td style=&quot;text-align: right&quot;&gt;8.2 s&lt;/td&gt;
&lt;td style=&quot;text-align: right&quot;&gt;5 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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 472ms to 15ms, roughly a 30x difference, and it’s the number you’ll feel most, because it runs constantly. The colder cases improve less dramatically: a fully clean install with nothing cached goes from 8.2s to 5s, about 1.6x, since the wall-clock time there is dominated by network and tarball unpacking rather than the engine.&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 id=&quot;its-already-rolling-out-incrementally&quot;&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=&quot;https://pnpm.io/blog/releases/11.7&quot;&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 is the default rather than an opt-in. The &lt;a href=&quot;https://github.com/pnpm/pnpm/issues/11633&quot;&gt;roadmap&lt;/a&gt; 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 in v12, parts of pnpm still run through the TypeScript implementation for now. The lockfile is what lets the two halves cooperate.&lt;/p&gt;
&lt;h2 id=&quot;what-changes-for-you&quot;&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 &lt;a href=&quot;https://github.com/pnpm/pnpm/releases/tag/v12.0.0&quot;&gt;breaking changes that shipped&lt;/a&gt; are narrow, and most are about correctness rather than workflow. pnpm now treats a git dependency on a known host (GitHub, GitLab, Bitbucket) as an identity rather than a transport choice, so the lockfile stops recording a machine-specific SSH URL that only worked wherever resolution happened to run. An unrecognized setting in &lt;code&gt;pnpm-workspace.yaml&lt;/code&gt; is reported instead of silently ignored, and fails the command outright when your project pins a matching pnpm version. And dependency cycles are broken canonically, so repeated installs produce byte-identical lockfiles regardless of importer order. One more change touches the command line: &lt;code&gt;--resolution-only&lt;/code&gt; is gone, and v12 &lt;a href=&quot;https://pnpm.io/blog/whats-different-in-pnpm-12&quot;&gt;rejects the flag outright&lt;/a&gt;. Its old job of printing peer-dependency issues now falls to &lt;code&gt;pnpm peers check&lt;/code&gt;, which reads them straight from the lockfile without a reinstall or a re-resolution. If none of those touch your setup, a v12 upgrade should be close to a no-op.&lt;/p&gt;
&lt;h3 id=&quot;will-the-upgrade-break-your-ci&quot;&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=&quot;/blog/git-worktrees-are-underrated&quot;&gt;git worktree&lt;/a&gt; before you flip it on in CI, the same sandbox I used while &lt;a href=&quot;/blog/solidjs-2-async-data&quot;&gt;exploring SolidJS 2.0’s async data model&lt;/a&gt;. This is still moving, so the right list of gaps is whatever the release notes and roadmap say on the day you upgrade.&lt;/p&gt;
&lt;h2 id=&quot;the-bigger-picture&quot;&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=&quot;https://biomejs.dev&quot;&gt;Biome&lt;/a&gt; for linting and formatting, &lt;a href=&quot;https://oxc.rs&quot;&gt;Oxc&lt;/a&gt; for parsing and transforming, &lt;a href=&quot;https://rolldown.rs&quot;&gt;Rolldown&lt;/a&gt; and Turbopack for bundling, and now &lt;a href=&quot;/blog/astro-7&quot;&gt;Astro 7&lt;/a&gt; with its Rust compiler and Sätteri markdown pipeline. TypeScript’s compiler went to Go. Deno is Rust from the ground up. (Bun was the odd one out for a while, written in Zig, not Rust. That’s no longer true: it’s now rewriting itself from Zig to Rust too, an AI-assisted port I wrote up separately in &lt;a href=&quot;/blog/bun-14-rust-rewrite&quot;&gt;Bun’s Rust rewrite&lt;/a&gt;.)&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 id=&quot;should-you-upgrade-to-pnpm-v12&quot;&gt;Should you upgrade to pnpm v12?&lt;/h2&gt;
&lt;p&gt;You can, and there’s little reason to hold off. pnpm &lt;a href=&quot;https://github.com/pnpm/pnpm/releases/tag/v12.0.0&quot;&gt;v12.0.0 shipped stable on August 26, 2026&lt;/a&gt;, so the Rust engine is no longer an alpha you opt into on a branch. Run &lt;code&gt;pnpm self-update&lt;/code&gt; to move onto it, or bump the &lt;code&gt;packageManager&lt;/code&gt; pin in your repo, and your existing scripts keep working.&lt;/p&gt;
&lt;p&gt;If you want to feel the difference before you commit the whole team, do a warm install on a throwaway branch and watch it 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 no longer one you ever wait on.&lt;/p&gt;
</content:encoded><category>javascript</category><category>webdev</category><category>rust</category><category>node</category><author>Dennis Morello</author></item><item><title>Make Your Site Usable by AI Agents with WebMCP</title><link>https://morello.dev/blog/webmcp-making-your-site-usable-by-ai-agents</link><guid isPermaLink="true">https://morello.dev/blog/webmcp-making-your-site-usable-by-ai-agents</guid><description>AI agents struggle to act on websites. WebMCP lets your page expose typed tools they can call directly. What it is, how to try it in Chrome, and the caveats.</description><pubDate>Mon, 13 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A while back I wrote about &lt;a href=&quot;/blog/configuring-my-site-for-ai-discoverability&quot;&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 id=&quot;what-is-webmcp&quot;&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=&quot;https://developer.chrome.com/docs/ai/webmcp&quot;&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=&quot;https://modelcontextprotocol.io&quot;&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 id=&quot;why-not-just-let-the-agent-read-the-page&quot;&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;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;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 id=&quot;what-does-the-api-look-like&quot;&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;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;await&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; document&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;modelContext&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;.registerTool&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  name&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt; &quot;search_posts&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  description&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt; &quot;Search blog posts by tag. Returns matching titles and URLs.&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  inputSchema&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;    type&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt; &quot;object&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;    properties&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;      tag&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; { type&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt; &quot;string&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; description&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt; &quot;A single lowercase tag, e.g. &apos;webdev&apos;&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; }&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;    }&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;    required&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;&quot;tag&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;]&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  }&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;  execute&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt; async&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; ({ tag }) &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;    const&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; posts&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; findPostsByTag&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(tag);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt; `Found &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;${&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;posts&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;length&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt; posts tagged &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;${&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;tag&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;.`&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  }&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That example returns a plain string, which the &lt;a href=&quot;https://developer.chrome.com/docs/ai/webmcp/imperative-api&quot;&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=&quot;https://modelcontextprotocol.io/specification/2025-06-18/server/tools&quot;&gt;&lt;code&gt;{ content: [{ type: &quot;text&quot;, text: &quot;…&quot; }] }&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 id=&quot;how-do-you-try-it-today&quot;&gt;How do you try it today?&lt;/h2&gt;
&lt;p&gt;In Chrome, behind an origin trial. Per the &lt;a href=&quot;https://groups.google.com/a/chromium.org/g/blink-dev/c/gmYffo5WOE8/m/OJxuQRP3AAAJ&quot;&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=&quot;https://developer.chrome.com/blog/ai-webmcp-origin-trial&quot;&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 id=&quot;is-webmcp-an-actual-standard&quot;&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=&quot;https://webmachinelearning.github.io/webmcp/&quot;&gt;W3C Web Machine Learning Community Group&lt;/a&gt;, and the draft I read when I wrote this was dated &lt;strong&gt;July 10, 2026&lt;/strong&gt; (it has kept moving since). 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=&quot;https://github.com/MiguelsPizza&quot;&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 that draft is dated July 2026, so when I wrote this we were 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 id=&quot;what-about-security&quot;&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=&quot;https://developer.chrome.com/docs/ai/webmcp/secure-tools&quot;&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;consequentialHint&lt;/code&gt;&lt;/strong&gt; marks a tool whose execution has significant effects, like booking travel or transferring money, so the agent or browser can ask the user to confirm before it runs.&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 id=&quot;should-you-add-it-to-your-site&quot;&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=&quot;/blog/configuring-my-site-for-ai-discoverability&quot;&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=&quot;/blog/the-new-http-query-method&quot;&gt;the QUERY method&lt;/a&gt; and &lt;a href=&quot;/blog/cross-origin-storage-api&quot;&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. A skills layer on top of it is already being proposed: &lt;a href=&quot;/blog/webskill-agent-skills-in-the-browser&quot;&gt;WebSkill&lt;/a&gt; packages instructions and scripts as Agent Skills that call WebMCP tools.&lt;/p&gt;
</content:encoded><category>ai</category><category>webdev</category><category>javascript</category><category>mcp</category><author>Dennis Morello</author></item><item><title>Build-Time OG Images with Satori and Astro</title><link>https://morello.dev/blog/generating-og-images-with-satori-and-astro</link><guid isPermaLink="true">https://morello.dev/blog/generating-og-images-with-satori-and-astro</guid><description>How this Astro site renders social cards at build time with Satori and resvg-js, and the design and performance choices that keep it fast.</description><pubDate>Sun, 12 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Every page on this site ships with its own &lt;a href=&quot;https://ogp.me/&quot;&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=&quot;https://github.com/vercel/satori&quot;&gt;Satori&lt;/a&gt; turns a chunk of JSX styled with a CSS subset into an SVG, and &lt;a href=&quot;https://github.com/yisibl/resvg-js&quot;&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 id=&quot;what-actually-generates-the-images&quot;&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=&quot;https://vercel.com/docs/og-image-generation&quot;&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=&quot;https://github.com/yisibl&quot;&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=&quot;https://github.com/linebender/resvg&quot;&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;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; svg&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; satori&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(layout&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  width&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; OG_WIDTH&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  height&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; OG_HEIGHT&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  fonts&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;    { name&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt; &quot;JetBrains Mono&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; data&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; fonts&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;.regular&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; weight&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; 400&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; style&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt; &quot;normal&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; }&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;    { name&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt; &quot;JetBrains Mono&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; data&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; fonts&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;.bold&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; weight&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; 700&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; style&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt; &quot;normal&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; }&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  ]&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; resvg&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; Resvg&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(svg&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  font&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; { loadSystemFonts&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; false&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; }&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  fitTo&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; { mode&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt; &quot;width&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; value&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; OG_WIDTH&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; }&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;return&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; resvg&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;.render&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;()&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;.asPng&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;();&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&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 id=&quot;the-design-one-card-deliberately-plain&quot;&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=&quot;https://www.jetbrains.com/lp/mono/&quot;&gt;JetBrains Mono&lt;/a&gt; throughout, which matches the monospace-first identity of &lt;a href=&quot;/blog/the-new-website&quot;&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=&quot;/blog/the-new-website&quot;&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 id=&quot;why-generate-at-build-time-instead-of-on-demand&quot;&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;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt; const&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; getStaticPaths&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; GetStaticPaths&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt; async&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; () &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;posts&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; tags&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; Promise&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;.all&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;([&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;getCollection&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;&quot;blog&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; getAllTags&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;()]);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-comment)&quot;&gt;  // ...one entry per static page, post, and tag&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;};&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&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=&quot;https://www.cloudflare.com/&quot;&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 id=&quot;the-performance-decisions-that-matter&quot;&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=&quot;https://docs.astro.build/en/reference/experimental-flags/fonts/&quot;&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;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt; const&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; loadFonts&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; (() &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;  let&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; cache&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; Promise&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;lt;{ regular&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; ArrayBuffer&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;; bold&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; ArrayBuffer&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; }&amp;gt; &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;|&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; undefined&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;  return&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; () &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;    cache &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;??=&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; Promise&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;.all&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;([&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;loadWeight&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;&quot;400&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; loadWeight&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;&quot;700&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;)])&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;.then&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;      ([regular&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; bold]) &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; ({ regular&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; bold })&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;    );&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; cache;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  };&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;})();&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&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 id=&quot;og-best-practices-worth-stealing&quot;&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=&quot;https://developers.facebook.com/docs/sharing/webmasters/images/&quot;&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=&quot;https://ogp.me/&quot;&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 id=&quot;the-takeaway&quot;&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=&quot;/blog/the-new-website&quot;&gt;rebuilding it with Astro and Tailwind&lt;/a&gt;, the &lt;a href=&quot;/blog/configuring-my-site-for-ai-discoverability&quot;&gt;discoverability plumbing&lt;/a&gt; that OG images are one small part of, and the &lt;a href=&quot;/blog/astro-7&quot;&gt;Astro 7 upgrade&lt;/a&gt; whose Rust compiler keeps the build fast.&lt;/p&gt;
</content:encoded><category>astro</category><category>webdev</category><category>javascript</category><category>performance</category><author>Dennis Morello</author></item><item><title>shadcn/typeset vs Tailwind Typography for Markdown</title><link>https://morello.dev/blog/shadcn-typeset-vs-tailwind-prose</link><guid isPermaLink="true">https://morello.dev/blog/shadcn-typeset-vs-tailwind-prose</guid><description>shadcn/typeset styles rendered markdown with one CSS file you own. Here&apos;s how it works and how it compares to Tailwind Typography and its prose class.</description><pubDate>Fri, 10 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&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=&quot;https://x.com/shadcn&quot;&gt;shadcn&lt;/a&gt; shipped a take on this on July 10, 2026, called &lt;a href=&quot;https://ui.shadcn.com/docs/typeset&quot;&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 id=&quot;what-is-shadcntypeset&quot;&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;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;jsx&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;div&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; className&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;&quot;typeset typeset-docs&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  &amp;lt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;YourMarkdownRenderer&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;gt;{content}&amp;lt;/&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;YourMarkdownRenderer&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;div&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&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=&quot;https://ui.shadcn.com/docs/changelog/2026-07-typeset&quot;&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=&quot;https://ui.shadcn.com/docs/components/base/typography&quot;&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 id=&quot;how-do-you-install-shadcntypeset&quot;&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=&quot;https://ui.shadcn.com/typeset&quot;&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;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;css&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;@import&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt; &quot;tailwindcss&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;@import&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt; &quot;./typeset.css&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&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 id=&quot;size-leading-and-flow&quot;&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;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;css&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;.typeset&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  --typeset-size&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;em&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;;      &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-comment)&quot;&gt;/* base font size */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  --typeset-leading&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; 1.75&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;;  &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-comment)&quot;&gt;/* line-height */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  --typeset-flow&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; 1.25&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;em&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;;   &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-comment)&quot;&gt;/* vertical space between blocks */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A preset is just those three variables set to different values under a class name:&lt;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;css&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;.typeset-chat&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  --typeset-flow&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;em&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  --typeset-leading&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; 1.6&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;.typeset-docs&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  --typeset-size&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; 15&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;px&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  --typeset-flow&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; 1.5&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;em&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Because it’s all CSS custom properties, you can override a single value inline without touching the file:&lt;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;jsx&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;article&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; className&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;&quot;typeset [--typeset-flow:1.75em]&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;gt;{content}&amp;lt;/&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;article&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&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 id=&quot;why-your-tailwind-utilities-win-without-important&quot;&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;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;css&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;@layer&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; components {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;  .typeset&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;    &amp;amp; &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;where(h1&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;) { &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;font-size&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; 1.75&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;em&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;; &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-comment)&quot;&gt;/* ... */&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;    &amp;amp; &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;:where&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;)  { &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;margin-block-start&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; var&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;(--typeset-flow)&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;; }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&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;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;jsx&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;Card&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; className&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;&quot;not-typeset&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;gt;Untouched component.&amp;lt;/&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;Card&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&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=&quot;https://github.com/tailwindlabs/tailwindcss-typography&quot;&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 id=&quot;shadcntypeset-vs-tailwind-typography&quot;&gt;shadcn/typeset vs Tailwind Typography&lt;/h2&gt;
&lt;p&gt;The &lt;a href=&quot;https://github.com/tailwindlabs/tailwindcss-typography&quot;&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;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;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 id=&quot;what-makes-it-streaming-safe&quot;&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;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;css&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;amp; &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;:where&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;  margin-block-start&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; var&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;(--typeset-flow)&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;  margin-block-end&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&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 id=&quot;what-it-costs-you&quot;&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=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/margin-trim&quot;&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 id=&quot;should-you-use-it&quot;&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=&quot;/blog/the-new-website&quot;&gt;rebuilding this site&lt;/a&gt;: the long-form styling here is hand-written CSS wired to theme variables rather than a plugin, for exactly the reason Typeset exists, and I’ve since &lt;a href=&quot;/blog/replacing-tailwind-with-vanilla-css&quot;&gt;replaced Tailwind with vanilla CSS across the whole site&lt;/a&gt;. The same instinct runs through &lt;a href=&quot;/blog/generating-og-images-with-satori-and-astro&quot;&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=&quot;https://ui.shadcn.com/typeset&quot;&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;
</content:encoded><category>css</category><category>tailwindcss</category><category>webdev</category><category>react</category><author>Dennis Morello</author></item><item><title>TypeScript 7 Is Here: The Go Compiler Rewrite</title><link>https://morello.dev/blog/typescript-7-is-here</link><guid isPermaLink="true">https://morello.dev/blog/typescript-7-is-here</guid><description>TypeScript 7.0 ships the compiler rewritten in Go: 8 to 12x faster builds, the same type system, and a new LSP tooling story. Here&apos;s what changed.</description><pubDate>Thu, 09 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&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 even with the &lt;a href=&quot;/blog/five-things-you-might-not-know-about-javascript&quot;&gt;JIT compilation that makes modern engines fast&lt;/a&gt;, 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=&quot;https://devblogs.microsoft.com/typescript/announcing-typescript-7-0/&quot;&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=&quot;https://go.dev&quot;&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 id=&quot;what-typescript-7-actually-is&quot;&gt;What TypeScript 7 actually is&lt;/h2&gt;
&lt;p&gt;TypeScript 7 is a &lt;a href=&quot;https://devblogs.microsoft.com/typescript/typescript-native-port/&quot;&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=&quot;https://devblogs.microsoft.com/typescript/announcing-typescript-7-0-rc/&quot;&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=&quot;https://devblogs.microsoft.com/typescript/progress-on-typescript-7-december-2025/&quot;&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 id=&quot;why-go-of-all-things&quot;&gt;Why Go, of all things&lt;/h2&gt;
&lt;p&gt;This was the question everyone asked when &lt;a href=&quot;https://devblogs.microsoft.com/typescript/typescript-native-port/&quot;&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=&quot;https://github.com/microsoft/typescript-go/discussions/411&quot;&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=&quot;https://en.wikipedia.org/wiki/Anders_Hejlsberg&quot;&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=&quot;/blog/pnpm-v12-rust-rewrite&quot;&gt;Rust rewrite of pnpm&lt;/a&gt; to Bun’s own &lt;a href=&quot;/blog/bun-14-rust-rewrite&quot;&gt;Zig-to-Rust rewrite&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;how-much-faster-is-typescript-7&quot;&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=&quot;https://devblogs.microsoft.com/typescript/announcing-typescript-7-0/&quot;&gt;7.0 release&lt;/a&gt; against TypeScript 6:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Project&lt;/th&gt;
&lt;th style=&quot;text-align: right&quot;&gt;TS 6&lt;/th&gt;
&lt;th style=&quot;text-align: right&quot;&gt;TS 7&lt;/th&gt;
&lt;th style=&quot;text-align: right&quot;&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 style=&quot;text-align: right&quot;&gt;125.7s&lt;/td&gt;
&lt;td style=&quot;text-align: right&quot;&gt;10.6s&lt;/td&gt;
&lt;td style=&quot;text-align: right&quot;&gt;11.9x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sentry&lt;/td&gt;
&lt;td style=&quot;text-align: right&quot;&gt;139.8s&lt;/td&gt;
&lt;td style=&quot;text-align: right&quot;&gt;15.7s&lt;/td&gt;
&lt;td style=&quot;text-align: right&quot;&gt;8.9x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bluesky&lt;/td&gt;
&lt;td style=&quot;text-align: right&quot;&gt;24.3s&lt;/td&gt;
&lt;td style=&quot;text-align: right&quot;&gt;2.8s&lt;/td&gt;
&lt;td style=&quot;text-align: right&quot;&gt;8.7x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Playwright&lt;/td&gt;
&lt;td style=&quot;text-align: right&quot;&gt;12.8s&lt;/td&gt;
&lt;td style=&quot;text-align: right&quot;&gt;1.47s&lt;/td&gt;
&lt;td style=&quot;text-align: right&quot;&gt;8.7x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tldraw&lt;/td&gt;
&lt;td style=&quot;text-align: right&quot;&gt;11.2s&lt;/td&gt;
&lt;td style=&quot;text-align: right&quot;&gt;1.46s&lt;/td&gt;
&lt;td style=&quot;text-align: right&quot;&gt;7.7x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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=&quot;https://devblogs.microsoft.com/typescript/typescript-native-port/&quot;&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=&quot;/blog/five-things-you-might-not-know-about-javascript&quot;&gt;JIT-compiled JavaScript&lt;/a&gt; for this kind of work, and Go lets the compiler use &lt;a href=&quot;https://devblogs.microsoft.com/typescript/announcing-typescript-7-0/&quot;&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 id=&quot;how-to-install-typescript-7&quot;&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;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;sh&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;npm&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string)&quot;&gt; install&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string)&quot;&gt; -D&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string)&quot;&gt; typescript&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&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;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;sh&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;npx&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string)&quot;&gt; tsgo&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string)&quot;&gt; --project&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string)&quot;&gt; ./src/tsconfig.json&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&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=&quot;https://github.com/microsoft/typescript-go&quot;&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 id=&quot;the-60--70-split&quot;&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=&quot;https://devblogs.microsoft.com/typescript/announcing-typescript-6-0/&quot;&gt;6.0&lt;/a&gt;, released in March 2026, is the &lt;a href=&quot;https://devblogs.microsoft.com/typescript/progress-on-typescript-7-december-2025/&quot;&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=&quot;https://devblogs.microsoft.com/typescript/announcing-typescript-7-0/&quot;&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 id=&quot;what-doesnt-work-in-typescript-7-yet&quot;&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=&quot;https://microsoft.github.io/language-server-protocol/&quot;&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=&quot;https://vuejs.org&quot;&gt;Vue&lt;/a&gt;, &lt;a href=&quot;https://svelte.dev&quot;&gt;Svelte&lt;/a&gt;, &lt;a href=&quot;https://astro.build&quot;&gt;Astro&lt;/a&gt;, MDX, and &lt;a href=&quot;https://angular.dev&quot;&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 id=&quot;should-you-upgrade-to-typescript-7&quot;&gt;Should you upgrade to TypeScript 7?&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=&quot;/blog/git-worktrees-are-underrated&quot;&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=&quot;https://github.com/microsoft/typescript-go&quot;&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=&quot;/blog/solidjs-2-react-developers-first-look&quot;&gt;SolidJS 2.0 is making with async&lt;/a&gt;: keep the surface still, rebuild the engine.&lt;/p&gt;
</content:encoded><category>typescript</category><category>javascript</category><category>webdev</category><category>programming</category><author>Dennis Morello</author></item><item><title>The HTTP QUERY Method: Safe Reads with a Body</title><link>https://morello.dev/blog/the-new-http-query-method</link><guid isPermaLink="true">https://morello.dev/blog/the-new-http-query-method</guid><description>HTTP finally has a safe, idempotent, cacheable method that carries a body. Here&apos;s what QUERY (RFC 10008) is, why it exists, and how to use it.</description><pubDate>Tue, 07 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&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=&quot;https://www.rfc-editor.org/info/rfc10008/&quot;&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 id=&quot;the-two-bad-options&quot;&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=&quot;https://www.rfc-editor.org/rfc/rfc9110#name-safe-methods&quot;&gt;safe&lt;/a&gt; (the client isn’t asking to change anything), it’s &lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc9110#name-idempotent-methods&quot;&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=&quot;https://www.rfc-editor.org/rfc/rfc9110#name-get&quot;&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 id=&quot;what-query-actually-is&quot;&gt;What QUERY actually is&lt;/h2&gt;
&lt;p&gt;The spec, authored by Julian Reschke, &lt;a href=&quot;https://github.com/jasnell&quot;&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;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;http&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;QUERY /products HTTP/1.1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;Host&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string)&quot;&gt; example.com&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;Content-Type&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string)&quot;&gt; application/json&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;Accept&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string)&quot;&gt; application/json&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;  &quot;filters&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; { &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;&quot;category&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt; &quot;keyboards&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt; &quot;inStock&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; true&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; }&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;  &quot;sort&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; [{ &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;&quot;field&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt; &quot;price&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt; &quot;order&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt; &quot;asc&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; }]&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;  &quot;page&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; { &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;&quot;size&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; 20&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&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=&quot;/blog/configuring-my-site-for-ai-discoverability&quot;&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 id=&quot;the-details-that-bite&quot;&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 id=&quot;can-you-use-http-query-today&quot;&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;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; res&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; fetch&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;&quot;/products&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  method&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt; &quot;QUERY&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  headers&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; { &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;&quot;Content-Type&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt; &quot;application/json&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; }&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  body&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; JSON&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;.stringify&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;({ filters&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; { category&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt; &quot;keyboards&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; } })&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One gotcha before you do: pass the method in uppercase. &lt;a href=&quot;https://fetch.spec.whatwg.org/#concept-method-normalize&quot;&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: &quot;query&quot;&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=&quot;https://fetch.spec.whatwg.org/#cors-safelisted-method&quot;&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. Some of that is already happening: &lt;a href=&quot;/blog/htmx-4&quot;&gt;htmx 4&lt;/a&gt;, released in August 2026, ships a new &lt;code&gt;hx-query&lt;/code&gt; attribute that issues an HTTP QUERY request straight from your markup.&lt;/p&gt;
&lt;h2 id=&quot;should-you-rush-to-switch&quot;&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=&quot;/blog/cross-origin-storage-api&quot;&gt;Cross-Origin Storage API&lt;/a&gt; and &lt;a href=&quot;/blog/webmcp-making-your-site-usable-by-ai-agents&quot;&gt;WebMCP&lt;/a&gt; got right: the platform finally naming something we’d been faking. QUERY isn’t the only 2026 addition that replaced a workaround; the Navigation API, container style queries, and the &lt;code&gt;:open&lt;/code&gt; pseudo-class all &lt;a href=&quot;/blog/baseline-2026-web-platform-apis&quot;&gt;became Baseline this year too&lt;/a&gt;. 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;
</content:encoded><category>http</category><category>webdev</category><category>api</category><category>backend</category><author>Dennis Morello</author></item><item><title>SolidJS 2.0 for React Developers: A First Look</title><link>https://morello.dev/blog/solidjs-2-react-developers-first-look</link><guid isPermaLink="true">https://morello.dev/blog/solidjs-2-react-developers-first-look</guid><description>SolidJS 2.0 lands first-class async, a new Loading boundary, and signals. Here&apos;s what caught my eye after years of writing React.</description><pubDate>Thu, 02 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&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=&quot;https://github.com/solidjs/solid/releases/tag/v2.0.0-beta.0&quot;&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 id=&quot;what-is-solidjs-20&quot;&gt;What is SolidJS 2.0?&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.solidjs.com&quot;&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=&quot;https://docs.solidjs.com/concepts/intro-to-reactivity&quot;&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;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;sh&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;pnpm&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string)&quot;&gt; add&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string)&quot;&gt; solid-js@next&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&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 id=&quot;fine-grained-reactivity-from-a-react-brain&quot;&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=&quot;https://react.dev/learn/react-compiler&quot;&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;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;jsx&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;function&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; Counter&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;count&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; setCount&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; createSignal&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-comment)&quot;&gt;  // This whole function runs ONCE. Only the text node updates on click.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;  return&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; &amp;lt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;button&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; onClick&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;{() &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; setCount&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;count&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;() &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;)}&amp;gt;{&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;count&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;()}&amp;lt;/&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;button&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&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 id=&quot;why-async-is-the-real-story-of-solidjs-20&quot;&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;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;jsx&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-comment)&quot;&gt;// Solid 1.x&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;data&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; createResource&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(userId&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; fetchUser);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-comment)&quot;&gt;// Solid 2.0: the promise flows through the reactive graph&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; data&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; createMemo&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(() &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; fetchUser&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;userId&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;()));&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&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;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;jsx&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; users&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; createMemo&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(() &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; api&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;.listUsers&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;());&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; refreshing&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; () &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; isPending&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(() &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; users&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;());&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;lt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  &amp;lt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;Show&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; when&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;refreshing&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;()}&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;    &amp;lt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;RefreshIndicator&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; /&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  &amp;lt;/&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;Show&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  &amp;lt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;Loading&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; fallback&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;{&amp;lt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;Spinner&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; /&amp;gt;}&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;    &amp;lt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;UserList&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; users&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;users&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;()} /&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  &amp;lt;/&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;Loading&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;lt;/&amp;gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;React 19 gets you most of the way here: put the update in a transition with &lt;a href=&quot;https://react.dev/reference/react/useTransition&quot;&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=&quot;/blog/solidjs-2-async-data&quot;&gt;follow-up deep dive on Solid 2.0’s async data&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;how-do-mutations-work-in-solidjs-20&quot;&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=&quot;https://react.dev/blog/2024/12/05/react-19&quot;&gt;Actions&lt;/a&gt; and the &lt;a href=&quot;https://react.dev/reference/react/useOptimistic&quot;&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;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;messages&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; setMessages&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; createOptimisticStore&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(() &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; chatServer&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;.loadMessages&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;()&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; []);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; sendMessage&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; action&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;function*&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; (next) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;  setMessages&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;((m) &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;    m&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;.push&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(next); &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-comment)&quot;&gt;// optimistic: show it instantly&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;  yield&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; chatServer&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;.sendMessage&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(next); &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-comment)&quot;&gt;// await the server&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;  refresh&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(messages); &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-comment)&quot;&gt;// reconcile with the source of truth&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&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=&quot;https://docs.solidjs.com/solid-router/reference/data-apis/create-async&quot;&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 id=&quot;deterministic-batching-and-a-gotcha&quot;&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;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;count&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; setCount&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; createSignal&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;setCount&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;count&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(); &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-comment)&quot;&gt;// still 0: the update is queued on the microtask&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;flush&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(); &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-comment)&quot;&gt;// apply queued updates synchronously&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;count&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(); &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-comment)&quot;&gt;// now 1&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&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 id=&quot;the-renames-a-react-developer-should-know&quot;&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=&quot;https://github.com/solidjs/solid/blob/next/documentation/solid-2.0/MIGRATION.md&quot;&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;p&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;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&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;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-comment)&quot;&gt;// Solid 1.x&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;createEffect&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(() &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; apiCall&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;signalB&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;()));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-comment)&quot;&gt;// Solid 2.0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;createEffect&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(signalB&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; apiCall);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;createEffect&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  () &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;signalA&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;()&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; signalB&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;()]&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  ([a&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; b]) &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; a &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; apiCall&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(b)&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&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;/li&gt;
&lt;li&gt;
&lt;p&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;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&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;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&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;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-comment)&quot;&gt;// 2.0 default: mutate the draft&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;setStore&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;((s) &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;  s&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;.todos[id].done &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; true&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-comment)&quot;&gt;// Legacy path style, opt-in&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;setStore&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;storePath&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;&quot;todos&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; id&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt; &quot;done&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; true&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;));&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&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;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&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;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&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;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;jsx&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; Theme&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; createContext&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(); &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-comment)&quot;&gt;// no default&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-comment)&quot;&gt;// 2.0: the context is its own provider&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;Theme&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; value&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;&quot;dark&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;gt;{&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-comment)&quot;&gt;/* ... */&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;}&amp;lt;/&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;Theme&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;signals-are-heading-toward-the-standard&quot;&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=&quot;https://github.com/tc39/proposal-signals&quot;&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=&quot;https://dev.to/this-is-learning/the-evolution-of-signals-in-javascript-8ob&quot;&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=&quot;/blog/five-things-you-might-not-know-about-javascript&quot;&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=&quot;/blog/typescript-7-is-here&quot;&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=&quot;https://svelte.dev/blog/runes&quot;&gt;runes&lt;/a&gt; API, and it’s unlikely to be the last framework to make the jump.&lt;/p&gt;
&lt;h2 id=&quot;should-you-drop-react-no-but-watch-this-closely&quot;&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=&quot;https://github.com/solidjs/solid/releases/tag/v2.0.0-beta.0&quot;&gt;github.com/solidjs/solid&lt;/a&gt;, and the &lt;a href=&quot;https://github.com/solidjs/solid/discussions/2596&quot;&gt;announcement discussion&lt;/a&gt; is worth reading in full if any of this piqued your interest.&lt;/p&gt;
</content:encoded><category>solidjs</category><category>react</category><category>javascript</category><category>webdev</category><author>Dennis Morello</author></item><item><title>Rebuilding Defrag98: Getting the Details Right</title><link>https://morello.dev/blog/defrag98-rebuild</link><guid isPermaLink="true">https://morello.dev/blog/defrag98-rebuild</guid><description>How I rebuilt Defrag98 to feel like the real thing: a truer simulation, canvas rendering, a lighter Vite stack, and full offline support.</description><pubDate>Tue, 30 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;There’s a specific kind of satisfaction that comes from watching the Windows 98 defrag screen do its thing. Clusters shuffling around. That progress bar crawling. The sense that your computer was doing something &lt;em&gt;important&lt;/em&gt;, even if you had no idea what.&lt;/p&gt;
&lt;p&gt;That’s the feeling I wanted to bottle when I built &lt;a href=&quot;/blog/windows-98-defrag-simulator&quot;&gt;the first version of defrag98.com&lt;/a&gt; back in 2024. The project took off beyond anything I’d expected. It got picked up by &lt;a href=&quot;https://news.ycombinator.com/item?id=40962195&quot;&gt;Hacker News&lt;/a&gt; and &lt;a href=&quot;https://www.theverge.com/2024/7/14/24198206/take-a-moment-to-reflect&quot;&gt;The Verge&lt;/a&gt;, and people from all over the world spent time staring at a browser tab the way their younger selves once stared at a CRT monitor. Mission accomplished.&lt;/p&gt;
&lt;p&gt;But the more I looked at it, the more I noticed the cracks.&lt;/p&gt;
&lt;h2 id=&quot;it-wasnt-quite-right&quot;&gt;It wasn’t quite right&lt;/h2&gt;
&lt;p&gt;The most significant problem was the simulation logic. The original Windows 98 defragmenter didn’t march through clusters sequentially. It read blocks, wrote them, and shuffled them around in a messier, more organic process that made watching it genuinely hypnotic. My first implementation was too orderly. It &lt;em&gt;looked&lt;/em&gt; like a simulation. The real one looked like a machine thinking.&lt;/p&gt;
&lt;p&gt;The legend was missing too. The real Windows 98 defragmenter had a dedicated button that opened a dialog explaining what each block color meant: optimized, reading, writing, bad cluster. Without it, the simulation was just a pretty light show. Now it has one.&lt;/p&gt;
&lt;p&gt;On the visual side, the cluster grid also had a black background, when the real defragmenter used white. A small detail, but exactly the kind of thing that breaks the spell for anyone who actually remembers the original.&lt;/p&gt;
&lt;p&gt;Then there was the progress bar. The real utility didn’t draw a smooth, continuous fill. It revealed a row of chunky segmented blocks one at a time, with a “% Complete” caption underneath. Mine was too modern, too clean. Now it fills block by block the way it should, and the window is sized to give it room to breathe.&lt;/p&gt;
&lt;p&gt;Even the controls were missing some manners. The original asked you to confirm before stopping a defrag, and it told you plainly when the job was &lt;em&gt;Paused&lt;/em&gt; or &lt;em&gt;Stopped&lt;/em&gt;. Small things, but they’re the difference between a screensaver and a tool you can actually operate.&lt;/p&gt;
&lt;h2 id=&quot;the-rebuild-nextjs-to-vite-and-a-faithful-simulation&quot;&gt;The rebuild: Next.js to Vite, and a faithful simulation&lt;/h2&gt;
&lt;p&gt;I moved the project from Next.js to Vite + React. This might sound like a lateral move, but it was the right call for what &lt;a href=&quot;https://defrag98.com&quot;&gt;defrag98.com&lt;/a&gt; actually is: a single-page, entirely client-side app with no routing, no server-side data, and no need for SSR. Next.js is excellent at what it does, but fighting its SSR model to build something that runs entirely in the browser felt like wearing a suit to go swimming.&lt;/p&gt;
&lt;p&gt;Vite gave back the dev experience that SSR had been quietly taxing. Faster builds, instant HMR, and a mental model that actually matches the app. While I was in there, the rest of the stack got a refresh too: Tailwind CSS v4 through its native Vite plugin (no more PostCSS pipeline), Base UI for the accessible dialogs, selects and checkboxes, Zustand for state, TypeScript 6, and the Rust-based oxc toolchain (oxlint and oxfmt) in place of ESLint and Prettier for near-instant linting and formatting.&lt;/p&gt;
&lt;p&gt;The bigger change was moving cluster rendering to a &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; element. Instead of creating and updating hundreds of DOM nodes, I’m now drawing directly to a 2D canvas context. The number of clusters rendered adapts to whichever disk size you select, so a larger virtual drive produces a denser, more satisfying grid. The difference is easy to feel: the animation is smoother, the browser is happier, and it scales without complaint. I also size the canvas before the first paint, which killed the layout shift that used to jump around on mobile.&lt;/p&gt;
&lt;p&gt;On the simulation side, I rewrote the defrag algorithm to properly model read and write operations on individual clusters, with non-sequential processing that mirrors how the original utility actually behaved. If you remember the way blocks would jump around seemingly at random before gradually settling into order, that’s what it does now.&lt;/p&gt;
&lt;p&gt;The sound got some attention too. The hard disk loop and the completion chime were re-encoded to a fraction of their old size (the HDD loop went from over a megabyte to around 350KB), shipped as Opus/WebM with an AAC fallback, and the Win98 fonts are preloaded now so nothing flashes on first paint. There are also toggles in Settings to mute the hard disk sound or the mouse clicks, and your choice is remembered the next time you visit.&lt;/p&gt;
&lt;p&gt;When a defrag finishes, you now get the authentic Windows 98 “Disk Defragmenter” message box telling you it’s done, chime and all, instead of the run just quietly ending.&lt;/p&gt;
&lt;p&gt;A couple of things changed off-screen that I care about as much as the visible ones. I dropped Google Analytics and Tag Manager in favor of Cloudflare’s cookieless Web Analytics, so there’s no tracking cookie and no consent banner getting between you and the nostalgia. The site is hosted on Cloudflare Workers as static assets, and it ships an &lt;code&gt;llms.txt&lt;/code&gt;, the &lt;a href=&quot;/blog/configuring-my-site-for-ai-discoverability&quot;&gt;same AI-discoverability setup I detailed for this blog&lt;/a&gt;, so the AI crawlers that come poking around get a clean description of what this thing is.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://defrag98.com&quot;&gt;defrag98.com&lt;/a&gt; also ships a web app manifest, so on supported browsers you can install it to your device and launch it like a native app. And it now runs fully offline: a service worker built with vite-plugin-pwa and Workbox precaches the app shell and every runtime asset, the fonts, cursors, audio, and icons, so once you’ve opened it once you can defragment a virtual drive on a plane with no signal. New builds activate quietly on the next launch, with no update prompt to nag you. Feels right for something built to feel like software from another era.&lt;/p&gt;
&lt;p&gt;There’s also a hidden Easter egg somewhere in the app. I’ll leave it at that. If you grew up on Windows 98, you’ll know it when you see it.&lt;/p&gt;
&lt;h2 id=&quot;a-quieter-ask&quot;&gt;A quieter ask&lt;/h2&gt;
&lt;p&gt;The original version had a donation banner styled as an authentic Windows ME system notification: yellow background, full viewport width, sliding down from the top of the screen five seconds into your session. It was accurate to the era. It was also an interruption, landing right in the middle of the nostalgic moment it was supposed to be protecting.&lt;/p&gt;
&lt;p&gt;The data confirmed it. Despite solid traffic, very few visitors converted to donors.&lt;/p&gt;
&lt;p&gt;The new approach takes a cue from pre-release Windows software, the small line of unobtrusive text in the corner of the screen that says something like &lt;em&gt;“This is a pre-release version”&lt;/em&gt;. I added a quiet line in the bottom-right of the desktop area, linking to my &lt;a href=&quot;https://www.buymeacoffee.com/morellodev&quot;&gt;Buy Me a Coffee page&lt;/a&gt;. It’s there if you want it. It doesn’t demand your attention. The only other time donating comes up is in that completion message box, once the defrag has actually finished and you’ve had your moment: a gentle Yes or No, never an interruption. The minimum donation has also dropped from $5 to $3, because the goal was never to extract money. It’s to help cover the domain and hosting costs that keep &lt;a href=&quot;https://defrag98.com&quot;&gt;defrag98.com&lt;/a&gt; running for anyone who gets something out of it.&lt;/p&gt;
&lt;h2 id=&quot;why-this-still-matters-to-me&quot;&gt;Why this still matters to me&lt;/h2&gt;
&lt;p&gt;I’ve received $148 in total donations since launching. That’s not a business. But it’s proof that people feel something when they use it, enough to voluntarily send a few dollars to a stranger on the internet for a free toy.&lt;/p&gt;
&lt;p&gt;That’s the part that keeps me iterating on this instead of moving on. It’s not a portfolio piece. It’s a tiny time machine, and apparently enough people want to visit 1998 for a few minutes that keeping the lights on feels worth caring about.&lt;/p&gt;
&lt;p&gt;If you haven’t tried it yet, &lt;a href=&quot;https://defrag98.com&quot;&gt;go defragment something&lt;/a&gt;. If you used to stare at this screen as a kid, I hope it hits the way I intended. And if you find the Easter egg, well, you’ll know what to do.&lt;/p&gt;
</content:encoded><category>react</category><category>vite</category><category>webdev</category><category>retro</category><author>Dennis Morello</author></item><item><title>Configuring My Site for AI Discoverability</title><link>https://morello.dev/blog/configuring-my-site-for-ai-discoverability</link><guid isPermaLink="true">https://morello.dev/blog/configuring-my-site-for-ai-discoverability</guid><description>How I set up this site for GEO. Raw Markdown, llms.txt, Content-Signal, and the Cloudflare bits that tie it all together.</description><pubDate>Mon, 20 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A growing share of web traffic doesn’t come from people anymore. It comes from models reading on their behalf. ChatGPT, Claude, Perplexity, Copilot. They fetch a handful of pages, summarize, and ship the answer back. If your site isn’t readable by those agents, you don’t exist to them.&lt;/p&gt;
&lt;p&gt;People are calling this &lt;a href=&quot;https://en.wikipedia.org/wiki/Generative_engine_optimization&quot;&gt;GEO&lt;/a&gt;, short for Generative Engine Optimization. It overlaps with SEO but the priorities are different. Agents don’t care about your layout. They care about your prose, your metadata, and how many tokens it costs them to read you.&lt;/p&gt;
&lt;p&gt;This post covers how I configured this site for GEO. The first half is framework-agnostic. The second half is specific to my setup on Cloudflare, and includes a deliberate choice that fails a popular GEO audit. I’ll explain why. (The site itself is a terminal-styled static &lt;a href=&quot;/blog/the-new-website&quot;&gt;Astro build&lt;/a&gt;, if you want the design and stack story first.)&lt;/p&gt;
&lt;h2 id=&quot;part-1-general-geo-techniques&quot;&gt;Part 1: general GEO techniques&lt;/h2&gt;
&lt;h3 id=&quot;serve-raw-markdown-alongside-html&quot;&gt;Serve raw Markdown alongside HTML&lt;/h3&gt;
&lt;p&gt;The single biggest GEO win is giving agents a version of each page without the navigation, styling, and scripts. HTML is designed for browsers. Markdown is designed for readers, human or otherwise. Agents spend their context window on your prose, not your DOM.&lt;/p&gt;
&lt;p&gt;Every blog post on this site has a mirror URL with a &lt;code&gt;.md&lt;/code&gt; suffix:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/blog/my-post&lt;/code&gt; is the full HTML page for humans&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/blog/my-post.md&lt;/code&gt; is the raw Markdown, served as &lt;code&gt;text/markdown&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In Astro, this is a two-line route at &lt;code&gt;src/pages/blog/[slug].md.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt; const&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; GET&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt; async&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; ({ params }) &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt; post&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; getPostById&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-constant)&quot;&gt;params&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;.slug);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;  return&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; Response&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;formatPostMarkdown&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;(post)&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line highlighted&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;    headers&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; { &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;&quot;Content-Type&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt; &quot;text/markdown; charset=utf-8&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; }&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;};&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Both variants are pre-generated at build time. Same content, &lt;strong&gt;a fraction of the payload&lt;/strong&gt; for an agent to consume: on the posts here, the Markdown mirror is under a fifth the size of the rendered HTML once you strip out the navigation, inline styles, scripts, and structured data.&lt;/p&gt;
&lt;h3 id=&quot;advertise-the-markdown-version-in-head&quot;&gt;Advertise the Markdown version in &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Agents landing on the HTML need to know the Markdown exists. A single &lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt; in the head does it:&lt;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;html&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;link&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; rel&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;&quot;alternate&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; type&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;&quot;text/markdown&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt; href&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;&quot;/blog/my-post.md&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; /&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Browsers ignore this tag. Agents that parse the head follow it.&lt;/p&gt;
&lt;h3 id=&quot;publish-an-llmstxt-index&quot;&gt;Publish an &lt;code&gt;llms.txt&lt;/code&gt; index&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://llmstxt.org/&quot;&gt;&lt;code&gt;llms.txt&lt;/code&gt;&lt;/a&gt; is a convention for a Markdown file at the root of your site listing your content with short descriptions and links. Think of it as a sitemap an LLM can actually read.&lt;/p&gt;
&lt;p&gt;I ship two variants:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/llms.txt&lt;/code&gt; is the index. Title, description, one line per post with a link to its &lt;code&gt;.md&lt;/code&gt; version.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/llms-full.txt&lt;/code&gt; is the full corpus. Every post body concatenated into a single response.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Why both? An agent researching a specific topic can fetch &lt;code&gt;llms.txt&lt;/code&gt;, pick the relevant links, and pull them. An agent doing deep research on the site as a whole fetches &lt;code&gt;llms-full.txt&lt;/code&gt; once and has everything it needs in one request. Either way there’s no crawling.&lt;/p&gt;
&lt;h3 id=&quot;declare-your-ai-stance-in-robotstxt&quot;&gt;Declare your AI stance in &lt;code&gt;robots.txt&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;robots.txt&lt;/code&gt; now carries a &lt;code&gt;Content-Signal&lt;/code&gt; directive for AI use. Mine reads:&lt;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;txt&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;User-agent: *&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line highlighted&quot;&gt;&lt;span&gt;Content-Signal: search=yes, ai-train=no, ai-input=yes&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Allow: /&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Sitemap: https://morello.dev/sitemap-index.xml&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Three independent knobs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;search=yes&lt;/code&gt; lets search engines index&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ai-train=no&lt;/code&gt; says my content is not for training data&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ai-input=yes&lt;/code&gt; says my content &lt;em&gt;can&lt;/em&gt; be retrieved and used as input for AI answers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is the stance I’m comfortable with. I want to show up when someone asks Claude about something I’ve written; I just don’t want my posts absorbed into the next base model.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Whether any given operator actually honors this is another question. The signal’s there regardless, and I’d rather be on record than silent about it.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;add-structured-data-that-actually-describes-the-content&quot;&gt;Add structured data that actually describes the content&lt;/h3&gt;
&lt;p&gt;Most blogs ship JSON-LD schema by reflex. Few of them include the fields that help a generative engine decide whether your article is worth fetching.&lt;/p&gt;
&lt;p&gt;On each post I emit a &lt;code&gt;BlogPosting&lt;/code&gt; graph with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;wordCount&lt;/code&gt; and &lt;code&gt;timeRequired&lt;/code&gt; (ISO 8601 duration), so an agent can estimate how much context it’ll spend before fetching&lt;/li&gt;
&lt;li&gt;&lt;code&gt;author&lt;/code&gt; linked to a &lt;code&gt;Person&lt;/code&gt; node with &lt;code&gt;knowsAbout&lt;/code&gt; so the entity is grounded in real topics&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BreadcrumbList&lt;/code&gt; for site hierarchy&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All of it goes into a single &lt;code&gt;@graph&lt;/code&gt; per page rather than scattered &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tags, which makes it cheaper for an engine to walk from post to author to site without cross-referencing.&lt;/p&gt;
&lt;h3 id=&quot;a-sitemap-that-actually-tracks-freshness&quot;&gt;A sitemap that actually tracks freshness&lt;/h3&gt;
&lt;p&gt;If you regenerate your sitemap once and never look at it again, you’re wasting a signal. Every URL in mine carries a &lt;code&gt;lastmod&lt;/code&gt; timestamp pulled from the post’s &lt;code&gt;updatedDate&lt;/code&gt; frontmatter, falling back to &lt;code&gt;pubDate&lt;/code&gt;. When I edit an old post, its &lt;code&gt;lastmod&lt;/code&gt; moves forward and crawlers reprioritize it.&lt;/p&gt;
&lt;h3 id=&quot;validate-with-real-tools&quot;&gt;Validate with real tools&lt;/h3&gt;
&lt;p&gt;Two tools I found useful while iterating on all of the above:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://isitagentready.com/&quot;&gt;isitagentready.com&lt;/a&gt; audits across five categories: discoverability, content accessibility, bot access control, protocol discovery, and commerce. The bot access control checks (&lt;code&gt;Content-Signal&lt;/code&gt;, Web Bot Auth, AI bot rules) are the part that actually influences how agents treat your content.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://acceptmarkdown.com/&quot;&gt;acceptmarkdown.com&lt;/a&gt; has a narrower focus. It checks whether your site responds to &lt;code&gt;Accept: text/markdown&lt;/code&gt; with a Markdown body, includes &lt;code&gt;Vary: Accept&lt;/code&gt;, returns &lt;code&gt;406&lt;/code&gt; for unsupported types, and parses q-values correctly.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I’ll come back to the second one at the end of the post, because my site deliberately fails it.&lt;/p&gt;
&lt;h2 id=&quot;part-2-the-cloudflare-specific-setup&quot;&gt;Part 2: the Cloudflare-specific setup&lt;/h2&gt;
&lt;p&gt;General GEO gets you most of the way there. The rest is delivery. How fast you respond, whether the edge caches correctly, and how you advertise your agent-facing resources without waiting for someone to parse your HTML.&lt;/p&gt;
&lt;h3 id=&quot;static-assets-zero-worker-invocations&quot;&gt;Static assets, zero Worker invocations&lt;/h3&gt;
&lt;p&gt;My &lt;code&gt;wrangler.jsonc&lt;/code&gt; points a &lt;code&gt;./dist&lt;/code&gt; directory at &lt;a href=&quot;https://developers.cloudflare.com/workers/static-assets/&quot;&gt;Cloudflare’s assets deployment&lt;/a&gt;, with no &lt;code&gt;main&lt;/code&gt; entry:&lt;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;jsonc&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;  &quot;name&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt; &quot;morellodev&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;  &quot;compatibility_date&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt; &quot;2026-04-18&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;  &quot;assets&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;    &quot;directory&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt; &quot;./dist&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;    &quot;html_handling&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt; &quot;drop-trailing-slash&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;    &quot;not_found_handling&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt; &quot;404-page&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;  }&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every request goes straight from the edge asset cache. HTML, Markdown, &lt;code&gt;llms.txt&lt;/code&gt;, sitemap, RSS. Same path for all of them, and no Worker ever runs. On the Workers Free tier this matters. A crawler sweep that would otherwise eat into 100k daily invocations now costs me nothing. Agents, for better or worse, don’t fingerprint politely.&lt;/p&gt;
&lt;h3 id=&quot;advertise-discovery-endpoints-in-a-link-header&quot;&gt;Advertise discovery endpoints in a &lt;code&gt;Link&lt;/code&gt; header&lt;/h3&gt;
&lt;p&gt;Cloudflare’s &lt;a href=&quot;https://developers.cloudflare.com/workers/static-assets/headers/&quot;&gt;&lt;code&gt;_headers&lt;/code&gt; file&lt;/a&gt; lets you ship response headers without any server code. I use it to tell every response, not just HTML ones, where the agent-facing files live:&lt;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;txt&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;/*&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  Link: &amp;lt;/sitemap-index.xml&amp;gt;; rel=&quot;sitemap&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        &amp;lt;/rss.xml&amp;gt;; rel=&quot;alternate&quot;; type=&quot;application/rss+xml&quot;; title=&quot;RSS&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        &amp;lt;/llms.txt&amp;gt;; rel=&quot;describedby&quot;; type=&quot;text/plain&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        &amp;lt;/llms-full.txt&amp;gt;; rel=&quot;describedby&quot;; type=&quot;text/plain&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A crawler doing a &lt;code&gt;HEAD&lt;/code&gt; against any URL on the site sees all four links before it parses a single byte of HTML. &lt;strong&gt;One round-trip, no body, full discovery.&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&quot;long-lived-cache-for-hashed-assets&quot;&gt;Long-lived cache for hashed assets&lt;/h3&gt;
&lt;p&gt;Astro emits fingerprinted filenames under &lt;code&gt;/_astro/&lt;/code&gt;, so those can sit in cache for a year:&lt;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;txt&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;/_astro/*&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  Cache-Control: public, max-age=31536000, immutable&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Faster first paint for humans, cheaper crawls for agents. Same lever.&lt;/p&gt;
&lt;h3 id=&quot;why-i-skipped-accept-textmarkdown-content-negotiation&quot;&gt;Why I skipped &lt;code&gt;Accept: text/markdown&lt;/code&gt; content negotiation&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://acceptmarkdown.com/&quot;&gt;acceptmarkdown.com&lt;/a&gt; will tell you this site doesn’t do content negotiation. No &lt;code&gt;Vary: Accept&lt;/code&gt;, no &lt;code&gt;406&lt;/code&gt;, no Markdown from the canonical URL. That’s not an oversight. I tried it, shipped it briefly, and rolled it back. (If you enjoy this corner of HTTP, &lt;a href=&quot;/blog/the-new-http-query-method&quot;&gt;the new QUERY method&lt;/a&gt; is another case of the protocol finally growing the semantics we’d been faking.)&lt;/p&gt;
&lt;p&gt;The reason is Cloudflare’s free plan. Custom cache keys are Enterprise-only, and &lt;a href=&quot;https://developers.cloudflare.com/cache/concepts/cache-control/&quot;&gt;their docs are clear&lt;/a&gt; that Cloudflare ignores &lt;code&gt;Vary&lt;/code&gt; for caching decisions by default (&lt;code&gt;Accept&lt;/code&gt; included). The edge collapses every variant of &lt;code&gt;/blog/my-post&lt;/code&gt; into one cache entry, so the first requester’s format &lt;strong&gt;poisons the cache for everyone else&lt;/strong&gt; until TTL expires.&lt;/p&gt;
&lt;p&gt;The workaround is a Worker that bypasses the edge cache. But now every &lt;code&gt;/blog/*&lt;/code&gt; request burns a Worker invocation, humans included, and the &lt;a href=&quot;https://developers.cloudflare.com/workers/platform/pricing/&quot;&gt;Workers Free plan&lt;/a&gt; gives you 100k per day and 10ms of CPU each. That’s a real budget to share across humans and bots, for no functional gain over a static &lt;code&gt;.md&lt;/code&gt; URL.&lt;/p&gt;
&lt;p&gt;So I deleted the Worker. The only thing I lost is &lt;code&gt;curl -H &quot;Accept: text/markdown&quot; …/blog/my-post&lt;/code&gt; returning Markdown. Between &lt;code&gt;llms.txt&lt;/code&gt;, &lt;code&gt;&amp;lt;link rel=&quot;alternate&quot;&amp;gt;&lt;/code&gt;, and the &lt;code&gt;/blog/[slug].md&lt;/code&gt; convention, no mainstream agent I’ve seen actually needs &lt;code&gt;Accept:&lt;/code&gt; negotiation. It’s the more elegant protocol; alternate URLs are the more robust one on a free-tier CDN. On a paid plan I’d probably do both.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update, July 2026:&lt;/strong&gt; Cloudflare has since &lt;a href=&quot;https://developers.cloudflare.com/changelog/post/2026-07-02-vary-for-cache-rules/&quot;&gt;added &lt;code&gt;Vary&lt;/code&gt; support to Cache Rules on every plan&lt;/a&gt;, free tier included, so the caching half of this problem is solved: the edge can now hold a separate variant per &lt;code&gt;Accept&lt;/code&gt; value without an Enterprise-only custom cache key. The remaining catch for me is that this site is assets-only, with no Worker or origin in the request path to actually generate Markdown for the canonical URL, so serving it would still mean putting compute back in front of every request. The static &lt;code&gt;.md&lt;/code&gt; mirror keeps that out of the path entirely, so I’m leaving the setup as is. But the specific free-plan blocker I leaned on above is gone, and I’d weigh this differently starting a site today.&lt;/p&gt;
&lt;h2 id=&quot;where-this-leaves-things&quot;&gt;Where this leaves things&lt;/h2&gt;
&lt;p&gt;Every page exists in two forms, both served from the edge. Agent-facing resources are advertised in response headers on every request, before any HTML gets parsed. Structured data tells engines what the article is and how much context it takes to read. &lt;code&gt;robots.txt&lt;/code&gt; says what I’ll allow and what I won’t.&lt;/p&gt;
&lt;p&gt;GEO is still very new. The standards are half-drafted, the tools disagree with each other, and half the signals I described above didn’t exist two years ago. I fully expect to be rewriting parts of this post within six months, probably with a different opinion about Accept-based negotiation, once I’ve either moved off the free plan or found a workaround that doesn’t involve a Worker. But for now: serve agents a version they can cheaply consume, be explicit about what you’ll allow, and accept that the defaults aren’t on your side.&lt;/p&gt;
&lt;p&gt;Everything here is about making the site &lt;em&gt;readable&lt;/em&gt;. The next step is making it &lt;em&gt;usable&lt;/em&gt;, letting an agent act on the page instead of just summarizing it, which is what I dug into in &lt;a href=&quot;/blog/webmcp-making-your-site-usable-by-ai-agents&quot;&gt;making your site usable by AI agents with WebMCP&lt;/a&gt;. The layer above that, teaching an agent when to use those tools, is what &lt;a href=&quot;/blog/webskill-agent-skills-in-the-browser&quot;&gt;WebSkill&lt;/a&gt; proposes. And for the heavy models some agents now run directly in the browser, the platform is starting on the storage side too, so a multi-gigabyte file isn’t re-downloaded once per site. That’s what &lt;a href=&quot;/blog/cross-origin-storage-api&quot;&gt;Cross-Origin Storage&lt;/a&gt; is for.&lt;/p&gt;
&lt;p&gt;If you’re reading this via a summary from some assistant, hi. Thanks for the traffic.&lt;/p&gt;
</content:encoded><category>ai</category><category>seo</category><category>cloudflare</category><category>webdev</category><author>Dennis Morello</author></item><item><title>Git Worktrees Are Underrated</title><link>https://morello.dev/blog/git-worktrees-are-underrated</link><guid isPermaLink="true">https://morello.dev/blog/git-worktrees-are-underrated</guid><description>Git worktrees let you work on multiple branches at once without stashing or losing context. Here&apos;s how they changed my workflow and how I manage them.</description><pubDate>Mon, 16 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;If you’ve ever stashed half-finished work to review a PR, or lost a train of thought switching branches, you should know about git worktrees. They’ve been in git since 2015, but most developers I talk to have never heard of them.&lt;/p&gt;
&lt;h2 id=&quot;the-problem-with-branches&quot;&gt;The problem with branches&lt;/h2&gt;
&lt;p&gt;The standard git workflow is linear. You’re on a branch, you need to context-switch, so you stash your changes (or worse, make a throwaway commit), switch branches, do your thing, switch back, and pop the stash. If you’re lucky, nothing conflicts.&lt;/p&gt;
&lt;p&gt;This gets worse when you’re juggling multiple things at once. Reviewing a colleague’s PR while your feature branch is mid-refactor. Running tests on one branch while coding on another. Hotfixing production while your main work sits frozen.&lt;/p&gt;
&lt;p&gt;A git repository has one working directory. One branch checked out at a time. Everything else has to wait.&lt;/p&gt;
&lt;h2 id=&quot;worktrees&quot;&gt;Worktrees&lt;/h2&gt;
&lt;p&gt;Git worktrees let you check out multiple branches simultaneously, each in its own directory. They share the same &lt;code&gt;.git&lt;/code&gt; history and objects, so you’re not cloning the entire repo, but each one has its own working directory and index.&lt;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;sh&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string)&quot;&gt; worktree&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string)&quot;&gt; add&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string)&quot;&gt; ../feat-login&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string)&quot;&gt; feat/login&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now &lt;code&gt;feat/login&lt;/code&gt; is checked out in &lt;code&gt;../feat-login&lt;/code&gt;. You can open it in a separate editor window, run tests there, or just let it sit while you work on something else.&lt;/p&gt;
&lt;p&gt;When you’re done:&lt;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;sh&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string)&quot;&gt; worktree&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string)&quot;&gt; remove&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string)&quot;&gt; ../feat-login&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;why-nobody-uses-them&quot;&gt;Why nobody uses them&lt;/h2&gt;
&lt;p&gt;If worktrees are so useful, why aren’t they more popular? I think it comes down to two things.&lt;/p&gt;
&lt;p&gt;First, the directory problem. Every time you create a worktree, you have to pick a path. There’s no convention for where they go. After a week you end up with directories scattered across your filesystem: &lt;code&gt;../feat-login&lt;/code&gt;, &lt;code&gt;~/tmp/hotfix-auth&lt;/code&gt;, &lt;code&gt;../../pr-review&lt;/code&gt;. Good luck remembering where anything is.&lt;/p&gt;
&lt;p&gt;Second, there are no ergonomics. You can’t easily list your worktrees with status info, cd into one quickly, or run something like &lt;code&gt;npm install&lt;/code&gt; in the new directory after checkout. The raw git commands work, but they don’t make worktrees feel like a real workflow.&lt;/p&gt;
&lt;p&gt;These aren’t fundamental problems with worktrees. They’re tooling gaps. That’s why I built &lt;a href=&quot;https://github.com/morellodev/arbor&quot;&gt;arbor&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;a-better-workflow-with-arbor&quot;&gt;A better workflow with arbor&lt;/h2&gt;
&lt;p&gt;Arbor is a CLI that manages git worktrees. It keeps them organized in a central directory (&lt;code&gt;~/.arbor/worktrees/&lt;/code&gt;) and handles the stuff that plain git doesn’t.&lt;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;sh&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-comment)&quot;&gt;# Clone a repo (sets up a bare repo + default branch worktree)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;arbor&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string)&quot;&gt; clone&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string)&quot;&gt; user/my-app&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-comment)&quot;&gt;# Create a worktree for a new feature&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;arbor&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string)&quot;&gt; add&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string)&quot;&gt; feat/login&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-comment)&quot;&gt;# Shell integration auto-cds you into the worktree&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-comment)&quot;&gt;# You&apos;re now in ~/.arbor/worktrees/my-app/feat-login&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-comment)&quot;&gt;# Need to review a PR? Open another worktree&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;arbor&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string)&quot;&gt; add&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string)&quot;&gt; fix/auth-bug&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-comment)&quot;&gt;# See all your worktrees with dirty/clean status&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;arbor&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string)&quot;&gt; ls&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every worktree for &lt;code&gt;my-app&lt;/code&gt; lives under &lt;code&gt;~/.arbor/worktrees/my-app/&lt;/code&gt;. Branch slashes become dashes in directory names, so the filesystem stays tidy. When you’re done with a branch:&lt;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;sh&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;arbor&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string)&quot;&gt; rm&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string)&quot;&gt; -d&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string)&quot;&gt; feat/login&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That removes the worktree and the local branch in one step.&lt;/p&gt;
&lt;h2 id=&quot;what-makes-it-stick&quot;&gt;What makes it stick&lt;/h2&gt;
&lt;p&gt;I’ve been using worktrees for a while, but I kept falling off because of the friction. A few things in arbor made the difference for me.&lt;/p&gt;
&lt;p&gt;Shell integration. &lt;code&gt;arbor init&lt;/code&gt; sets up a wrapper so that &lt;code&gt;arbor add&lt;/code&gt; and &lt;code&gt;arbor switch&lt;/code&gt; automatically cd you into the worktree directory. Without it you create the worktree and then have to &lt;code&gt;cd&lt;/code&gt; into it yourself every single time, which gets old fast.&lt;/p&gt;
&lt;p&gt;Post-create hooks. You can add an &lt;code&gt;.arbor.toml&lt;/code&gt; to your repo:&lt;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;toml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;[hooks]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;post_create &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt; &quot;npm install&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every new worktree gets its dependencies installed automatically. You can chain commands too:&lt;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;toml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;[hooks]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;post_create &lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-keyword)&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt;&quot;npm install&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-punctuation)&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string-expression)&quot;&gt; &quot;cp .env.example .env&quot;&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-foreground)&quot;&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And &lt;code&gt;arbor ls --all&lt;/code&gt; shows the state of every worktree across all your repos, which is nice for a quick scan before you call it a day.&lt;/p&gt;
&lt;h2 id=&quot;where-i-use-them-most&quot;&gt;Where I use them most&lt;/h2&gt;
&lt;p&gt;PR reviews. &lt;code&gt;arbor add&lt;/code&gt; the branch, review it in a separate editor window, remove it when done. My own work is untouched.&lt;/p&gt;
&lt;p&gt;Long test suites. Tests running on one branch, coding on another. No waiting around.&lt;/p&gt;
&lt;p&gt;Hotfixes. &lt;code&gt;arbor add hotfix/critical&lt;/code&gt;, fix it, push it, remove it. Back to what I was doing.&lt;/p&gt;
&lt;p&gt;Comparing behavior. Two worktrees open side by side, one on &lt;code&gt;main&lt;/code&gt; and one on my feature branch. Way easier than switching back and forth.&lt;/p&gt;
&lt;p&gt;Big migrations. Upgrading a framework to a new major version in a worktree while the rest of the team keeps shipping from &lt;code&gt;main&lt;/code&gt;, the way I kicked the tires on &lt;a href=&quot;/blog/solidjs-2-react-developers-first-look&quot;&gt;SolidJS 2.0&lt;/a&gt; and dug into &lt;a href=&quot;/blog/solidjs-2-async-data&quot;&gt;its async data model&lt;/a&gt;. No half-broken build blocking anyone.&lt;/p&gt;
&lt;p&gt;Parallel AI agents. Running several coding agents at once, each isolated in its own worktree so they can’t clobber each other’s changes, which is exactly how &lt;a href=&quot;/blog/bun-14-rust-rewrite&quot;&gt;Bun’s Rust rewrite&lt;/a&gt; ran 64 Claude agents across four worktrees.&lt;/p&gt;
&lt;h2 id=&quot;git-worktree-best-practices&quot;&gt;Git worktree best practices&lt;/h2&gt;
&lt;p&gt;A few habits keep worktrees from turning into the mess that makes people give up on them, whether you’re on arbor or raw git.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep them in one predictable place.&lt;/strong&gt; Pick a home for your worktrees and stick to it: a &lt;code&gt;~/worktrees/&lt;/code&gt; directory, or let arbor manage &lt;code&gt;~/.arbor/worktrees/&lt;/code&gt; for you. Scattering them across &lt;code&gt;../&lt;/code&gt;, &lt;code&gt;~/tmp&lt;/code&gt;, and &lt;code&gt;../../&lt;/code&gt; is how you lose track of what lives where.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;One branch per worktree.&lt;/strong&gt; Git won’t let you check out the same branch in two worktrees without &lt;code&gt;--force&lt;/code&gt;, and that guardrail is worth respecting. Keep the mapping one branch to one directory.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Create the branch and the worktree together.&lt;/strong&gt; &lt;code&gt;git worktree add -b feat/login ../feat-login&lt;/code&gt; makes the branch and checks it out in the new directory in a single command, instead of creating the branch first and adding it after.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Remove worktrees with git, not &lt;code&gt;rm -rf&lt;/code&gt;.&lt;/strong&gt; Run &lt;code&gt;git worktree remove ../feat-login&lt;/code&gt; instead of deleting the directory by hand. A manual delete leaves stale metadata behind in &lt;code&gt;.git/worktrees&lt;/code&gt;, which you then have to clear out with &lt;code&gt;git worktree prune&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Run &lt;code&gt;git worktree list&lt;/code&gt; when you lose the thread.&lt;/strong&gt; It prints every worktree with its branch and commit, so you can see what’s checked out where before you go hunting through directories.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Don’t nest them.&lt;/strong&gt; Give each worktree its own top-level directory rather than tucking one inside another worktree or the main repo. Nesting confuses tooling, and worktrees on a repo with submodules are only partly supported anyway.&lt;/p&gt;
&lt;h2 id=&quot;try-it&quot;&gt;Try it&lt;/h2&gt;
&lt;p&gt;Arbor is open source, written in Rust. Install with Homebrew:&lt;/p&gt;
&lt;pre class=&quot;astro-code css-variables&quot; style=&quot;background-color:var(--shiki-background);color:var(--shiki-foreground); overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;sh&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:var(--shiki-token-function)&quot;&gt;brew&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string)&quot;&gt; install&lt;/span&gt;&lt;span style=&quot;color:var(--shiki-token-string)&quot;&gt; morellodev/tap/arbor&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or grab a binary from the &lt;a href=&quot;https://github.com/morellodev/arbor/releases/latest&quot;&gt;releases page&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Run &lt;code&gt;arbor init&lt;/code&gt; after installing to set up shell integration. Takes about 30 seconds.&lt;/p&gt;
&lt;p&gt;If you’ve never tried worktrees, give them a shot. And if you have but gave up because of the directory mess, this might be worth another look.&lt;/p&gt;
&lt;p&gt;The repo is at &lt;a href=&quot;https://github.com/morellodev/arbor&quot;&gt;github.com/morellodev/arbor&lt;/a&gt;.&lt;/p&gt;
</content:encoded><category>git</category><category>cli</category><category>productivity</category><category>opensource</category><author>Dennis Morello</author></item><item><title>The New Website</title><link>https://morello.dev/blog/the-new-website</link><guid isPermaLink="true">https://morello.dev/blog/the-new-website</guid><description>How I rebuilt my personal site with Astro and Tailwind CSS v4: a terminal-inspired design with a user-selectable 16-theme switcher.</description><pubDate>Mon, 15 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I’ve rebuilt my personal website from the ground up with &lt;a href=&quot;https://astro.build&quot;&gt;Astro&lt;/a&gt; and &lt;a href=&quot;https://tailwindcss.com&quot;&gt;Tailwind CSS v4&lt;/a&gt;. The design is terminal-inspired: monospace type, tight 4px corners, and a color scheme you pick yourself.&lt;/p&gt;
&lt;h2 id=&quot;why-astro&quot;&gt;Why Astro?&lt;/h2&gt;
&lt;p&gt;After years of React-based frameworks, I wanted something simpler for a content-focused site. Astro fit: it ships zero JavaScript by default, renders to static HTML, and treats Markdown as a first-class content source through its Content Layer.&lt;/p&gt;
&lt;p&gt;The pages are plain static files served from a CDN. There’s very little to load and not much that can go wrong, so navigation feels close to instant.&lt;/p&gt;
&lt;h2 id=&quot;the-design&quot;&gt;The design&lt;/h2&gt;
&lt;p&gt;The type is monospace-first. &lt;a href=&quot;https://www.jetbrains.com/lp/mono/&quot;&gt;JetBrains Mono&lt;/a&gt; handles the UI, headings, and code; &lt;a href=&quot;https://fonts.google.com/specimen/Source+Serif+4&quot;&gt;Source Serif 4&lt;/a&gt; takes over for the body of long-form posts, where a serif is easier to read at length. The pairing keeps the developer-tool feel without making articles a chore to read. The article styling is hand-written CSS wired to theme variables rather than a plugin, the same own-the-CSS instinct behind &lt;a href=&quot;/blog/shadcn-typeset-vs-tailwind-prose&quot;&gt;shadcn/typeset&lt;/a&gt;. I’ve since &lt;a href=&quot;/blog/replacing-tailwind-with-vanilla-css&quot;&gt;pulled Tailwind out entirely and rewritten the site in vanilla CSS&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The part I had the most fun with is theming. Instead of following your system’s light or dark setting, the site lets you choose from 16 developer color schemes (&lt;a href=&quot;https://catppuccin.com/&quot;&gt;Catppuccin&lt;/a&gt;, Tokyo Night, Dracula, Nord, Gruvbox, and more) from a command-palette-style switcher. Press &lt;code&gt;t&lt;/code&gt; to open it, arrow through the palettes to preview them live, and hit Enter to commit. Your choice is saved in &lt;code&gt;localStorage&lt;/code&gt; and reapplied before first paint, so there’s no flash and the site remembers it next time. It defaults to Catppuccin Mocha.&lt;/p&gt;
&lt;h2 id=&quot;technical-highlights&quot;&gt;Technical highlights&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Tailwind CSS v4&lt;/strong&gt;: CSS-first configuration with &lt;code&gt;@theme&lt;/code&gt; blocks instead of a &lt;code&gt;tailwind.config.js&lt;/code&gt;. Each palette is a set of CSS variables swapped by a &lt;code&gt;data-theme&lt;/code&gt; attribute.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OG image generation&lt;/strong&gt;: social cards generated at build time with &lt;a href=&quot;https://github.com/vercel/satori&quot;&gt;Satori&lt;/a&gt;, which I broke down in &lt;a href=&quot;/blog/generating-og-images-with-satori-and-astro&quot;&gt;how the OG images are generated&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Syntax highlighting&lt;/strong&gt;: code blocks rendered with &lt;a href=&quot;https://shiki.style/&quot;&gt;Shiki&lt;/a&gt;, wired to CSS variables so the token colors track whichever theme you’ve selected.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SEO plumbing&lt;/strong&gt;: a generated sitemap via &lt;code&gt;@astrojs/sitemap&lt;/code&gt;, an RSS feed, JSON-LD structured data, and raw Markdown mirrors of every post. I later wrote up the full &lt;a href=&quot;/blog/configuring-my-site-for-ai-discoverability&quot;&gt;AI-discoverability setup&lt;/a&gt; in its own post, covering &lt;code&gt;llms.txt&lt;/code&gt;, &lt;code&gt;Content-Signal&lt;/code&gt;, and the Cloudflare bits.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;whats-next&quot;&gt;What’s next&lt;/h2&gt;
&lt;p&gt;I’ll be using this blog for notes on front-end development, open source, and the odd deep dive into web performance. Thanks for stopping by. The social links in the footer are the best way to reach me.&lt;/p&gt;
</content:encoded><category>astro</category><category>tailwindcss</category><category>webdev</category><category>css</category><author>Dennis Morello</author></item><item><title>Windows 98 Disk Defrag Simulator</title><link>https://morello.dev/blog/windows-98-defrag-simulator</link><guid isPermaLink="true">https://morello.dev/blog/windows-98-defrag-simulator</guid><description>Relive the 90s with this nostalgic Windows 98 Disk Defrag Simulator. Built with React and Next.js, featured on The Verge and Hacker News.</description><pubDate>Fri, 12 Jul 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://defrag98.com&quot;&gt;Defrag98&lt;/a&gt; is a browser recreation of the Windows 98 Disk Defragmenter, the utility that somehow turned a boring maintenance chore into compelling viewing. Colored blocks shuffling around a grid, a progress bar crawling toward 100%, and the vague sense that your PC was busy doing something important. This is that, running in a browser tab.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note (2026):&lt;/strong&gt; I’ve since rebuilt Defrag98 from scratch for a more faithful simulation. If the technical side interests you, read &lt;a href=&quot;/blog/defrag98-rebuild&quot;&gt;Rebuilding Defrag98: Getting the Details Right&lt;/a&gt;. What follows is the original 2024 announcement.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I built the first version with &lt;strong&gt;React&lt;/strong&gt;, &lt;strong&gt;Tailwind CSS&lt;/strong&gt;, and &lt;strong&gt;Next.js&lt;/strong&gt;, the same Tailwind and component approach I later used when &lt;a href=&quot;/blog/the-new-website&quot;&gt;rebuilding this site&lt;/a&gt;. The part I actually cared about was the motion: the real defragmenter didn’t march through clusters in order, so I wrote a custom algorithm that picks clusters to process in a way that feels closer to the original: a little chaotic, gradually settling into order.&lt;/p&gt;
&lt;p&gt;It found a far bigger audience than I expected. It reached the front page of &lt;a href=&quot;https://news.ycombinator.com/item?id=40962195&quot;&gt;Hacker News&lt;/a&gt;, got a write-up in &lt;a href=&quot;https://www.theverge.com/2024/7/14/24198206/take-a-moment-to-reflect&quot;&gt;The Verge&lt;/a&gt;, and went &lt;a href=&quot;https://www.producthunt.com/posts/windows-98-disk-defrag-simulator&quot;&gt;live on Product Hunt&lt;/a&gt;. Thanks to everyone who tried it out and shared it around, and if you’ve got a minute, an upvote on Product Hunt never hurts.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.producthunt.com/posts/windows-98-disk-defrag-simulator?embed=true&amp;utm_source=badge-featured&amp;utm_medium=badge&amp;utm_souce=badge-windows&amp;#0045;98&amp;#0045;disk&amp;#0045;defrag&amp;#0045;simulator&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=472182&amp;theme=light&quot; alt=&quot;Windows&amp;#0032;98&amp;#0032;Disk&amp;#0032;Defrag&amp;#0032;Simulator - Relive&amp;#0032;the&amp;#0032;90s&amp;#0058;&amp;#0032;Windows&amp;#0032;98&amp;#0032;Defrag&amp;#0032;Simulator&amp;#0032;Nostalgia | Product Hunt&quot; style=&quot;width: 250px; height: 54px;&quot; width=&quot;250&quot; height=&quot;54&quot; class=&quot;not-prose&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;what-the-windows-98-disk-defrag-simulator-does&quot;&gt;What the Windows 98 Disk Defrag Simulator does&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Windows 98 UI&lt;/strong&gt;: the blue-and-gray interface, recreated down to the window chrome.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multiple drive options&lt;/strong&gt;: pick from virtual drives of different sizes and speeds, each defragmenting at its own pace.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hard disk sounds&lt;/strong&gt;: the clicks and whirs of a late-90s drive, if you want them on.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pause and resume&lt;/strong&gt;: you control when it runs, just like the original.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Custom defrag algorithm&lt;/strong&gt;: the cluster shuffle is simulated, not scripted, so no two runs look quite the same.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The frontend is React on Next.js, styled with Tailwind CSS. The interesting bit is that defrag algorithm: instead of animating a fixed sequence, it randomizes which clusters get processed and when, which is what keeps a run from looking canned. The settings screen lets you trade drive size against speed, so you can set up anything from a crawling old disk to a quicker late-90s one.&lt;/p&gt;
&lt;h2 id=&quot;why-i-built-it&quot;&gt;Why I built it&lt;/h2&gt;
&lt;p&gt;I grew up watching this screen. For anyone who did, it’s a quick hit of nostalgia. For anyone who didn’t, it’s a small window into what keeping a PC running felt like in the 90s. No real disks are harmed and nothing actually gets optimized, and that’s the whole point.&lt;/p&gt;
&lt;h2 id=&quot;keeping-it-online&quot;&gt;Keeping it online&lt;/h2&gt;
&lt;p&gt;Keeping this online isn’t free: there’s a domain and hosting to cover. If you’ve enjoyed reliving this classic Windows experience, a small donation helps keep it running for everyone else. Every bit is appreciated.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.buymeacoffee.com/morellodev&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png&quot; alt=&quot;Buy Me A Coffee&quot; style=&quot;width: 217px; height: 60px;&quot; width=&quot;217&quot; height=&quot;60&quot; class=&quot;not-prose&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://defrag98.com&quot;&gt;Defrag98&lt;/a&gt; is free. If it makes you smile, share it. I’d love to see your reactions with &lt;strong&gt;#Defrag98&lt;/strong&gt;.&lt;/p&gt;
</content:encoded><category>react</category><category>nextjs</category><category>retro</category><category>webdev</category><author>Dennis Morello</author></item><item><title>5 Things You Might Not Know About JavaScript</title><link>https://morello.dev/blog/five-things-you-might-not-know-about-javascript</link><guid isPermaLink="true">https://morello.dev/blog/five-things-you-might-not-know-about-javascript</guid><description>Oracle is fighting to keep the JavaScript trademark. ECMAScript exists because Sun refused to license the name. typeof null is a 30-year-old bug.</description><pubDate>Fri, 26 May 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I write JavaScript every day and have for years. Most of us do. The language is so familiar that it’s easy to stop asking questions about it. But some of the details hiding underneath are genuinely strange: Oracle owns the JavaScript trademark and is fighting to keep it, the spec is called ECMAScript because Sun refused to license the name in 1997, and &lt;code&gt;typeof null&lt;/code&gt; returns &lt;code&gt;&apos;object&apos;&lt;/code&gt; – a 30-year-old bug nobody can fix. Here are five things about the language that catch even experienced developers off guard.&lt;/p&gt;
&lt;h2 id=&quot;1---javascript-is-a-trademark-oracle-owns-and-might-lose&quot;&gt;1 - “JavaScript” is a trademark Oracle owns (and might lose)&lt;/h2&gt;
&lt;p&gt;The name “JavaScript” is a &lt;a href=&quot;https://tsdr.uspto.gov/#caseNumber=75026640&quot;&gt;registered trademark&lt;/a&gt; held by Oracle America, Inc. Sun Microsystems applied for it in 1995, the same year Netscape created the language, and it was registered in 2000. Oracle inherited it when it acquired Sun in a deal announced in 2009 and closed in January 2010. The registration number is 2416017.&lt;/p&gt;
&lt;p&gt;That name is being openly contested. In September 2022, Node.js and Deno creator Ryan Dahl published &lt;a href=&quot;https://tinyclouds.org/trademark&quot;&gt;an open letter&lt;/a&gt; asking Oracle to release the trademark to the public domain. The effort grew into &lt;a href=&quot;https://javascript.tm&quot;&gt;a petition at javascript.tm&lt;/a&gt; that gathered over 19,550 signatures, including JavaScript creator Brendan Eich and many TC39 members.&lt;/p&gt;
&lt;p&gt;Then it escalated. On November 22, 2024, Deno Land filed a formal petition with the USPTO’s Trademark Trial and Appeal Board to cancel the trademark (case #92086835). Three claims: genericness, abandonment, and fraud. The fraud claim was the spicy one. During Oracle’s 2019 trademark renewal, it submitted a screenshot of Node.js as evidence of “use in commerce.” Node.js is Ryan Dahl’s project, not Oracle’s. Oracle’s second specimen was a page about Oracle JET, a UI toolkit. On June 18, 2026, the TTAB dismissed the fraud claim. The case continues on genericness and abandonment.&lt;/p&gt;
&lt;p&gt;Oracle must respond to the petition by August 7, 2026. Discovery begins September 6, 2026. For now, Oracle still owns the name.&lt;/p&gt;
&lt;p&gt;The language itself is a separate matter. It’s an open standard, ECMA-262, implemented by Google (V8), Apple (JavaScriptCore), Mozilla (SpiderMonkey), and others. Anyone can implement it. The trademark only controls commercial use of the name.&lt;/p&gt;
&lt;h2 id=&quot;2---why-the-spec-is-called-ecmascript-instead-of-javascript&quot;&gt;2 - Why the spec is called ECMAScript instead of JavaScript&lt;/h2&gt;
&lt;p&gt;People use the two names interchangeably, but they’re not the same thing. JavaScript is the language. ECMAScript is the standardized specification, ECMA-262, that defines its syntax and semantics. ECMAScript is the contract. JavaScript is one implementation of it.&lt;/p&gt;
&lt;p&gt;The spec is maintained by Ecma International through Technical Committee 39, and a new edition ships every year. Different editions correspond to different capabilities. ES5 (2009) brought strict mode and native JSON. ES6, later renamed ES2015, added arrow functions, classes, &lt;code&gt;let&lt;/code&gt;/&lt;code&gt;const&lt;/code&gt;, and modules. The yearly releases since have layered on async/await, optional chaining, and plenty more.&lt;/p&gt;
&lt;p&gt;Here’s the part most people miss. The standard was called “ECMAScript” in the first place because Sun owned the JavaScript trademark and wouldn’t transfer it to the standards body in 1997. The name was a compromise – Brendan Eich &lt;a href=&quot;https://web.archive.org/web/20210224153118/https://mail.mozilla.org/pipermail/es-discuss/2006-October/000133.html&quot;&gt;called it&lt;/a&gt; “an unwanted trade name that sounds like a skin disease.”&lt;/p&gt;
&lt;h2 id=&quot;3---why-typeof-null-returns-object-a-30-year-old-bug&quot;&gt;3 - Why &lt;code&gt;typeof null&lt;/code&gt; returns ‘object’ (a 30-year-old bug)&lt;/h2&gt;
&lt;p&gt;JavaScript is dynamically typed. You don’t annotate types up front, and a variable can hold a value of any type. It’s also loosely typed, meaning implicit coercion happens whether you want it to or not. The &lt;code&gt;+&lt;/code&gt; operator adds numbers, concatenates strings, and will coerce values across types to make an operation work.&lt;/p&gt;
&lt;p&gt;But none of that means types don’t exist. Every value has a type, and the &lt;code&gt;typeof&lt;/code&gt; operator returns a string describing it: &lt;code&gt;&apos;number&apos;&lt;/code&gt;, &lt;code&gt;&apos;string&apos;&lt;/code&gt;, &lt;code&gt;&apos;boolean&apos;&lt;/code&gt;, &lt;code&gt;&apos;undefined&apos;&lt;/code&gt;, &lt;code&gt;&apos;object&apos;&lt;/code&gt;, &lt;code&gt;&apos;function&apos;&lt;/code&gt;, &lt;code&gt;&apos;symbol&apos;&lt;/code&gt;, &lt;code&gt;&apos;bigint&apos;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;And then there’s the one that lies. &lt;code&gt;typeof null === &apos;object&apos;&lt;/code&gt;. This is a known bug from the first implementation in 1995, and it can’t be fixed without breaking the web. Here’s why: JavaScript’s original engine stored type information in the lower bits of every value. Objects used the type tag &lt;code&gt;000&lt;/code&gt;. &lt;code&gt;null&lt;/code&gt; was represented as the null pointer, &lt;code&gt;0x00&lt;/code&gt; – all zeros. The type-checking code saw the &lt;code&gt;000&lt;/code&gt; tag and returned &lt;code&gt;&apos;object&apos;&lt;/code&gt;. Every value type has a corresponding &lt;code&gt;typeof&lt;/code&gt; string except &lt;code&gt;null&lt;/code&gt;, which has been reporting the wrong answer for 30 years and counting.&lt;/p&gt;
&lt;h2 id=&quot;4---javascripts-seven-primitive-types&quot;&gt;4 - JavaScript’s seven primitive types&lt;/h2&gt;
&lt;p&gt;JavaScript has seven primitive types. Everything else is an object. The Records and Tuples proposal, which would have added two more for deeply immutable data structures, was &lt;a href=&quot;https://github.com/tc39/proposal-record-tuple/issues/394&quot;&gt;withdrawn by TC39&lt;/a&gt; in April 2025. So the count holds steady.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Boolean&lt;/strong&gt;: &lt;code&gt;true&lt;/code&gt; or &lt;code&gt;false&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Null&lt;/strong&gt;: the intentional absence of a value. Its only value is &lt;code&gt;null&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Undefined&lt;/strong&gt;: a declared but unassigned variable. Its only value is &lt;code&gt;undefined&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Number&lt;/strong&gt;: double-precision 64-bit &lt;a href=&quot;https://en.wikipedia.org/wiki/IEEE_754&quot;&gt;IEEE 754&lt;/a&gt; floating point. This covers integers and decimals alike, and yes, &lt;code&gt;typeof NaN === &apos;number&apos;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;BigInt&lt;/strong&gt;: introduced in ES2020, for integers beyond &lt;code&gt;Number.MAX_SAFE_INTEGER&lt;/code&gt; (2^53 - 1).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;String&lt;/strong&gt;: a sequence of characters, immutable.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Symbol&lt;/strong&gt;: introduced in ES2015, a unique identifier often used as an object key to avoid collisions.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Two properties worth remembering. Primitives are immutable: you can’t change a primitive value in place. And they’re passed by value: assign one to a variable or hand it to a function and you get a copy, so changes to the copy never touch the original.&lt;/p&gt;
&lt;h2 id=&quot;5---javascript-is-jit-compiled-not-interpreted&quot;&gt;5 - JavaScript is JIT-compiled, not interpreted&lt;/h2&gt;
&lt;p&gt;The old line that JavaScript is an interpreted language is wrong. Modern JavaScript is JIT-compiled. There’s no separate build step that turns your source into a machine-code executable ahead of time, like C++ or Rust. Instead, the engine compiles your code while it runs.&lt;/p&gt;
&lt;p&gt;Every modern engine, V8, SpiderMonkey, JavaScriptCore, uses a &lt;a href=&quot;https://v8.dev/blog/maglev&quot;&gt;tiered compilation pipeline&lt;/a&gt;. Code starts out interpreted for fast startup, and as the engine spots “hot” functions that run often, it compiles and optimizes them into machine code based on runtime heuristics. V8’s pipeline specifically goes Ignition (interpreter) to Sparkplug (fast non-optimizing compiler) to Maglev (mid-tier) to Turbofan (optimizing). That tiering is why JS engines are as fast as they are today.&lt;/p&gt;
&lt;p&gt;The tooling ecosystem is chasing the same speed. TypeScript’s compiler was &lt;a href=&quot;/blog/typescript-7-is-here&quot;&gt;rewritten in Go&lt;/a&gt; in 2026 for roughly an order-of-magnitude faster builds, and pnpm is &lt;a href=&quot;/blog/pnpm-v12-rust-rewrite&quot;&gt;rewriting its install engine in Rust&lt;/a&gt;. Bun went further, porting its &lt;a href=&quot;/blog/bun-14-rust-rewrite&quot;&gt;whole runtime from Zig to Rust&lt;/a&gt;. The language runs fast at runtime, and now the tools around it are catching up at build time.&lt;/p&gt;
&lt;h2 id=&quot;one-more-corner-worth-poking&quot;&gt;One more corner worth poking&lt;/h2&gt;
&lt;p&gt;None of these five facts change how you write a &lt;code&gt;for&lt;/code&gt; loop. But they explain the language’s shape. Why coercion behaves the way it does, why the spec is called ECMAScript, why &lt;code&gt;typeof null&lt;/code&gt; lies, why your code runs fast without a compile step. The language most of us treat as background noise has a 30-year-old bug baked into its type tags and a trademark case scheduled for discovery in federal court this September. And &lt;code&gt;Math.sumPrecise()&lt;/code&gt;, &lt;a href=&quot;/blog/baseline-2026-web-platform-apis&quot;&gt;newly Baseline in 2026&lt;/a&gt;, quietly fixed the floating-point summation errors that &lt;code&gt;reduce&lt;/code&gt; silently introduces. That’s worth knowing.&lt;/p&gt;
</content:encoded><category>javascript</category><category>webdev</category><category>programming</category><author>Dennis Morello</author></item></channel></rss>