<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: KahWee Teng</title>
    <description>The latest articles on DEV Community by KahWee Teng (@kahwee).</description>
    <link>https://dev.to/kahwee</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3161395%2F2a8ba4ac-9318-4a86-b6c0-bece43e29d81.jpg</url>
      <title>DEV Community: KahWee Teng</title>
      <link>https://dev.to/kahwee</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kahwee"/>
    <language>en</language>
    <item>
      <title>Thread Claims Interoperability. Your Network Has Three Meshes.</title>
      <dc:creator>KahWee Teng</dc:creator>
      <pubDate>Wed, 19 Aug 2026 17:45:31 +0000</pubDate>
      <link>https://dev.to/kahwee/thread-claims-interoperability-your-network-has-three-meshes-30gl</link>
      <guid>https://dev.to/kahwee/thread-claims-interoperability-your-network-has-three-meshes-30gl</guid>
      <description>&lt;p&gt;I was debugging a Home Assistant Thread configuration when I ran &lt;code&gt;dns-sd -B _meshcop._udp .&lt;/code&gt; and counted the results. Every ecosystem claims Thread. Matter unifies everything, they say. I have three separate Thread networks on the same physical network.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;eero-thread-3d8a&lt;/code&gt; is eero's mesh, with three border routers. &lt;code&gt;MyHome503847291&lt;/code&gt; is Apple's, backed by a single Apple TV. &lt;code&gt;AMZN-Thread-9e6b&lt;/code&gt; is Amazon's, with three Echo devices. None of them share traffic.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Command Nobody Runs
&lt;/h2&gt;

&lt;p&gt;Thread border routers broadcast their presence over mDNS under &lt;code&gt;_meshcop._udp&lt;/code&gt;. Run the browse and you get every border router regardless of ecosystem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dns-sd &lt;span class="nt"&gt;-B&lt;/span&gt; _meshcop._udp &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each result is a border router instance name. To see the full TXT record for one of them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dns-sd &lt;span class="nt"&gt;-L&lt;/span&gt; &lt;span class="s2"&gt;"eero_thread #3F2A"&lt;/span&gt; _meshcop._udp &lt;span class="nb"&gt;local&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives you enough detail to reconstruct your full Thread topology:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;nn&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;eero-thread-3d8a    ← network name&lt;/span&gt;
&lt;span class="py"&gt;xa&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;3d8ab4c291e7f052   ← thread extended address&lt;/span&gt;
&lt;span class="py"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;3d8ab4c291e7f052   ← device ID&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;nn=&lt;/code&gt; field is the Thread network name. Every border router sharing the same &lt;code&gt;nn=&lt;/code&gt; is on the same mesh. Group by &lt;code&gt;nn=&lt;/code&gt; and you have your mesh count.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;xa=&lt;/code&gt; field (the Thread Extended Address, equivalent to a MAC at the Thread layer) is where instance names come from. &lt;code&gt;eero_thread #3F2A&lt;/code&gt; has &lt;code&gt;xa=...c4a93f2a&lt;/code&gt;. The suffixes match. Except for the Thread leader: Thread's election protocol assigns RouterID 0 to the winning node, giving it RLOC16 &lt;code&gt;0x0000&lt;/code&gt;. That's why my gateway eero at &lt;code&gt;192.168.1.1&lt;/code&gt; shows up as &lt;code&gt;eero_thread #0000&lt;/code&gt;. Instance names change whenever Thread reassigns router IDs after a reboot or re-election. The hostname and IP are the stable identifiers.&lt;/p&gt;

&lt;p&gt;None of this is in any smart home app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three Meshes, No Bridge
&lt;/h2&gt;

&lt;p&gt;Thread devices on the same mesh route packets over 802.15.4 radio directly — no Wi-Fi, lower latency, fewer failure points. The assumption behind Thread is that your devices share a mesh.&lt;/p&gt;

&lt;p&gt;They probably don't. A device on eero's mesh talking to one on Amazon's routes: Thread → eero border router → Wi-Fi → Amazon border router → Thread. Two extra hops, Wi-Fi back in the critical path, which is exactly what Thread was designed to avoid.&lt;/p&gt;

&lt;p&gt;This is structural. Apple, Amazon, and eero each provision Thread credentials at setup time. Thread 1.3 has no mechanism for border routers from different vendors to merge meshes — the Connectivity Standards Alliance lists it as a known limitation. "Matter over Thread" performance depends on which mesh the communicating devices happen to share.&lt;/p&gt;

&lt;p&gt;For battery-powered Thread devices it's worse. They're end devices — they sleep, wake, send a packet, sleep again. When they wake they contact their parent router on their own mesh. A sensor on Amazon's mesh is unreachable from an eero automation without going through Wi-Fi both ways, with no timing guarantee. Automations chaining across ecosystems will have gaps. The more ecosystems in the mix, the more gaps.&lt;/p&gt;

&lt;h2&gt;
  
  
  eero Joins Every Device Without Asking
&lt;/h2&gt;

&lt;p&gt;Scanning &lt;code&gt;_matter._tcp&lt;/code&gt; and grouping by hostname reveals the fabric picture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dns-sd &lt;span class="nt"&gt;-B&lt;/span&gt; _matter._tcp &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For each result, resolve the hostname:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dns-sd &lt;span class="nt"&gt;-L&lt;/span&gt; &lt;span class="s2"&gt;"E0F4C2918B3A-00000000014B0CDA"&lt;/span&gt; _matter._tcp &lt;span class="nb"&gt;local&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then query the AAAA record to get the IPv6 address — Thread devices show a mesh-local &lt;code&gt;fd&lt;/code&gt; prefix rather than an IPv4:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dns-sd &lt;span class="nt"&gt;-q&lt;/span&gt; A3E817F294C06B5D.local AAAA IN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five of my devices each hold three fabrics:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A3E817F294C06B5D.local  FD8C:3A17:B294:0000:2E4F:91C3:7A08:D365
  Amazon Alexa     ← paired by user
  Apple Home       ← paired by user
  eero             ← auto-commissioned by eero
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I never added these to eero. Matter commissioning is supposed to be deliberate — the commissioner initiates, the device responds, they exchange cryptographic credentials. eero does this automatically for every Matter device it can reach via mDNS.&lt;/p&gt;

&lt;p&gt;The rationale is border router coordination: holding a fabric on every device lets eero steer which Thread mesh a device prefers. The side effect is that eero holds administrative access to every Matter device on your network regardless of which app you used to pair it.&lt;/p&gt;

&lt;p&gt;eero also registers itself as a Matter device in the browse — raw MAC address as hostname, link-local IPv6 only, no IPv4, eero-only fabric. It's not a controllable device. It's eero claiming a seat in the fabric to manage border router priority. You'd never see it from the app.&lt;/p&gt;

&lt;h2&gt;
  
  
  One Echo on the Wrong Mesh
&lt;/h2&gt;

&lt;p&gt;One Echo Studio showed up on a network called &lt;code&gt;eero-thread-b7c1&lt;/code&gt;. An eero-named mesh, but the only border router on it was an Amazon Echo.&lt;/p&gt;

&lt;p&gt;Amazon Echos create their Thread mesh on first setup and name it &lt;code&gt;AMZN-Thread-XXXX&lt;/code&gt;. This one picked up a stale credential instead, probably a leftover from a previous reset, and had been running as an eero-named mesh with Amazon hardware ever since. Thread devices joining it were routing through Amazon's stack under an eero identity. Neither ecosystem handles that gracefully.&lt;/p&gt;

&lt;p&gt;The fix is a factory reset. Fresh credentials, correct name, correct mesh. You'd never catch this from the Alexa app — it shows the device as connected and fine. &lt;code&gt;dns-sd&lt;/code&gt; shows the &lt;code&gt;nn=&lt;/code&gt; field and the OUI on the border router, and the mismatch is immediate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I Commission via eero First
&lt;/h2&gt;

&lt;p&gt;Given all this, the practical question is which ecosystem to use when first pairing a new device. I now default to eero, for three reasons.&lt;/p&gt;

&lt;p&gt;First, eero has three border routers. Apple has one. If the Apple TV reboots, every device on its Thread mesh loses its border router. eero's mesh survives losing any single node.&lt;/p&gt;

&lt;p&gt;Second, eero joins everything anyway. There's no configuration that prevents eero from auto-commissioning a Matter device onto its fabric. Fighting it is not possible. Accepting it means the question is just whether eero is the first fabric or the third.&lt;/p&gt;

&lt;p&gt;Third, all five of my Thread-capable Matter devices carry IPv6 addresses in the &lt;code&gt;FD8C:3A17:B294&lt;/code&gt; range — eero's Thread mesh-local /48 prefix. That prefix is fixed for the life of the Thread network. Whatever fabric Apple or Amazon holds on those devices, eero's mesh is the one routing their packets. Pairing via eero first means the fabric that controls the device matches the mesh that moves its traffic. That alignment is the closest you can get to a clean setup under the current fragmentation.&lt;/p&gt;

&lt;p&gt;The apps show which ecosystems have keys to a device. The network shows which mesh is doing the actual work. For most setups, those are different answers. Knowing which one is which changes how you approach commissioning.&lt;/p&gt;




&lt;h3&gt;
  
  
  Go deeper
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://kahwee.com/2026/thread-fragmentation-mdns/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=thread-network-debugging&amp;amp;utm_content=bottom-cta" rel="noopener noreferrer"&gt;The original article with its interactive Thread network model&lt;/a&gt;&lt;/p&gt;

</description>
      <category>smarthome</category>
      <category>networking</category>
      <category>matter</category>
      <category>debugging</category>
    </item>
    <item>
      <title>Tripsy Now Has a CLI (and an MCP)</title>
      <dc:creator>KahWee Teng</dc:creator>
      <pubDate>Wed, 19 Aug 2026 17:44:34 +0000</pubDate>
      <link>https://dev.to/kahwee/tripsy-now-has-a-cli-and-an-mcp-1ne4</link>
      <guid>https://dev.to/kahwee/tripsy-now-has-a-cli-and-an-mcp-1ne4</guid>
      <description>&lt;p&gt;Everything I wrote in &lt;a href="https://kahwee.com/2026/tripsy-sqlite-read-write/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=tripsy-cli-mcp&amp;amp;utm_content=inline-link" rel="noopener noreferrer"&gt;Reading and Writing Tripsy's SQLite Database on macOS&lt;/a&gt; is now unnecessary.&lt;/p&gt;

&lt;p&gt;Tripsy released an official &lt;a href="https://github.com/tripsyapp/cli" rel="noopener noreferrer"&gt;CLI&lt;/a&gt; and an &lt;a href="https://mcp.tripsy.app" rel="noopener noreferrer"&gt;MCP server&lt;/a&gt;. Install the CLI in one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://tripsy.app/install_cli | bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The installer drops two binaries: &lt;code&gt;tripsy&lt;/code&gt; for the CLI and &lt;code&gt;tripsy-mcp&lt;/code&gt; for running the MCP server standalone. Log in once and credentials go into the OS keychain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tripsy auth login &lt;span class="nt"&gt;--username&lt;/span&gt; you@example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What the old approach cost
&lt;/h2&gt;

&lt;p&gt;The SQLite method required quitting Tripsy before every write, backing up the database directory, managing WAL checkpoints, generating unique identifiers that avoided CloudKit tombstones, re-querying &lt;code&gt;Z_PRIMARYKEY&lt;/code&gt; every session because it jumps during sync, and then opening each inserted row through the Tripsy UI to push it to CloudKit. Updates to existing rows were silently reverted. One wrong identifier and the row vanished on the second app open with no error.&lt;/p&gt;

&lt;p&gt;That's a lot of ceremony to fix a check-in date.&lt;/p&gt;

&lt;h2&gt;
  
  
  The CLI is the right tool
&lt;/h2&gt;

&lt;p&gt;Fixing a hosting date, adding a missing transportation, backfilling a confirmation number — these are two-line commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tripsy hostings update &lt;span class="nt"&gt;--trip&lt;/span&gt; 1094984 &amp;lt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;--set&lt;/span&gt; &lt;span class="nv"&gt;starts_at&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'2026-06-07T22:00:00Z'&lt;/span&gt;
tripsy transportations create &lt;span class="nt"&gt;--trip&lt;/span&gt; 1094984 &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s1"&gt;'Drive SF → Fresno'&lt;/span&gt; &lt;span class="nt"&gt;--transportation-type&lt;/span&gt; roadtrip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Commands map directly to the API: &lt;code&gt;tripsy trips&lt;/code&gt;, &lt;code&gt;tripsy activities&lt;/code&gt;, &lt;code&gt;tripsy transportations&lt;/code&gt;, &lt;code&gt;tripsy hostings&lt;/code&gt;, &lt;code&gt;tripsy expenses&lt;/code&gt;, &lt;code&gt;tripsy documents&lt;/code&gt;. Output is human-readable by default, JSON when piped or passed &lt;code&gt;--json&lt;/code&gt;. Every response includes a &lt;code&gt;breadcrumbs&lt;/code&gt; field suggesting follow-up commands.&lt;/p&gt;

&lt;p&gt;A few things worth knowing:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;--help&lt;/code&gt; on every subcommand.&lt;/strong&gt; &lt;code&gt;tripsy activities --help&lt;/code&gt; shows syntax, examples, and common gotchas. Run it before guessing flag names.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;tripsy request&lt;/code&gt; for everything else.&lt;/strong&gt; Named commands cover the common cases. Raw API calls go through &lt;code&gt;tripsy request&lt;/code&gt; for anything not yet wrapped.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;tripsy doctor&lt;/code&gt;.&lt;/strong&gt; Checks auth and connectivity. Start here if something misbehaves.&lt;/p&gt;

&lt;p&gt;I prefer the CLI over the MCP for most things. The MCP hands control to an AI that interprets your intent — useful for open-ended planning, but less predictable for precise edits. The CLI does exactly what you tell it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Surprises
&lt;/h2&gt;

&lt;p&gt;Two things I didn't expect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every response includes a &lt;code&gt;breadcrumbs&lt;/code&gt; field that suggests what to run next.&lt;/strong&gt; After listing activities, the output tells you the exact command to show or create one. After creating a transportation, it gives you the show command with the new ID pre-filled. Most CLIs make you look this up. This one just tells you. It's a small thing that makes the tool feel designed for exploration rather than assumed expertise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;--set&lt;/code&gt; silently truncates values at the first space.&lt;/strong&gt; Set an address with &lt;code&gt;--set address='43011 N Fork Drive, Three Rivers, CA'&lt;/code&gt; and the stored value is &lt;code&gt;43011&lt;/code&gt;. No error, no warning — just wrong data. The workaround is &lt;code&gt;tripsy request PATCH&lt;/code&gt; with a &lt;code&gt;--data&lt;/code&gt; JSON body, but that path had its own issues with endpoint discovery. For a tool that otherwise has good ergonomics, this is a jarring gap. Worth filing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The MCP for planning conversations
&lt;/h2&gt;

&lt;p&gt;The MCP is the better fit when you want to reason about a trip rather than edit specific fields. Add &lt;code&gt;https://mcp.tripsy.app&lt;/code&gt; in Claude, sign in, authorize — done in under a minute. From there, Claude can read your trips, suggest activities, and adjust itineraries through natural language.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://tripsy.blog/plan-smarter-trips-with-tripsy-and-claude-or-chatgpt/" rel="noopener noreferrer"&gt;Tripsy blog post announcing it&lt;/a&gt; frames it as iterative conversation rather than database management. That framing is right for planning. It's less right when you need to fix a wrong departure time on a specific flight.&lt;/p&gt;

&lt;h2&gt;
  
  
  The SQLite post has mostly lost its value
&lt;/h2&gt;

&lt;p&gt;The schema documentation and CoreData timestamp explanation are still accurate — the &lt;code&gt;ZGENERALACTIVITY&lt;/code&gt; structure, &lt;code&gt;Z_PK&lt;/code&gt; management, and CoreData epoch offset haven't changed. If you're debugging what Tripsy stores locally or building something directly on the raw data, it's still a useful reference.&lt;/p&gt;

&lt;p&gt;For anything else, the CLI is the better tool, and the SQLite approach no longer makes sense to reach for. The quit-Tripsy, backup-the-WAL, generate-a-fresh-identifier workflow existed because there was no alternative. Now there is.&lt;/p&gt;

&lt;p&gt;The API has been available since May 2026. Use the CLI.&lt;/p&gt;




&lt;h3&gt;
  
  
  Go deeper
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://kahwee.com/2026/tripsy-sqlite-read-write/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=tripsy-cli-mcp&amp;amp;utm_content=bottom-cta" rel="noopener noreferrer"&gt;The detailed SQLite workaround this official CLI replaced&lt;/a&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>cli</category>
      <category>ai</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Your Linter Needs to Be as Fast as Your AI</title>
      <dc:creator>KahWee Teng</dc:creator>
      <pubDate>Wed, 19 Aug 2026 17:44:30 +0000</pubDate>
      <link>https://dev.to/kahwee/your-linter-needs-to-be-as-fast-as-your-ai-5dme</link>
      <guid>https://dev.to/kahwee/your-linter-needs-to-be-as-fast-as-your-ai-5dme</guid>
      <description>&lt;p&gt;I switched my own projects from ESLint to Biome because linting had become the slow part of an otherwise fast loop. Claude Code or Codex could hand me a complete patch, then ESLint made me wait to find out whether the patch was usable.&lt;/p&gt;

&lt;p&gt;Biome made the save-and-check cycle feel immediate. I gave up a few type-aware rules I was barely reading anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where ESLint slowed me down
&lt;/h2&gt;

&lt;p&gt;ESLint wasn't designed for a workflow where your AI commits 300 lines in one shot. It's a plugin framework, and every layer adds cost:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A parser (&lt;code&gt;@typescript-eslint/parser&lt;/code&gt;, or Babel, or both)&lt;/li&gt;
&lt;li&gt;Plugins that extend configs that extend other configs&lt;/li&gt;
&lt;li&gt;Optional type-checking via &lt;code&gt;tsconfig.json&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one is the real killer. Type-aware rules like &lt;code&gt;@typescript-eslint/no-floating-promises&lt;/code&gt; require ESLint to resolve your entire TypeScript type graph before running. On a medium-sized project, that's 15–30 seconds per run.&lt;/p&gt;

&lt;p&gt;ESLint also runs on Node.js. Not a complaint — that's how it ships to everyone. But interpreted JavaScript is just slower than compiled code for CPU-bound parse work.&lt;/p&gt;

&lt;p&gt;The result: most teams quietly disable type-aware rules because CI times balloon. Without them, ESLint catches style issues, not logic bugs. You're running a slow tool for modest returns.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Biome changes
&lt;/h2&gt;

&lt;p&gt;Biome is a linter and formatter written in Rust, with its own parser, and no dependency on the TypeScript compiler.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;ESLint + Prettier&lt;/th&gt;
&lt;th&gt;Biome&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Runtime&lt;/td&gt;
&lt;td&gt;Node.js (JavaScript)&lt;/td&gt;
&lt;td&gt;Native (Rust)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Parser&lt;/td&gt;
&lt;td&gt;External or separate&lt;/td&gt;
&lt;td&gt;Built-in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Formatter&lt;/td&gt;
&lt;td&gt;Prettier parses again&lt;/td&gt;
&lt;td&gt;Same AST as linter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Type-checking&lt;/td&gt;
&lt;td&gt;Optional, expensive&lt;/td&gt;
&lt;td&gt;None (syntax-only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Config surface&lt;/td&gt;
&lt;td&gt;Plugins + extends chains&lt;/td&gt;
&lt;td&gt;Single &lt;code&gt;biome.json&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The double-parse problem alone matters at scale. ESLint parses your files, then Prettier parses them again for formatting. Biome parses once and runs both passes on the same AST.&lt;/p&gt;

&lt;p&gt;According to Biome's own benchmarks, formatting runs ~25x faster than Prettier (multithreaded on modern hardware) and linting runs ~15x faster than ESLint on equivalent rule sets. Single-threaded, the numbers drop to around 7x and 4x respectively. Both figures come from Biome's benchmark suite, not independent testing — and the ESLint comparison deliberately excludes type-aware rules because Biome doesn't support them. Run the benchmark on your actual codebase before committing to the numbers.&lt;/p&gt;

&lt;p&gt;Everything runs in parallel across files by default.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup I use
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--save-dev&lt;/span&gt; &lt;span class="nt"&gt;--save-exact&lt;/span&gt; @biomejs/biome
npx @biomejs/biome init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;init&lt;/code&gt; generates a &lt;code&gt;biome.json&lt;/code&gt;. A working config for a TypeScript React project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"$schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://biomejs.dev/schemas/1.9.0/schema.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"organizeImports"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"linter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"rules"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"recommended"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"formatter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"indentStyle"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"space"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"indentWidth"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"javascript"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"formatter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"quoteStyle"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"double"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"trailingCommas"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"es5"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add to &lt;code&gt;package.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"lint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"biome lint ./src"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"biome format --write ./src"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"check"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"biome check --write ./src"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;biome check&lt;/code&gt; runs lint, format, and import sorting in one pass. That's the command you want on save and in CI.&lt;/p&gt;

&lt;p&gt;For VS Code, install the &lt;a href="https://marketplace.visualstudio.com/items?itemName=biomejs.biome" rel="noopener noreferrer"&gt;Biome extension&lt;/a&gt; and set it as the default formatter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"[javascript]"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"editor.defaultFormatter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"biomejs.biome"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"[typescript]"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"editor.defaultFormatter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"biomejs.biome"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"[typescriptreact]"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"editor.defaultFormatter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"biomejs.biome"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Disable Prettier if it's installed — they'll conflict on save.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I gave up
&lt;/h2&gt;

&lt;p&gt;Biome's rule surface is smaller — around 250 rules vs ESLint's 1,000+. No &lt;code&gt;@typescript-eslint/no-misused-promises&lt;/code&gt;. No &lt;code&gt;eslint-plugin-react-hooks&lt;/code&gt;. No type-aware rules at all, because Biome doesn't run the TypeScript compiler. Custom rules written in JavaScript aren't possible; Biome's rules are compiled Rust, so you're limited to what the project ships.&lt;/p&gt;

&lt;p&gt;Migrating also means a large formatting diff on day one. Biome's formatter matches Prettier's output in most cases, but not all. If your repo has clean formatting history, expect one noisy commit.&lt;/p&gt;

&lt;p&gt;These are real limitations. Whether they matter depends on how much you rely on niche rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  The linter has a smaller job now
&lt;/h2&gt;

&lt;p&gt;ESLint's value was always in catching things humans missed — a convention violation, a misused promise, a forgotten dependency in a hook.&lt;/p&gt;

&lt;p&gt;With AI generating the first draft, I can hand a lint error back to the agent that wrote the code. That makes quick feedback more useful to me than a large rule catalog that slows the loop down.&lt;/p&gt;

&lt;p&gt;That changes what I need from a linter. It has to run on every save, catch obvious errors, and stay out of the way. Biome fits that job better in my projects than a large ESLint plugin stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fast feedback is the feature
&lt;/h2&gt;

&lt;p&gt;I switched from ESLint to Biome on my own projects. The speed difference was immediately obvious in the save-on-format loop. What I gave up: a handful of type-aware rules I was barely reading the output of anyway. What I gained: a linter I actually leave on.&lt;/p&gt;

&lt;p&gt;Lint speed is part of the same AI coding loop in &lt;a href="https://kahwee.com/2026/claude-code-what-i-actually-use/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=biome-ai-workflows&amp;amp;utm_content=inline-link" rel="noopener noreferrer"&gt;Claude Code: what I actually use&lt;/a&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  Go deeper
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://kahwee.com/2026/ai-code-quality-is-changing/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=biome-ai-workflows&amp;amp;utm_content=bottom-cta" rel="noopener noreferrer"&gt;How I think about code review when AI writes the first draft&lt;/a&gt;&lt;/p&gt;

</description>
      <category>biome</category>
      <category>typescript</category>
      <category>webdev</category>
      <category>ai</category>
    </item>
    <item>
      <title>AI-Generated Code Review: What Reviewers Should Still Block</title>
      <dc:creator>KahWee Teng</dc:creator>
      <pubDate>Wed, 19 Aug 2026 17:42:16 +0000</pubDate>
      <link>https://dev.to/kahwee/ai-generated-code-review-what-reviewers-should-still-block-2bdf</link>
      <guid>https://dev.to/kahwee/ai-generated-code-review-what-reviewers-should-still-block-2bdf</guid>
      <description>&lt;p&gt;I spend less review time fixing awkward AI code than I did a year ago. A strong current model can often rewrite another model's clumsy abstraction in one pass. What I still cannot outsource is approval: I will not ask a teammate to approve code neither of us can explain.&lt;/p&gt;

&lt;p&gt;Those are different problems. Teams keep bundling them together as “code quality,” then spend expensive review time litigating taste.&lt;/p&gt;

&lt;p&gt;The current models I use produce much better code than the ones I used when I copied snippets out of ChatGPT. The older output often needed a human to sand down every rough edge. I would spend longer simplifying its first attempt than I saved by generating it.&lt;/p&gt;

&lt;p&gt;That has changed in my workflow. A strong current model can read a messy change from an older model, identify an unnecessary abstraction, and rewrite it into something smaller. I expect the cost of correcting yesterday's AI code to keep falling.&lt;/p&gt;

&lt;p&gt;That makes some technical debt less permanent than it used to be. We should change the review standard accordingly.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;span&gt;Review rule&lt;/span&gt;&lt;br&gt;
Spend human attention in proportion to blast radius and rollback cost, not generated line count.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  A prompt turns constraints into architecture
&lt;/h2&gt;

&lt;p&gt;I usually get a working implementation. The trouble starts when the model treats every detail in my prompt as a first-class system concern.&lt;/p&gt;

&lt;p&gt;I describe an airport feature and stress its boundaries: who can access it, when it applies, which cases should not qualify, and what must happen at the edges. The model responds by building gates around every condition. It writes a long ladder of &lt;code&gt;if&lt;/code&gt; statements, feature flags, eligibility checks, and fallbacks.&lt;/p&gt;

&lt;p&gt;Each condition sounds reasonable in isolation. Together, the code resembles a rule engine that nobody requested.&lt;/p&gt;

&lt;p&gt;The model struggles to decide whether those conditions belong in a component, a domain rule, or a policy boundary. It also struggles to distinguish a passing requirement from a constraint that deserves a durable abstraction. It gives similar weight to every sentence because the prompt is its whole specification. A human engineer usually gets that distinction from the roadmap, previous arguments, and the cost of changing the code later.&lt;/p&gt;

&lt;p&gt;This creates an awkward review. The working diff contains three layers of gating. A reviewer asks why, and the honest answer is often: “Claude did it.” That explains the origin. It never justifies the design.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cost has moved from writing to approval
&lt;/h2&gt;

&lt;p&gt;Better future models will clean up a meaningful share of the excess. We should ship some of it and move on.&lt;/p&gt;

&lt;p&gt;For a reversible feature, spending an hour polishing a model's awkward first draft can cost more than merging a working change and asking a later model to simplify it when the code proves worth keeping. The rewrite is cheap. The business value arrives sooner.&lt;/p&gt;

&lt;p&gt;Reviewers still pay a cost now. They need enough legibility to decide whether the change is safe, matches the product intent, or silently widens the system's behavior. A future model cannot retroactively make today's approval informed.&lt;/p&gt;

&lt;p&gt;I lower the bar for &lt;em&gt;polish&lt;/em&gt; and keep the bar for comprehension. Generated components do not need beauty on their first merge. Reviewers need explainable control flow and tradeoffs.&lt;/p&gt;

&lt;p&gt;I would also separate a real maintenance concern from a team preference dressed up as one. Code review has always carried local taste: whether a helper belongs above or below a component, which abstraction feels clean, and how much indirection looks respectable. Those arguments consume real time because everybody has a mental picture of neat code.&lt;/p&gt;

&lt;p&gt;In my experience, AI tends to write toward the most common pattern it has seen. That makes the result less tied to one engineer's private style. Stable, discoverable, cheap-to-change code does not need to satisfy the team's aesthetic ideal.&lt;/p&gt;

&lt;p&gt;Every blocking review comment should name the cost it avoids: a failure mode, an ownership boundary, a likely future extension, or a path that will make incidents harder to diagnose. “This feels ugly” is not a cost. If a team wants a style, it should encode it in a formatter, a linter, or a short written convention. It should not rediscover the rule in every pull request.&lt;/p&gt;

&lt;p&gt;The same shift is why fast validation matters more in AI workflows. AI writes the initial diff quickly, but the team still needs feedback before the context disappears. I made that case while moving to &lt;a href="https://kahwee.com/2026/biome-linting-for-ai-workflows/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=ai-code-review&amp;amp;utm_content=inline-link" rel="noopener noreferrer"&gt;Biome&lt;/a&gt;: fast checks keep the review loop from becoming a CI archaeology project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Line-by-line review is the wrong interface
&lt;/h2&gt;

&lt;p&gt;Line-by-line review does not scale to generated code. It survives because humans used to write every line slowly and reviewers had time to follow.&lt;/p&gt;

&lt;p&gt;Spelling, formatting, imports, dead branches, and simple convention violations should never consume reviewer attention. A formatter, linter, or targeted check can catch them more consistently than a tired engineer scanning a large diff. Teams already know this. They still leave too much of it for pull-request discussion.&lt;/p&gt;

&lt;p&gt;AI needs a distinct review job. One agent writes the change. Another examines the diff against the ticket, the relevant tests, and the parts of the repository it touches. It should flag expanded permissions, duplicate business rules, missing tests, and behavior that does not match the stated requirement.&lt;/p&gt;

&lt;p&gt;An AI reviewer needs an independent prompt, an independent model or context, and deterministic checks beneath it. Human reviewers can then focus on the remaining questions: did we choose the right behavior, is the risk acceptable, and are we changing a boundary that will be hard to undo?&lt;/p&gt;

&lt;h2&gt;
  
  
  Reversibility should set the bar
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;th&gt;Sensible review bar&lt;/th&gt;
&lt;th&gt;Block when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Local UI or internal tool&lt;/td&gt;
&lt;td&gt;Behavior is tested; rollback is cheap&lt;/td&gt;
&lt;td&gt;Control flow is unexplained or scope quietly expands&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Business rule or data migration&lt;/td&gt;
&lt;td&gt;Intent, edge cases, and ownership are explicit&lt;/td&gt;
&lt;td&gt;Rules are duplicated or recovery is uncertain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authentication, payments, shared infrastructure&lt;/td&gt;
&lt;td&gt;Design and failure modes are understood end to end&lt;/td&gt;
&lt;td&gt;Permissions, callers, observability, or rollback remain unclear&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I would accept an inelegant AI-generated condition tree in code that is easy to replace: a local UI flow, an internal tool, or a feature whose behavior is covered by ordinary tests. The team can ship, observe, and refactor once reality tells us which rules matter.&lt;/p&gt;

&lt;p&gt;Payments, authentication, and shared infrastructure require the stricter standard. Those systems accumulate callers, permissions, data, and operational assumptions. The cost of a bad abstraction rises before the next model gets a chance to repair it. “We can ask Opus to clean it up later” cannot serve as a rollback plan.&lt;/p&gt;

&lt;p&gt;For low-stakes code, the question is whether the change works and remains cheap to rewrite. For high-stakes code, the old standard holds: understand it, challenge the design, and reject the abstraction if it is not earned.&lt;/p&gt;

&lt;p&gt;The review I want is aimed at the decisions that matter, with automated checks handling the mechanical parts. Human attention belongs on the business boundary, not the missing comma.&lt;/p&gt;




&lt;h3&gt;
  
  
  Go deeper
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://kahwee.com/2026/ai-code-quality-is-changing/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=ai-code-review&amp;amp;utm_content=bottom-cta" rel="noopener noreferrer"&gt;Read the complete code-review field note on KahWee.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>codereview</category>
      <category>softwareengineering</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Vite’s ?url Import Gives You a URL, Not jQuery</title>
      <dc:creator>KahWee Teng</dc:creator>
      <pubDate>Wed, 19 Aug 2026 17:41:49 +0000</pubDate>
      <link>https://dev.to/kahwee/vites-url-import-gives-you-a-url-not-jquery-1dp4</link>
      <guid>https://dev.to/kahwee/vites-url-import-gives-you-a-url-not-jquery-1dp4</guid>
      <description>&lt;p&gt;The confusing part is only four characters long: &lt;code&gt;?url&lt;/code&gt;. Add it to a Vite import and the value changes from executable JavaScript to the address of an asset.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jquery/dist/jquery.slim.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;jqueryUrl&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jquery/dist/jquery.slim.js?url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;          &lt;span class="c1"&gt;// "function"&lt;/span&gt;
&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;jqueryUrl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// "string"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Import&lt;/th&gt;
&lt;th&gt;Value in your code&lt;/th&gt;
&lt;th&gt;What happens at import time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;jquery.slim.js&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;jQuery’s API&lt;/td&gt;
&lt;td&gt;JavaScript is loaded as a dependency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;jquery.slim.js?url&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A URL string&lt;/td&gt;
&lt;td&gt;The file is treated as an asset&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;[!TIP]&lt;br&gt;
If the next line calls the imported value, you probably do not want &lt;code&gt;?url&lt;/code&gt;. If the next line passes it to &lt;code&gt;src&lt;/code&gt;, &lt;code&gt;addModule()&lt;/code&gt;, or another URL-based browser API, you probably do.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The 20-second fix
&lt;/h2&gt;

&lt;p&gt;This fails because &lt;code&gt;jqueryUrl&lt;/code&gt; is a string, not the jQuery function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;jqueryUrl&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jquery/dist/jquery.slim.js?url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;jqueryUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#app&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;hide&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// TypeError: jqueryUrl is not a function&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remove the suffix when you need to use jQuery in the current module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jquery/dist/jquery.slim.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#app&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;hide&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For jQuery 4, prefer the package’s public slim entry point instead of reaching into &lt;code&gt;dist&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jquery/slim&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#app&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;hide&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The jQuery project documents &lt;code&gt;jquery/slim&lt;/code&gt; as the supported npm import for the slim build. Older jQuery 3 projects often use the &lt;code&gt;dist/jquery.slim.js&lt;/code&gt; path shown above.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Vite actually does
&lt;/h2&gt;

&lt;p&gt;Vite calls &lt;code&gt;?url&lt;/code&gt; an &lt;a href="https://vite.dev/guide/assets.html#explicit-url-imports" rel="noopener noreferrer"&gt;explicit URL import&lt;/a&gt;. It opts a file into the asset pipeline and makes the module’s default export a string.&lt;/p&gt;

&lt;p&gt;During development, that string may point to the source file. In a production build, Vite can give the asset a hashed filename or inline it as a data URL, depending on the file and configuration. The stable contract is not the exact path. The stable contract is: &lt;strong&gt;you receive a string that the browser can use as a URL&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Importing that URL does not execute the file in the current module. The file can still execute later if you load the URL through a browser API such as a script element or a worklet loader.&lt;/p&gt;

&lt;h2&gt;
  
  
  When ?url is the right tool
&lt;/h2&gt;

&lt;p&gt;A Paint Worklet is the clean example because &lt;code&gt;addModule()&lt;/code&gt; asks for a URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;workletUrl&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./paint-worklet.js?url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;CSS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paintWorklet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addModule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;workletUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same idea applies when a document needs a classic script URL. For example, you can load the jQuery distribution into an iframe rather than importing jQuery into the parent page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;jqueryUrl&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jquery/dist/jquery.slim.js?url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;script&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;iframe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contentDocument&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;script&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jqueryUrl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;iframe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contentDocument&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;head&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here the string is exactly what &lt;code&gt;script.src&lt;/code&gt; needs. jQuery executes when the iframe loads the script, not when Vite imports the URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do not use bare ?url for a worker graph
&lt;/h2&gt;

&lt;p&gt;If a worker imports other modules, let Vite process the worker and its dependencies. Vite’s &lt;a href="https://vite.dev/guide/features.html#web-workers" rel="noopener noreferrer"&gt;recommended worker syntax&lt;/a&gt; stays close to the browser standard:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;worker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Worker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./worker.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;module&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vite also supports &lt;code&gt;?worker&lt;/code&gt; when you want an imported constructor, and &lt;code&gt;?worker&amp;amp;url&lt;/code&gt; when an API specifically needs the URL of a processed worker.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Need&lt;/th&gt;
&lt;th&gt;Use&lt;/th&gt;
&lt;th&gt;You receive&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Call jQuery in this module&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;jquery/slim&lt;/code&gt; or &lt;code&gt;jquery.slim.js&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;jQuery’s API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pass a file to a URL-based API&lt;/td&gt;
&lt;td&gt;&lt;code&gt;?url&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;URL string&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read a file’s contents&lt;/td&gt;
&lt;td&gt;&lt;code&gt;?raw&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Source string&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Create a processed worker&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;new Worker(new URL(...))&lt;/code&gt; or &lt;code&gt;?worker&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Worker instance or constructor&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The rule worth remembering
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;?url&lt;/code&gt; does not mean “import this module from a URL.” It means “import &lt;strong&gt;the URL of this file&lt;/strong&gt;.”&lt;/p&gt;

&lt;p&gt;That one distinction explains both the error and the fix.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://vite.dev/guide/assets.html" rel="noopener noreferrer"&gt;Vite: Static Asset Handling&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://vite.dev/guide/features.html#web-workers" rel="noopener noreferrer"&gt;Vite: Web Workers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/jquery/jquery-dist#using-jquery-from-npm" rel="noopener noreferrer"&gt;jQuery: npm and slim-build imports&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Go deeper
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://kahwee.com/2026/vite-url-import-jquery/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=vite-url-guide&amp;amp;utm_content=bottom-cta" rel="noopener noreferrer"&gt;Read the original guide and future updates on KahWee.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>vite</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>jquery</category>
    </item>
    <item>
      <title>Sora Isn't the Problem: It's the Mirror</title>
      <dc:creator>KahWee Teng</dc:creator>
      <pubDate>Mon, 20 Oct 2025 04:50:32 +0000</pubDate>
      <link>https://dev.to/kahwee/sora-isnt-the-problem-its-the-mirror-hd8</link>
      <guid>https://dev.to/kahwee/sora-isnt-the-problem-its-the-mirror-hd8</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This article was originally published on &lt;a href="https://kahwee.com/2025/sora-isnt-the-problem/" rel="noopener noreferrer"&gt;KahWee's blog&lt;/a&gt;. Read more on the original site.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Real Problem
&lt;/h2&gt;

&lt;p&gt;I finally got access to Sora in TikTok form, and something clicked. This isn't annoying because Sora is bad. It's annoying because it's honest.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Problem: Friction Is Gone
&lt;/h2&gt;

&lt;p&gt;Here's the insight: lying used to have a cost. If you wanted to fabricate video evidence, you had to &lt;em&gt;work&lt;/em&gt;. Film it. Edit it. Make it convincing. That work was friction. Friction meant there was a penalty for lying—time, effort, risk of being caught in the production.&lt;/p&gt;

&lt;p&gt;Sora removes that penalty entirely.&lt;/p&gt;

&lt;p&gt;Now you prompt an AI. You want a fake historical event? Seconds. Celebrity deepfake? Done. False testimony on video? Trivial. The cost of lying just collapsed to zero. It's now &lt;em&gt;easier&lt;/em&gt; to fabricate than to capture reality.&lt;/p&gt;

&lt;p&gt;This changes everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Breaks the System
&lt;/h2&gt;

&lt;p&gt;Social media has always optimized for engagement over truth. But engagement-over-truth only works if truth is expensive enough to be selective. You can't put &lt;em&gt;everything&lt;/em&gt; on the feed. You have to choose.&lt;/p&gt;

&lt;p&gt;With friction, lying is &lt;em&gt;selective&lt;/em&gt;. You lie when it matters, when the payoff justifies the work. The system survives because most content is still just... regular stuff. People sharing their lives. Creators doing legitimate work.&lt;/p&gt;

&lt;p&gt;Sora removes that selection. Now lying is &lt;em&gt;free&lt;/em&gt;. So the system optimizes: if truth and lies cost the same to produce, and if engagement cares about neither, then the algorithm picks based on one thing: what keeps you scrolling?&lt;/p&gt;

&lt;p&gt;The answer isn't truth. It's novelty. Spectacle. Uncanniness. Deepfakes of celebrities doing weird things are more engaging than someone filming their actual day.&lt;/p&gt;

&lt;p&gt;Once friction is gone, the system &lt;em&gt;must&lt;/em&gt; fill the feed with fabrication. Not because creators are evil. But because the incentives make fabrication &lt;em&gt;cheaper than reality&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Looks Like
&lt;/h2&gt;

&lt;p&gt;Echo chambers accelerate. TikTok already concentrates algorithmic curation into bubbles. Sora makes that bubble self-reinforcing: same IP, same memes, same likenesses, generated infinitely. Homogeneity at scale. The machine feeding itself.&lt;/p&gt;

&lt;p&gt;Truth becomes optional. Audiences stop sorting by "real vs. fake" and start sorting by "entertaining vs. dull." The Verge reported getting trapped in scroll loops of deepfaked celebrities and fabricated moments—even knowing they were AI. The uncanniness is &lt;em&gt;the feature&lt;/em&gt;. Plausibility doesn't require proof anymore.&lt;/p&gt;

&lt;p&gt;Persuasion decouples from evidence. Short-form video already rewards punchy narratives over grounded reporting. Sora makes that worse: you can &lt;em&gt;visually assert&lt;/em&gt; anything now. Any scene. Any dialogue. Any context. Spectacle beats verification. Virality beats truth.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Epiphany
&lt;/h2&gt;

&lt;p&gt;Here's what hit me: Sora isn't &lt;em&gt;breaking&lt;/em&gt; social media. It's just making visible what was always there.&lt;/p&gt;

&lt;p&gt;We built platforms optimized for engagement. Then we added algorithms designed to concentrate attention. Then we added infinite scroll and habit-forming UI. Then we added unlimited content generation via AI.&lt;/p&gt;

&lt;p&gt;At each step, we told ourselves it was fine. "Engagement metrics are just incentives." "Echo chambers are just efficiency." "Disinformation is a problem we'll solve later."&lt;/p&gt;

&lt;p&gt;But watching Sora—watching people trapped in loops of fabricated celebrity cameos—you can't pretend anymore. The problem isn't that AI is &lt;em&gt;too good&lt;/em&gt; at generating video. The problem is that friction is &lt;em&gt;gone&lt;/em&gt;. When lying costs nothing and engagement doesn't care about truth, the system must fill itself with slop.&lt;/p&gt;

&lt;p&gt;That's not a bug. That's the machine working exactly as designed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Friction Did For Us
&lt;/h2&gt;

&lt;p&gt;We didn't realize it, but friction was a natural check on this. Video production was slow. Editing took time. Deepfakes required skill. There was a &lt;em&gt;price&lt;/em&gt; for lying on camera. Most people paid it in truth instead.&lt;/p&gt;

&lt;p&gt;Now there is no price. Lying is as effortless as telling the truth. The system was never equipped to handle that. It was designed assuming &lt;em&gt;some&lt;/em&gt; friction. Some cost to fabrication. Some incentive to be selective about deception.&lt;/p&gt;

&lt;p&gt;Sora removes that assumption.&lt;/p&gt;

&lt;p&gt;The result is what you're seeing: infinite scroll of fabricated content, algorithmically sorted for engagement, presented in a UI designed for habit formation, watched by people who stopped caring if it's real.&lt;/p&gt;

&lt;p&gt;That's not a problem with Sora. That's what happens when you remove friction from a system designed to exploit engagement above all else.&lt;/p&gt;

&lt;p&gt;The machine didn't break. It just stopped pretending.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Related posts:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://future.forem.com/2025/ai-the-new-backdoor-layoff/"&gt;AI The New Backdoor Layoff&lt;/a&gt; - How companies use AI as cover for not hiring&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://future.forem.com/2025/on-technological-stratification/"&gt;On Technological Stratification&lt;/a&gt; - How AI widens inequality and access gaps&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://future.forem.com/2025/perplexity-replaces-google-search-and-apple-news-for-me/"&gt;Perplexity replaces Google Search and Apple News for me&lt;/a&gt; - How AI is changing information consumption&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://future.forem.com/2025/google-ai-overviews-cutting-web-traffic-in-half/"&gt;Google AI Overviews are cutting web traffic in half&lt;/a&gt; - The impact of AI on traditional web patterns&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>socialmedia</category>
      <category>technology</category>
      <category>analysis</category>
    </item>
    <item>
      <title>Migrating from Remix to React Router v7</title>
      <dc:creator>KahWee Teng</dc:creator>
      <pubDate>Mon, 20 Oct 2025 04:50:17 +0000</pubDate>
      <link>https://dev.to/kahwee/migrating-from-remix-to-react-router-v7-4gfo</link>
      <guid>https://dev.to/kahwee/migrating-from-remix-to-react-router-v7-4gfo</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This article was originally published on &lt;a href="https://kahwee.com/2025/migrating-from-remix-to-react-router-v7/" rel="noopener noreferrer"&gt;KahWee's blog&lt;/a&gt;. Read more on the original site.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why I Switched
&lt;/h2&gt;

&lt;p&gt;Last weekend, I made the decision to migrate one of my full-stack React applications from Remix to React Router v7 framework mode. The migration took about two days and went surprisingly smooth - here's why I made the switch, what the process entailed, and the practical insights that made it successful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why React Router v7?
&lt;/h2&gt;

&lt;p&gt;React Router v7 is Remix v3 renamed. Ryan Florence and Michael Jackson merged the projects because "Remix v2 had become such a thin wrapper around React Router that an artificial separation developed between the two projects."&lt;/p&gt;

&lt;p&gt;The practical benefits are immediate: instead of juggling &lt;code&gt;@remix-run/node&lt;/code&gt;, &lt;code&gt;@remix-run/react&lt;/code&gt;, &lt;code&gt;@remix-run/serve&lt;/code&gt;, and others, everything consolidates into the unified &lt;code&gt;react-router&lt;/code&gt; package. My dependencies dropped from 16 to 3. The new &lt;code&gt;react-router typegen&lt;/code&gt; command eliminates manual type annotations in loader functions. The Vite-based build system is cleaner than Remix's custom setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Toughest Part: TypeScript Type System Overhaul
&lt;/h2&gt;

&lt;p&gt;While the API compatibility made most changes straightforward, &lt;strong&gt;TypeScript integration was by far the most challenging aspect&lt;/strong&gt; of the migration. The git log reveals multiple commits dedicated to resolving type issues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LoaderFunctionArgs and useLoaderData typing&lt;/strong&gt;: Every route needed manual type updates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Route module type safety&lt;/strong&gt;: New type patterns required learning React Router v7's approach&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interface mismatches&lt;/strong&gt;: Database query results needed type alignment with component expectations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Case-sensitivity bugs&lt;/strong&gt;: Database queries failed due to case-sensitive matching that worked in Remix&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Multiple commits were dedicated to resolving TypeScript errors, indicating hours of type debugging across the migration.&lt;/p&gt;

&lt;p&gt;This taught me that &lt;strong&gt;React Router v7's type system is more strict&lt;/strong&gt; than Remix v2, which is ultimately beneficial but creates friction during migration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migration Overview: What Actually Changed
&lt;/h2&gt;

&lt;p&gt;The migration touched &lt;strong&gt;40 files&lt;/strong&gt; with &lt;strong&gt;327 insertions and 918 deletions&lt;/strong&gt; - a net reduction of 591 lines. Here's the breakdown:&lt;/p&gt;

&lt;h3&gt;
  
  
  Package Dependencies
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- "@remix-run/node": "^2.17.0"
- "@remix-run/react": "^2.17.0"
- "@remix-run/serve": "^2.17.0"
- "@remix-run/dev": "^2.17.0"
&lt;/span&gt;&lt;span class="gi"&gt;+ "@react-router/node": "^7.8.2"
+ "@react-router/serve": "^7.8.2"
+ "@react-router/dev": "^7.8.2"
+ "react-router": "^7.8.2"
+ "react-router-dom": "^7.8.2"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Build Scripts
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- "build": "remix vite:build"
- "dev": "remix vite:dev"
- "start": "remix-serve ./build/server/index.js"
&lt;/span&gt;&lt;span class="gi"&gt;+ "build": "react-router build"
+ "dev": "react-router dev"
+ "start": "react-router-serve ./build/server/index.js"
+ "typecheck": "react-router typegen &amp;amp;&amp;amp; tsc"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Vite Configuration
&lt;/h3&gt;

&lt;p&gt;The Vite config transformation was dramatic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- import { vitePlugin as remix } from '@remix-run/dev';
- // Complex remix configuration with future flags
- remix({
-   future: {
-     v3_fetcherPersist: true,
-     v3_relativeSplatPath: true,
-     v3_throwAbortReason: true,
-     v3_singleFetch: true,
-     v3_lazyRouteDiscovery: true,
-   }
- })
&lt;/span&gt;&lt;span class="gi"&gt;+ import { reactRouter } from '@react-router/dev/vite';
+ // Simple, clean configuration
+ plugins: [reactRouter(), tsconfigPaths(), tailwindcss()]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Import Updates Across Components
&lt;/h3&gt;

&lt;p&gt;Every route and component needed import updates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- import type { LoaderFunctionArgs } from '@remix-run/node';
- import { useLoaderData } from '@remix-run/react';
&lt;/span&gt;&lt;span class="gi"&gt;+ import type { LoaderFunctionArgs } from 'react-router';
+ import { useLoaderData } from 'react-router';
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Step-by-Step Migration Process
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Phase 1: Dependencies and Configuration (30 minutes)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Package.json overhaul&lt;/strong&gt;: Replaced all &lt;code&gt;@remix-run/*&lt;/code&gt; packages with React Router equivalents&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Script updates&lt;/strong&gt;: Changed build/dev commands to use &lt;code&gt;react-router&lt;/code&gt; CLI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vite configuration&lt;/strong&gt;: Simplified from complex Remix config to minimal React Router setup&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;New config file&lt;/strong&gt;: Added &lt;code&gt;react-router.config.ts&lt;/code&gt; for framework-specific settings&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Phase 2: Import Refactoring (90 minutes)
&lt;/h3&gt;

&lt;p&gt;This was the most time-consuming phase. Every file importing from &lt;code&gt;@remix-run/*&lt;/code&gt; needed updates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Route files&lt;/strong&gt; (23 files): Updated loader/action imports and useLoaderData calls&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Component files&lt;/strong&gt; (8 files): Changed Link and Form imports&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Entry files&lt;/strong&gt; (2 files): Updated server/client entry points&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Utility files&lt;/strong&gt; (7 files): Modified auth and theme utilities&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The process was methodical: start with entry points, then routes, then components, fixing TypeScript errors as they appeared.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 3: Configuration Fine-tuning (30 minutes)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript config&lt;/strong&gt;: Updated to use React Router types&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Route file naming&lt;/strong&gt;: Converted splat routes from &lt;code&gt;api.auth.$.ts&lt;/code&gt; to &lt;code&gt;api.auth.$rest.tsx&lt;/code&gt; for clarity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Development scripts&lt;/strong&gt;: Added the new &lt;code&gt;react-router typegen&lt;/code&gt; command&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Understanding React Router Typegen: The Game Changer
&lt;/h2&gt;

&lt;p&gt;One feature that took me time to fully appreciate was &lt;code&gt;react-router typegen&lt;/code&gt;. &lt;strong&gt;This wasn't available in Remix&lt;/strong&gt; - it's a React Router v7 innovation that represents a significant leap in TypeScript integration.&lt;/p&gt;

&lt;h3&gt;
  
  
  What React Router Typegen Does
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;typegen&lt;/code&gt; command generates route-specific TypeScript types automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;react-router typegen
&lt;span class="c"&gt;# Generates types in .react-router/types/ directory&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It creates a &lt;code&gt;+types/&amp;lt;route-file&amp;gt;.d.ts&lt;/code&gt; for each route, providing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Automatic type inference&lt;/strong&gt; for loader data without manual interfaces&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Route-specific parameter typing&lt;/strong&gt; (URL params, search params)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Action and loader return type validation&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Component prop type safety&lt;/strong&gt; based on your actual route implementation&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Rationale Behind Typegen
&lt;/h3&gt;

&lt;p&gt;React Router's type generation executes your route config (&lt;code&gt;app/routes.ts&lt;/code&gt;) to determine routes, then generates corresponding TypeScript definitions. This &lt;strong&gt;build-time analysis&lt;/strong&gt; provides runtime safety without polluting the API design.&lt;/p&gt;

&lt;p&gt;Instead of manually defining &lt;code&gt;LoaderData&lt;/code&gt; interfaces for every route, typegen infers them from your actual loader implementations. This eliminates the common Remix pattern of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Old Remix pattern - manual interface definition&lt;/span&gt;
&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;LoaderData&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;recipes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Recipe&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nl"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;LoaderData&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With React Router v7, you simply write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// New React Router v7 pattern - types inferred automatically&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;LoaderFunctionArgs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Return whatever you want, typegen handles the interface&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;recipes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getRecipes&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Component gets proper typing automatically&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;RecipeList&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;recipes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useLoaderData&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;loader&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="c1"&gt;// recipes and user are fully typed without manual interfaces&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The type inference approach fixes the interface drift problem that plagued my Remix codebase. With manual interfaces, you change a loader to return additional data but forget to update the interface. TypeScript doesn't complain because the interface still "works" - it just silently ignores the new properties.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;useLoaderData&amp;lt;typeof loader&amp;gt;&lt;/code&gt; eliminates this entirely. The type reflects your actual loader implementation, not what you think it should return. When you refactor a loader's return structure, every component using that data updates automatically.&lt;/p&gt;

&lt;p&gt;Here's where this really helps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;LoaderFunctionArgs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getAuthUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;recipes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;requiresAuth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;recipes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getUserRecipes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;recipes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;requiresAuth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;totalCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;recipes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;RecipePage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useLoaderData&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;loader&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="c1"&gt;// TypeScript knows data.requiresAuth is boolean&lt;/span&gt;
  &lt;span class="c1"&gt;// data.totalCount only exists when user is present&lt;/span&gt;
  &lt;span class="c1"&gt;// data.user is exactly User | null based on the conditional logic&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The manual interface approach would require defining a union type or optional properties, then keeping that interface in sync with the loader logic. With type inference, your implementation is the contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Happened to Remix?
&lt;/h2&gt;

&lt;p&gt;Ryan Florence and Michael Jackson handed React Router v7 to an open governance committee while they focus on Remix v3. But Remix v3 isn't an iteration - it's a complete rewrite with no React dependencies. They're building on a fork of Preact for "AI-first development" and "AI driven user interfaces." This leaves React Router v7 as the clear successor for production React applications that want the Remix experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migration Notes
&lt;/h2&gt;

&lt;p&gt;The API compatibility made most changes straightforward - loaders, actions, and components work identically. The migration was primarily changing imports and build configuration.&lt;/p&gt;

&lt;p&gt;Some gotchas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Route files with &lt;code&gt;.ts&lt;/code&gt; extensions needed to become &lt;code&gt;.tsx&lt;/code&gt; for React Router v7 to recognize them&lt;/li&gt;
&lt;li&gt;The entry.server.tsx got much simpler - React Router v7 removed the bot/browser request splitting logic&lt;/li&gt;
&lt;li&gt;Bundle size dropped ~30% and build times improved with the unified package structure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AI assistance was critical for batch import updates across 40+ files. What could have been a full day of manual work became two focused sessions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Worth the Switch?
&lt;/h2&gt;

&lt;p&gt;React Router v7 is actively developed while Remix v2 is in maintenance mode. The simplified dependency tree, better TypeScript integration, and improved build system provide immediate benefits. For teams on Remix v2 with future flags enabled, the migration is straightforward.&lt;/p&gt;

&lt;p&gt;Whether coming from Remix or considering alternatives to Next.js, React Router v7 framework mode handles full-stack React applications well.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The migration is complete and running smoothly on React Router v7. The process has proven that React Router v7 framework mode is a worthy successor to Remix, offering the same power with simplified tooling and enhanced type safety.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>migration</category>
    </item>
    <item>
      <title>AI Overviews are cutting web traffic in half</title>
      <dc:creator>KahWee Teng</dc:creator>
      <pubDate>Mon, 20 Oct 2025 04:48:38 +0000</pubDate>
      <link>https://dev.to/kahwee/ai-overviews-are-cutting-web-traffic-in-half-1jml</link>
      <guid>https://dev.to/kahwee/ai-overviews-are-cutting-web-traffic-in-half-1jml</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This article was originally published on &lt;a href="https://kahwee.com/2025/google-ai-overviews-cutting-web-traffic-in-half/" rel="noopener noreferrer"&gt;KahWee's blog&lt;/a&gt;. Read more on the original site.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Numbers
&lt;/h2&gt;

&lt;p&gt;New research from the Pew Research Center, as &lt;a href="https://arstechnica.com/ai/2025/07/research-shows-google-ai-overviews-reduce-website-clicks-by-almost-half/" rel="noopener noreferrer"&gt;reported by Ars Technica&lt;/a&gt;, reveals that Google's AI Overviews are significantly impacting website traffic. According to the study, when AI-generated summaries appear at the top of Google search results, users are almost half as likely to click through to other websites—dropping from a 15% to an 8% click rate.&lt;/p&gt;

&lt;p&gt;Even more striking, just 1% of users click on the sources cited within the AI Overviews, with Wikipedia, YouTube, and Reddit being the most frequently referenced. The study found that about 1 in 5 Google searches now display these AI Overviews, especially for longer, question-based queries.&lt;/p&gt;

&lt;p&gt;Despite Google's claims that AI features drive engagement and new opportunities for websites, the data suggests users are more likely to end their search after reading an AI-generated summary—potentially leaving them with incomplete or even incorrect information, as generative AI is known to occasionally produce errors.&lt;/p&gt;

&lt;h2&gt;
  
  
  My experience mirrors this trend
&lt;/h2&gt;

&lt;p&gt;I use Dia, a browser with an LLM model built in, and I love it. I barely use Google anymore. With information synthesized directly, I rarely visit websites either. Getting immediate, contextualized answers without clicking through multiple sites has completely changed how I consume information.&lt;/p&gt;

&lt;p&gt;This behavioral shift raises fascinating questions about the future of the web. Are we witnessing the emergence of the "dead internet theory" in practice? When AI systems can synthesize and present information without requiring users to visit original sources, what happens to the web's fundamental click-through economy?&lt;/p&gt;

</description>
      <category>seo</category>
      <category>google</category>
      <category>ai</category>
      <category>analysis</category>
    </item>
  </channel>
</rss>
