<?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: Isaiah Kim</title>
    <description>The latest articles on DEV Community by Isaiah Kim (@kyisaiah47).</description>
    <link>https://dev.to/kyisaiah47</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%2F3981339%2F7515e39c-98fe-4819-bcc5-d59ace01fc25.jpeg</url>
      <title>DEV Community: Isaiah Kim</title>
      <link>https://dev.to/kyisaiah47</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kyisaiah47"/>
    <language>en</language>
    <item>
      <title>I turned on error tracking. Most of it was Outlook.</title>
      <dc:creator>Isaiah Kim</dc:creator>
      <pubDate>Mon, 17 Aug 2026 14:27:01 +0000</pubDate>
      <link>https://dev.to/kyisaiah47/i-turned-on-error-tracking-most-of-it-was-outlook-3eke</link>
      <guid>https://dev.to/kyisaiah47/i-turned-on-error-tracking-most-of-it-was-outlook-3eke</guid>
      <description>&lt;p&gt;A weekly report landed on August 17th telling me my sites had thrown 363 exceptions, up 156 percent from the week before, with crash-free sessions at 94.29 percent. That is the kind of number that reorganises a day. Something is broken badly enough that roughly one visit in eighteen hits it, and I have no idea which page.&lt;/p&gt;

&lt;p&gt;333 of those 363 were not my code. Not a subtle bug in my code, not a dependency of my code. Two browser artifacts and a dev server.&lt;/p&gt;

&lt;h2&gt;
  
  
  What was actually in there
&lt;/h2&gt;

&lt;p&gt;The products involved have nothing to do with each other. AgentWire is a curation lane for agentic developer tooling, MCP servers and coding-agent harnesses, and its site is a 1:1 reflection of what the accounts actually posted. KitGrade is a directory of SaaS starter kits scored from measured facts, where every kit is labelled hands-on or documented and the label records whether the kit was cloned onto a machine and built. Different data, different stacks underneath, different everything. The only thing they share is the analytics snippet.&lt;/p&gt;

&lt;p&gt;327 of the 363 were this string:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Non-Error promise rejection captured with value: Object Not Found Matching Id:N,
MethodName:update, ParamCount:4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is not from any code I wrote. It comes from the wrapper Microsoft puts around links opened out of Outlook, running inside the page it just opened. Which is why it fired on AgentWire and KitGrade and everything else at the same moment: an error that appears simultaneously across products sharing no code is not coming from any of them. That simultaneity is the only tell you get, and you only get it if you have more than one site reporting into the same place.&lt;/p&gt;

&lt;p&gt;6 more were &lt;code&gt;ResizeObserver loop completed with undelivered notifications&lt;/code&gt;, which is the notice a browser emits when a layout observer runs long. Nothing is on fire. Nobody sees anything.&lt;/p&gt;

&lt;p&gt;The remaining 30 were mine, in the least useful sense. They were a dev server on &lt;code&gt;http://localhost:3737&lt;/code&gt; reporting compile errors into the production project while I was mid-edit: &lt;code&gt;PAD_X is not defined&lt;/code&gt;, &lt;code&gt;SelectedWork is not defined&lt;/code&gt;. Errors I had already fixed before the report was written, filed alongside real ones.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa6rigbd35dkes6a5jmle.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa6rigbd35dkes6a5jmle.png" alt="I turned on error tracking. Most of it was Outlook. — code" width="800" height="538"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that generalises
&lt;/h2&gt;

&lt;p&gt;Turning on browser exception capture is one boolean now. In posthog-js, which I am on at 1.407.5, it is &lt;code&gt;capture_exceptions: true&lt;/code&gt; next to the pageview settings. You add a line, you deploy, and a week later you have a health metric.&lt;/p&gt;

&lt;p&gt;The metric is wrong out of the box, and it is wrong in the direction that costs you the most. It reads worse than reality, so the first thing it makes you do is go looking for a crash that is not there. Then, once you have learned that the report is noise, it reads as noise forever, and the first real one arrives in a mail that looks exactly like the previous four.&lt;/p&gt;

&lt;p&gt;The dev server half is worth its own sentence. The project key lives in the client bundle, so it is the same key on localhost, on a preview URL and in production. Every environment reports into one place unless you tell it not to.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fornexm063d7u4a2y0342.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fornexm063d7u4a2y0342.png" alt="I turned on error tracking. Most of it was Outlook. — architecture" width="800" height="980"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The gate
&lt;/h2&gt;

&lt;p&gt;posthog-js has a &lt;code&gt;before_send&lt;/code&gt; hook that runs in the browser and drops the event when you return null. I put the filter there rather than in the dashboard, because a dropped event costs nothing and a filtered one still has to be sent, stored and then excluded from every view forever after.&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="nx"&gt;before_send&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="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;event&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;$exception&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&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;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;host&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;localhost&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.localhost&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;[\d&lt;/span&gt;&lt;span class="sr"&gt;.&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+$/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.vercel.app&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&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;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;$exception_values&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/Object Not Found Matching Id:&lt;/span&gt;&lt;span class="se"&gt;\d&lt;/span&gt;&lt;span class="sr"&gt;+/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&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="sr"&gt;/ResizeObserver loop/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&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;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// a noise gate must never be the thing that breaks a page&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;event&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;Three details in there I would keep if I wrote it again.&lt;/p&gt;

&lt;p&gt;The first line checks &lt;code&gt;event.event !== '$exception'&lt;/code&gt; and returns early. &lt;code&gt;before_send&lt;/code&gt; sees everything, including pageviews and clicks, so a filter written loosely here quietly eats your entire analytics feed instead of your error feed.&lt;/p&gt;

&lt;p&gt;The host check drops localhost, any &lt;code&gt;.localhost&lt;/code&gt; subdomain, bare IP addresses and preview deployments. That is the environment split I did not get for free.&lt;/p&gt;

&lt;p&gt;And the &lt;code&gt;catch&lt;/code&gt; is the one I care about most. This function runs inside the page, on every event, before anything is sent. If it throws, it throws in front of the visitor. A filter that exists to protect a dashboard is not allowed to be the reason a page stops working, so it fails open and lets the event through.&lt;/p&gt;

&lt;p&gt;The two regexes are the least durable part of this. They match strings that a browser vendor can change without telling anyone, and when that happens the noise comes back and I will have to read a digest carefully to notice. I would rather that than a filter loose enough to swallow something real.&lt;/p&gt;

&lt;p&gt;The number I get now is 30 exceptions smaller and it is entirely mine. That is the point of the exercise: not a better crash rate, a crash rate that is about my software.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://agentwire.kynth.studio/?utm_source=founder-devto&amp;amp;utm_medium=social" rel="noopener noreferrer"&gt;https://agentwire.kynth.studio/?utm_source=founder-devto&amp;amp;utm_medium=social&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>debugging</category>
      <category>analytics</category>
    </item>
    <item>
      <title>I shipped a download that iOS turned into a navigation</title>
      <dc:creator>Isaiah Kim</dc:creator>
      <pubDate>Fri, 14 Aug 2026 14:51:58 +0000</pubDate>
      <link>https://dev.to/kyisaiah47/i-shipped-a-download-that-ios-turned-into-a-navigation-4g0i</link>
      <guid>https://dev.to/kyisaiah47/i-shipped-a-download-that-ios-turned-into-a-navigation-4g0i</guid>
      <description>&lt;p&gt;I read a session from an iPhone where the visitor pressed an export button four times, and &lt;code&gt;$pageleave&lt;/code&gt; fired about 150 milliseconds after each press, with a reset page when they came back. Those four departures were the export itself.&lt;/p&gt;

&lt;p&gt;WebKit does not honour the &lt;code&gt;download&lt;/code&gt; attribute on a &lt;code&gt;blob:&lt;/code&gt; URL. On iOS and iPadOS, where every browser is WebKit underneath regardless of what it says on the icon, &lt;code&gt;a.download&lt;/code&gt; pointed at a blob is ignored and the click becomes a navigation to that blob. The page unloads. Whatever state produced the file goes with it.&lt;/p&gt;

&lt;p&gt;That pattern is everywhere in my code, so I went and fixed the place where it costs the most.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the page was holding
&lt;/h2&gt;

&lt;p&gt;PartsProof is a Cyber Resilience Act tool I'm building under Kynth, and its free surface is an SBOM generator. You give it a public GitHub repo and an email, and it returns a CycloneDX 1.5 document you can download.&lt;/p&gt;

&lt;p&gt;The work behind that is not cheap. &lt;code&gt;src/app/api/sbom/route.ts&lt;/code&gt; resolves the repo and its default branch, pulls the full recursive tree, filters that down to dependency manifests, then fetches and parses up to thirty of them. &lt;code&gt;MAX_MANIFESTS&lt;/code&gt; is 30, so the whole run is as many as thirty-two GitHub round-trips before a single component is counted.&lt;/p&gt;

&lt;p&gt;Because it takes real time, the route streams NDJSON and fires a stage as each piece finishes, with the measured elapsed per stage. The five ids live in one file, &lt;code&gt;src/lib/stages.ts&lt;/code&gt;, with a note on top I put there for myself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/* ⛔ Adding a label here does not create a stage. A stage exists when the route fires it. */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;StageId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;repo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tree&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;manifests&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bom&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lead&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So on a phone, the sequence was: wait through thirty-two round-trips, watch five stages report, get told how many distinct components came back, press download, and land on a blob viewer with the whole result gone. Pressing back gives you an empty form. The only way to see the number again is to run all of it again.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa6rigbd35dkes6a5jmle.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa6rigbd35dkes6a5jmle.png" alt="I shipped a download that iOS turned into a navigation — code" width="800" height="538"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The branch, and the iPad that says it is a Mac
&lt;/h2&gt;

&lt;p&gt;The fix in &lt;code&gt;src/components/Lookup.tsx&lt;/code&gt; is small. On WebKit mobile, stop pretending the anchor is a download and let it be what WebKit is going to make it anyway: a navigation, but into its own tab, where the document opens with a share sheet instead of destroying the page that produced it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ua&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;undefined&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userAgent&lt;/span&gt; &lt;span class="o"&gt;||&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;webkitMobile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="sr"&gt;/iP&lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;hone|od|ad&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ua&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;platform&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;MacIntel&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxTouchPoints&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;webkitMobile&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;aEl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;_blank&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;aEl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;noopener&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;aEl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nx"&gt;aEl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="nx"&gt;aEl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second clause of that test is the one I would have missed. An iPad running iPadOS reports &lt;code&gt;navigator.platform&lt;/code&gt; as &lt;code&gt;MacIntel&lt;/code&gt; and sends a desktop user agent string with no &lt;code&gt;iPad&lt;/code&gt; in it, so the regex alone reads it as a laptop and leaves it broken. &lt;code&gt;maxTouchPoints &amp;gt; 1&lt;/code&gt; is what separates the iPad from an actual MacIntel machine, and a Mac with a trackpad reports 0. It is a sniff, and I do not love that it is a sniff, but there is no feature query for "this browser ignores an attribute".&lt;/p&gt;

&lt;p&gt;I also considered the File System Access API and skipping the anchor entirely. Safari does not implement &lt;code&gt;showSaveFilePicker&lt;/code&gt;, so the fallback path is this same anchor, and adding a branch that only ever runs on the browsers already working buys nothing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fornexm063d7u4a2y0342.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fornexm063d7u4a2y0342.png" alt="I shipped a download that iOS turned into a navigation — architecture" width="800" height="980"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Revoking on the next line
&lt;/h2&gt;

&lt;p&gt;The other half of that commit is a bug that had been sitting there the whole time and would have been much harder to see.&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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;aEl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nx"&gt;aEl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="nx"&gt;aEl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&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="nf"&gt;revokeObjectURL&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="c1"&gt;// ← this&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;click()&lt;/code&gt; starts a save. It does not finish one. Revoking the object URL on the very next statement pulls the blob out from under a transfer that has only just been handed to the browser. On desktop with a small JSON body it wins the race essentially always, which is exactly why it survives review: it looks like tidy cleanup and it behaves like tidy cleanup right up until the document is large or the device is slow or the save goes through a share sheet.&lt;/p&gt;

&lt;p&gt;It now reads:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Revoking on the next line races the save the click just started; give it a window.&lt;/span&gt;
&lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;revokeObjectURL&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="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sixty seconds of one blob held in memory is not a leak worth optimising against a file that sometimes does not arrive.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc2pzxcw1wfhfooc15q6g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc2pzxcw1wfhfooc15q6g.png" alt="partsproof — live" width="800" height="508"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Saying "downloaded" when nothing downloaded
&lt;/h2&gt;

&lt;p&gt;There was one more thing wrong, and it took a second pass to notice. The success message was a single template string:&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="nf"&gt;setSbomMsg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`downloaded &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;repoName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-sbom.cdx.json.`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the WebKit path that sentence is false. Nothing landed in a downloads folder. The document opened in its own tab and is waiting for the reader to press Share. Telling someone a file was saved when it was not is the specific failure this panel exists to avoid, since the whole product is about producing a document you can hand to somebody.&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="nf"&gt;setSbomMsg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;webkitMobile&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;repoName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-sbom.cdx.json opened in a new tab — use Share to save it.`&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`downloaded &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;repoName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-sbom.cdx.json.`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The branch that changes behaviour and the branch that describes it have to be the same branch. Fixing the navigation without fixing the sentence would have left the tool confidently reporting a save on the one platform where it never performs one.&lt;/p&gt;

&lt;p&gt;None of this showed up in a build, a type check, or any gate I run against the deployed page. Blob downloads are one of the last things in a web app that no automated check I have will exercise, because the interesting part happens after the click, in the browser's own plumbing, on a platform I was not testing on. The telemetry found it, and it found it as four unexplained departures rather than as an error.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://partsproof.kynth.studio/?utm_source=founder-devto&amp;amp;utm_medium=social" rel="noopener noreferrer"&gt;https://partsproof.kynth.studio/?utm_source=founder-devto&amp;amp;utm_medium=social&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>ios</category>
      <category>react</category>
    </item>
    <item>
      <title>I built a Playwright gate that cried wolf 889 times</title>
      <dc:creator>Isaiah Kim</dc:creator>
      <pubDate>Tue, 11 Aug 2026 14:28:35 +0000</pubDate>
      <link>https://dev.to/kyisaiah47/i-built-a-playwright-gate-that-cried-wolf-889-times-3nd5</link>
      <guid>https://dev.to/kyisaiah47/i-built-a-playwright-gate-that-cried-wolf-889-times-3nd5</guid>
      <description>&lt;p&gt;On August 2nd I shipped a landing page whose headline was in the DOM at the right size, the right colour and the right position, and invisible. StoreReady's &lt;code&gt;data-animate="17g0wbg"&lt;/code&gt; resolved against motion data from a different capture, so the h1 held its start frame, opacity 0.001, permanently. Every gate I had passed the build.&lt;/p&gt;

&lt;p&gt;They passed because they all read source. &lt;code&gt;icons-gate&lt;/code&gt; greps the icon set, &lt;code&gt;mark-gate&lt;/code&gt; greps the lockup, &lt;code&gt;workbench check&lt;/code&gt; hashes the vendored files. All of them are decidable without a renderer, and there is no string to grep for a pixel that never got painted.&lt;/p&gt;

&lt;p&gt;So I wrote one that never reads a file. &lt;code&gt;ops/qa/render-gate.mjs&lt;/code&gt; in the workbench repo takes a URL, drives real Chrome, and asks six questions a screenshot answers: effective opacity down the whole ancestor chain, WCAG against the computed background, horizontal scroll from 320 to 1920, control labels breaking to a second line, headline and CTA above the fold at 1280x800, and content trapped past the start edge of its own scroll container. Each product's &lt;code&gt;scripts/deploy.sh&lt;/code&gt; runs it after the deploy, against the live host, and a failure is hard.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first sweep reported 889 invisible elements
&lt;/h2&gt;

&lt;p&gt;Essentially all of them were scroll-reveal sections below the fold, sitting at opacity 0 because nothing had asked them to appear yet. A gate that cries wolf 889 times is not a gate.&lt;/p&gt;

&lt;p&gt;The part I want to keep from that run is why I did not see it coming. My own shell's entrance runtime reveals on mount rather than on intersection, so the sites already on it scored honestly by accident, which is the worst way to be right. Pointing the same measurement at pages with intersection-driven reveals is what exposed the model.&lt;/p&gt;

&lt;p&gt;The fix is that the gate judges only what is on screen. It walks the page, probes at each stop, and merges by element plus text keeping the best reading of each one. An element off-screen at opacity 0 is not a defect. An element in front of the reader at opacity 0 is the StoreReady bug. One scroll position cannot tell those apart.&lt;/p&gt;

&lt;p&gt;Per-stop settle is 750ms, long enough for a reveal to finish rather than merely to start. At 320ms it kept catching FetchDue's sections at opacity 0.061, mid-fade, which reads as a defect and is not one.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa6rigbd35dkes6a5jmle.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa6rigbd35dkes6a5jmle.png" alt="I built a Playwright gate that cried wolf 889 times — code" width="800" height="538"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Two ways the walk still lied
&lt;/h2&gt;

&lt;p&gt;The first was arithmetic. The stops stepped a fixed 0.75 viewport down from the top and then took &lt;code&gt;.slice(0, 14)&lt;/code&gt; for the time budget, so on anything taller than about 9,450px at a 900px viewport the sweep just stopped and everything below was never judged at all.&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;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scrollHeight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scrollHeight&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;MAX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 14 stops at 750ms is the time budget&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;innerHeight&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.75&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;MAX&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;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Spreading the same budget over the real height means density degrades on a very long page instead of coverage vanishing. A missed defect in the last third is worse than a coarser walk.&lt;/p&gt;

&lt;p&gt;The second is the interesting one. On a long page an element can be in front of the reader at exactly one stop. If that stop catches it entering at the bottom edge, or partway through a stagger, then the best-reading merge has only the bad reading to keep. The finding looks real, reproduces across runs, and describes nothing anyone would ever see. On QuorumFile that condemned three cells of a seventeen-row table, all three at opacity 1 the moment you scroll to them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Look at it again, on purpose
&lt;/h2&gt;

&lt;p&gt;Anything the walk condemns now gets one more measurement, taken deliberately.&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;condemned&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;opacity&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;OPACITY_FLOOR&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;decorative&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clipped&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;for &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;t&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;condemned&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;top&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;scrollTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;top&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;innerHeight&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;top&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1400&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;fresh&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;probeText&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;better&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fresh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sel&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sel&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;inFold&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;better&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;better&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;opacity&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;better&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;pxRatio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;OPACITY_FLOOR&lt;/code&gt; is 0.08. Two details in there cost me something. Only condemned runs are re-measured, so a clean page pays nothing for this, which is what makes a 1400ms settle affordable at all. And the fresh reading replaces the stale one whole, rather than just its opacity. A reading taken while an element was at opacity 0 also carries the contrast it had against whatever was behind it, which is 1:1 because no ink was painted. Adopting the opacity alone clears the visibility finding and hands that dead 1:1 straight to the contrast check. One false finding becomes a different false finding.&lt;/p&gt;

&lt;p&gt;A finding that survives being scrolled to and stared at for a second is the real bug. One that does not was never a bug.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fornexm063d7u4a2y0342.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fornexm063d7u4a2y0342.png" alt="I built a Playwright gate that cried wolf 889 times — architecture" width="800" height="980"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What it caught
&lt;/h2&gt;

&lt;p&gt;On August 8th, on a QuorumFile deploy, it failed on the pricing block's document preview: painted at opacity 0, and not marked decorative.&lt;/p&gt;

&lt;p&gt;The cause was not in the pricing block. &lt;code&gt;ScrollReveals&lt;/code&gt; collects auto-fade candidates from &lt;code&gt;main.page &amp;gt; *&lt;/code&gt; and walks down, so any element added to any section inherits a 0-opacity start state whether it asked for one or not. Mine carried no &lt;code&gt;data-reveal&lt;/code&gt; and got one anyway. The fix was to put it in the runtime's skip list next to &lt;code&gt;.header&lt;/code&gt;, for the same reason the header is there.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SKIP&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.header, .trail, .nav-list, .nav-dropdown-list, .sheet-overlay, .kd-artefact-link&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The gate cannot decide this on its own, and it does not try. My footers carry a brand wordmark at opacity 0.07, which is a watermark and correct. StoreReady's hero sat at 0.001, which was a bug and shipped. Measured, those are the same observation, so the rule is that text below the floor has to declare itself with &lt;code&gt;aria-hidden="true"&lt;/code&gt; or it counts as a failure. A heading is never decoration.&lt;/p&gt;

&lt;p&gt;I had opened that pricing block in a browser more than once before the deploy. The gate found it, I did not.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>playwright</category>
      <category>testing</category>
      <category>css</category>
    </item>
    <item>
      <title>The early return that shipped a false all-clear</title>
      <dc:creator>Isaiah Kim</dc:creator>
      <pubDate>Sun, 09 Aug 2026 14:25:28 +0000</pubDate>
      <link>https://dev.to/kyisaiah47/the-early-return-that-shipped-a-false-all-clear-1mfg</link>
      <guid>https://dev.to/kyisaiah47/the-early-return-that-shipped-a-false-all-clear-1mfg</guid>
      <description>&lt;p&gt;I typed the word "construction" into my own lookup tool and it answered:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;All 200 qualifying contracts under null have an approved Affidavit of Wages Paid on file.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Two things wrong in one sentence, and the more interesting one is that nothing had been checked against the affidavit file at all. The route had returned before it got there.&lt;/p&gt;

&lt;p&gt;The product is HeldBack, a free retainage blocker check I'm building under Kynth. You give it a company name or a 9-digit UBI, and it reads two of Washington's published Socrata files live: the Statements of Intent a contractor files before a public works job, and the Affidavits of Wages Paid filed after. A job with an intent and no matching approved affidavit is a job whose retainage, capped at 5% under RCW 60.28.011, is likely still sitting with the awarding agency.&lt;/p&gt;

&lt;h2&gt;
  
  
  The floors are most of the logic
&lt;/h2&gt;

&lt;p&gt;The naive version of this is a set difference: intents minus affidavits. That version is wrong in a way that destroys the product's credibility on first contact, because a job that started six months ago has no affidavit for the boring reason that the work isn't finished. Report that as frozen money and you've told a contractor something they know to be false about a job they remember clearly.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;src/app/api/lookup/route.ts&lt;/code&gt; filters before it compares:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MIN_CONTRACT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5000&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;MIN_AGE_MONTHS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;24&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;priced&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;intents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cntrct_amt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;MIN_CONTRACT&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;tooRecentOrUndated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;expected_start_dt&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;At or under $5,000 the affidavit is folded into the combined intent form and never appears separately, so those rows are false positives by construction. Undated rows can't be aged at all, so &lt;code&gt;tooRecentOrUndated&lt;/code&gt; returns true for them and they drop out too.&lt;/p&gt;

&lt;p&gt;Both floors are correct. The bug was what happens when they eat everything.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa6rigbd35dkes6a5jmle.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa6rigbd35dkes6a5jmle.png" alt="The early return that shipped a false all-clear — code" width="800" height="538"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The return that skipped the comparison
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;priced&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;term&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;company&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;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="na"&gt;searched&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;intents&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;searched&lt;/code&gt; is the number the UI prints back. Everywhere else in the file it means "contracts we actually compared against the affidavit file". Here it meant "rows the first query returned", because at the moment I wrote the line those felt like the same quantity.&lt;/p&gt;

&lt;p&gt;They're the same quantity right up until a search term is generic. The intents query is capped at &lt;code&gt;$limit: '200'&lt;/code&gt;, and a common word matches far more than 200 filings across the whole state, none of them one company. Nearly all of them fail the $5,000 floor or the 24-month floor. &lt;code&gt;priced&lt;/code&gt; comes back empty, the route returns here, and the client is handed &lt;code&gt;searched: 200&lt;/code&gt; with an empty &lt;code&gt;rows&lt;/code&gt; array. The panel reads that as 200 contracts checked and zero blocked.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;null&lt;/code&gt; in the sentence has the same root. Company name is resolved as &lt;code&gt;priced[0]?.companyname&lt;/code&gt;, and on this path &lt;code&gt;priced&lt;/code&gt; is empty.&lt;/p&gt;

&lt;p&gt;The fix splits the count into two:&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;priced&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="cm"&gt;/* `searched` IS THE QUALIFYING COUNT, NOT THE FILING COUNT, and this line used to return
   * `intents.length` for it. [...] `found` is the honest count for this case: filings
   * located, none of them judgeable. */&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;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;term&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;company&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;intents&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nx"&gt;companyname&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="na"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
      &lt;span class="na"&gt;searched&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;found&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;intents&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="na"&gt;cleared&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;totalHeld&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;searched: 0&lt;/code&gt; is now literally true. Nothing was searched against the affidavit file.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fornexm063d7u4a2y0342.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fornexm063d7u4a2y0342.png" alt="The early return that shipped a false all-clear — architecture" width="800" height="980"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Two branches for three outcomes
&lt;/h2&gt;

&lt;p&gt;The other half was in &lt;code&gt;src/components/HeroLookup.tsx&lt;/code&gt;, which had an empty state and a clear state and nothing else. Any result with &lt;code&gt;rows.length === 0&lt;/code&gt; fell into "nothing appears to be blocked". There was no way to express "we found filings and none of them can be judged yet", so that case borrowed the reassuring one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;{(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;found&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
  &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;No public works contracts found under that name.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searched&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Nothing here can be judged yet.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Nothing appears to be blocked.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The middle branch explains itself in the body copy: every match is under $5,000 or started inside the last 24 months, a job that recent has no affidavit because the work is not finished, and search the exact legal entity name or the UBI to narrow it. That last part matters more than the correction does. The person who typed a generic term wanted an answer about a company, and now the panel tells them how to get one.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc2pzxcw1wfhfooc15q6g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc2pzxcw1wfhfooc15q6g.png" alt="heldback — live" width="800" height="508"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The same short-circuit, one component away
&lt;/h2&gt;

&lt;p&gt;The wait panel streams NDJSON and fires a stage as each of the route's four real pieces of work finishes, with the measured elapsed printed per stage. On a real company that reads 587ms, 587ms, 756ms, 757ms, and the third line says "53 of 74 have an approved Affidavit on file". Two of the four stages are live network calls, which is where the seconds go.&lt;/p&gt;

&lt;p&gt;The orb at the head of that panel spun forever on the short-circuit path, because it asked whether all four stages were done. On an early return two of them never fire, so the answer is permanently no, and the orb kept spinning next to a finished answer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;running&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;running&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;phase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;idle&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;idle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;failed&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;running&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;running&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;done&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What the orb reports is whether anything is still happening, not whether every stage reported.&lt;/p&gt;

&lt;p&gt;Neither of these turned up in reading. I found both by typing a word into the live thing and looking at the sentence it gave back, which is also the exact sentence a first-time visitor would be most likely to see, since a generic term is what you type before you know what the tool wants.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>typescript</category>
      <category>nextjs</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>I paged a table with no ORDER BY and lost 2,797 rows</title>
      <dc:creator>Isaiah Kim</dc:creator>
      <pubDate>Fri, 07 Aug 2026 14:24:14 +0000</pubDate>
      <link>https://dev.to/kyisaiah47/i-paged-a-table-with-no-order-by-and-lost-2797-rows-39n2</link>
      <guid>https://dev.to/kyisaiah47/i-paged-a-table-with-no-order-by-and-lost-2797-rows-39n2</guid>
      <description>&lt;p&gt;I ran a verification pass over 22,313 rows, it reported 22,313 rows, and I read that number as proof it had checked all of them. It had checked about 19,500 and looked at some of those twice.&lt;/p&gt;

&lt;p&gt;The product is BlockDex, an item-level index of public shadcn registries I'm building under Kynth. Every item page can render a Live preview — an iframe of the component's own docs page — under a caption that says this is the component running, not a screenshot of it. Nothing checked that claim. &lt;code&gt;preview_url&lt;/code&gt; was a URL &lt;em&gt;shape&lt;/em&gt; learned once per registry from a single sample item name, accepted on &lt;code&gt;status === 200 &amp;amp;&amp;amp; text.length &amp;gt; 500&lt;/code&gt;, then stamped onto every item in that registry.&lt;/p&gt;

&lt;p&gt;Two things went wrong with that. The last docs pattern tried is &lt;code&gt;{origin}/r/{name}&lt;/code&gt;, which on a large class of registries is the registry-item JSON route — the same route the install command reads. A 4KB JSON body clears a length test, the request sent &lt;code&gt;accept: 'text/html,*/*'&lt;/code&gt; so the &lt;code&gt;*/*&lt;/code&gt; forced nothing, and an API route sends no &lt;code&gt;X-Frame-Options&lt;/code&gt;, so the framability check returned true and marked that registry the most embeddable kind there is. One registry framed its own JSON and Chrome rendered its JSON viewer inside the panel. Separately, a shape learned from one item name does not hold for 7,794 items, so most of the rest resolved to the registry's 404.&lt;/p&gt;

&lt;p&gt;I fetched all 22,313 stored preview URLs. 7,382 were not component pages: 4,704 hard 404s, 1,588 JSON documents, 691 soft 404s, 399 server errors.&lt;/p&gt;

&lt;h2&gt;
  
  
  The count that matched
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ops/verify-previews.mjs&lt;/code&gt; pulls its work list through a paging helper that walks a table with limit/offset — 23 pages at a thousand rows each. The query had no &lt;code&gt;ORDER BY&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;An offset over an unordered query is not a stable window. Postgres is free to return the rows in a different order for each page, so between page 4 and page 5 a row can shift across the offset boundary and be handed to you twice while another row slides the other way and never arrives. The totals still add up, because you asked for 23 slices of a thousand and got them.&lt;/p&gt;

&lt;p&gt;I found it the slow way. After the pass finished and wrote its updates, &lt;code&gt;shadcn-io/area-interactive&lt;/code&gt; still had a Live preview panel mounted over a 404. Its row was in the set the paging skipped.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ⛔ `order=id.asc` IS LOAD BEARING. `selectAll` pages with limit/offset, and an offset over a&lt;/span&gt;
&lt;span class="c1"&gt;// query with no ORDER BY is not a stable window — Postgres is free to return the rows in a&lt;/span&gt;
&lt;span class="c1"&gt;// different order for each of the 23 pages, so a row can arrive twice and another never arrive&lt;/span&gt;
&lt;span class="c1"&gt;// at all. The first full run pulled exactly 22,313 rows, which looked like complete coverage&lt;/span&gt;
&lt;span class="c1"&gt;// and was not.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;selectAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;blockdex_items&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s2"&gt;`select=id,registry_slug,name,preview_url,preview_embeddable,docs_url,status&amp;amp;preview_url=not.is.null&amp;amp;order=id.asc&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second pass with the sort key found 17,728 rows still carrying a preview URL where 14,931 were expected, and unpublished 1,603 more. A third pass agreed with the second: 16,121 of 16,125 verified, the remaining four a transient 502.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa6rigbd35dkes6a5jmle.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa6rigbd35dkes6a5jmle.png" alt="I paged a table with no ORDER BY and lost 2,797 rows — code" width="800" height="538"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The fan-out that wasn't doing anything
&lt;/h2&gt;

&lt;p&gt;The same file had a second bug I'd have shrugged at if I hadn't been staring at it. Global concurrency is 40. The real limiter is a per-host cap of 3, which is what keeps a registry running on one small deployment from getting hammered. And rows come back from PostgREST grouped by registry.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;shadcn-io&lt;/code&gt; is 7,794 contiguous rows on one host. So all 40 workers picked up shadcn.io items, 37 of them parked on that host's semaphore, and the other 127 hosts in the corpus sat idle. The run did 1,500 items in eleven minutes and was on course for about three hours.&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;byHost&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;Map&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;rows&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;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;?.(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;preview_url&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;host&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;registry_slug&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;byHost&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;byHost&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;
  &lt;span class="nx"&gt;byHost&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&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;queues&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;byHost&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;()];&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;work&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;work&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;rows&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="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &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;q&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;queues&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;q&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="nx"&gt;work&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&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;Round-robin by host, then hand that array to the pool. Wall clock becomes the slowest single host instead of the sum of all of them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fornexm063d7u4a2y0342.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fornexm063d7u4a2y0342.png" alt="I paged a table with no ORDER BY and lost 2,797 rows — architecture" width="800" height="980"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What counts as a missing page
&lt;/h2&gt;

&lt;p&gt;The check itself, in &lt;code&gt;ops/lib/preview.mjs&lt;/code&gt;, is deliberately narrow. My first version read the first 4KB of the body looking for not-found markers, and on a 400-URL sample it condemned two real, rendering component pages that happened to carry the string &lt;code&gt;404&lt;/code&gt; in a nav and an inlined script. Deleting a working item from the index is the worse outcome, because a 404 that slips through is visible on the page and a missing item is not. So the test only reads &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt;, which is the one place a framework's not-found page reliably announces itself — fumadocs writes "Component not found", Next writes "404".&lt;/p&gt;

&lt;p&gt;A failure clears &lt;code&gt;docs_url&lt;/code&gt; alongside &lt;code&gt;preview_url&lt;/code&gt;, too. Downgrading a dead frame to a "See it running" link is the same claim in smaller type.&lt;/p&gt;

&lt;p&gt;Final state: 11,502 items carry a verified Live preview, 2,369 a verified link, 45,343 neither, 8,985 unpublished. The &lt;code&gt;order=id.asc&lt;/code&gt; has a comment sitting on top of it, because the version without it doesn't fail — it returns a number that matches the table.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>sql</category>
      <category>node</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I rewrote 1.2M Postgres rows a run to change nothing</title>
      <dc:creator>Isaiah Kim</dc:creator>
      <pubDate>Wed, 05 Aug 2026 14:25:17 +0000</pubDate>
      <link>https://dev.to/kyisaiah47/i-rewrote-12m-postgres-rows-a-run-to-change-nothing-1lno</link>
      <guid>https://dev.to/kyisaiah47/i-rewrote-12m-postgres-rows-a-run-to-change-nothing-1lno</guid>
      <description>&lt;p&gt;I went looking for why one Supabase project kept draining its Disk IO Budget, and the answer was a Python script rewriting 1,206,313 rows to change nothing.&lt;/p&gt;

&lt;p&gt;The product is GoodStanding, a free EIN diagnostic I'm building under Kynth. You type in a nonprofit's EIN and it tells you whether that org is on the IRS Auto-Revocation List or on California's "May Not Operate or Solicit" list. The table behind it, &lt;code&gt;gs_orgs&lt;/code&gt;, is fed by &lt;code&gt;ops/src/ingest-registries.py&lt;/code&gt;, which pulls two files: the IRS bulk revocation zip, and the CA AG CSV that refreshes on the 1st and 3rd Wednesday.&lt;/p&gt;

&lt;p&gt;Both feeds republish their entire corpus every cycle. A few thousand rows actually move. The ingest didn't know that, so every run it upserted all 1.2 million.&lt;/p&gt;

&lt;p&gt;Measured against the stats reset on May 22, that one script was the largest consumer of disk IO on the whole project: 24,721 upsert statements, 10.2 GB read from disk, 3h17m of database time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fingerprint the row, not the feed
&lt;/h2&gt;

&lt;p&gt;The obvious fix is to diff against what's already in the table, but reading 1.2M rows back to decide whether to write 1.2M rows is the same IO with extra steps. What made a cheaper option available is a property of this particular table: &lt;code&gt;ingest-registries.py&lt;/code&gt; is the only writer to &lt;code&gt;gs_orgs&lt;/code&gt;. Nothing else touches it. So a local record of what the script last wrote is authoritative, and no read is needed at all.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fingerprint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Stable 16-hex digest of the columns we actually write.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;blob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\x1f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;''&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;COLUMNS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;blake2b&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;digest_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hashing &lt;code&gt;COLUMNS&lt;/code&gt; and not the parsed source record matters. The feeds carry fields the table doesn't store, and a whitespace change in one of those would otherwise flag a row as changed and buy back the write I'm trying to avoid.&lt;/p&gt;

&lt;p&gt;The selection is then four lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;fresh&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ein&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;fingerprint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;ein&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;merged&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;full&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;merged&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;seen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_fingerprints&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;ein&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;merged&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ein&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;fresh&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ein&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things about the cache file are deliberate. Missing it isn't an error — &lt;code&gt;load_fingerprints()&lt;/code&gt; swallows &lt;code&gt;FileNotFoundError&lt;/code&gt; and returns &lt;code&gt;{}&lt;/code&gt;, every row mismatches, and you get a full rewrite. The degraded state is the old behaviour, which is slow and correct. And it's written once, after the last batch lands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Written only after every batch landed: a crash mid-run leaves the cache
# untouched so the next run retries the same rows.
&lt;/span&gt;&lt;span class="nf"&gt;save_fingerprints&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fresh&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Saving per batch would have been the natural thing to write, and it's the version where a timeout halfway through convinces the next run that rows it never wrote are already durable.&lt;/p&gt;

&lt;p&gt;Run against that day's corpus: 0 of 1,206,313 rows flagged as changed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa6rigbd35dkes6a5jmle.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa6rigbd35dkes6a5jmle.png" alt="I rewrote 1.2M Postgres rows a run to change nothing — code" width="800" height="538"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The indexes nobody had ever used
&lt;/h2&gt;

&lt;p&gt;While I was in the stats I checked which indexes on &lt;code&gt;gs_orgs&lt;/code&gt; had been scanned. Three had a scan count of zero: &lt;code&gt;gs_orgs_name_trgm&lt;/code&gt;, &lt;code&gt;gs_orgs_posting_idx&lt;/code&gt;, &lt;code&gt;gs_orgs_ca_status_idx&lt;/code&gt;. Never once, and maintained on every one of those upserts.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;gs_orgs_name_trgm&lt;/code&gt; is the one worth sitting with. Despite the name it wasn't a trigram index at all — it was a btree on &lt;code&gt;lower(name) text_pattern_ops&lt;/code&gt;. The name search the app actually issues is in &lt;code&gt;src/app/api/lookup/route.ts:65&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;rows&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;sb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`gs_orgs?name=ilike.*&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;safe&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;*&amp;amp;...&amp;amp;limit=8`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Leading wildcard. A &lt;code&gt;text_pattern_ops&lt;/code&gt; btree can serve a prefix match and nothing else, so that query had been sequential-scanning the whole table since the day it shipped, next to an index named as though it were handling it. Dropping all three took the table's index footprint from 198 MB to 72 MB.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fornexm063d7u4a2y0342.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fornexm063d7u4a2y0342.png" alt="I rewrote 1.2M Postgres rows a run to change nothing — architecture" width="800" height="980"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The write was the smaller problem
&lt;/h2&gt;

&lt;p&gt;The next day I made the bigger call, and it went the other way from where I started. The project had been moved onto its own Supabase instance for blast-radius isolation, at $10/month. For one product that isn't a trade worth making — so instead of paying to keep 1.2M rows somewhere safe, I asked what the product can actually do with them.&lt;/p&gt;

&lt;p&gt;An org revoked in 2013 is looking at a full 1023 re-application. GoodStanding sells a reinstatement Cure Pack; that org isn't a buyer, and its row is heap, index and WAL on a database the rest of the estate shares. California is different — AB 488 blocks a listed org's donation pages regardless of how old the listing is.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;worth_keeping&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ca_reg_no&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ca_registry_status&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;                      &lt;span class="c1"&gt;# CA-listed: AB 488 buyer at any age
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;reinstatement_date&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;                     &lt;span class="c1"&gt;# back in good standing; "clear" is correct
&lt;/span&gt;    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;revocation_date&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;keep_since&lt;/span&gt; \
        &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;revocation_posting_date&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;keep_since&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;331,377 rows of 1,206,313. 261 MB down to 81 MB, back on the shared database, and the outreach delta came through unchanged: 716 IRS postings in the last 45 days, 8,219 CA delinquent or suspended.&lt;/p&gt;

&lt;p&gt;There's a trap in that, and it's the reason the comment block above &lt;code&gt;worth_keeping&lt;/code&gt; is longer than the function. &lt;code&gt;/api/lookup&lt;/code&gt; reads "no row" as clear — the table only ever holds orgs that are on a list, so absence is the good answer. Drop a still-revoked 2013 org and the tool tells that nonprofit it's fine.&lt;/p&gt;

&lt;p&gt;So the cutoff is load-bearing on a claim in the UI, and the two are now written down together. &lt;code&gt;GS_KEEP_REVOKED_SINCE&lt;/code&gt; defaults to &lt;code&gt;2020-01-01&lt;/code&gt;, the route file carries a retention warning at the top, and the clear-state copy in &lt;code&gt;EinLookup.tsx&lt;/code&gt; says "No IRS auto-revocation posted since 2020" rather than the unqualified all-clear it used to say. Widening the cutoff means widening that sentence.&lt;/p&gt;

&lt;p&gt;What I'd been treating as a performance problem had a correctness statement buried in it. The rows I was paying to rewrite twice a month were also the rows that decided what the product was allowed to promise, and I only found that by going looking for the disk IO.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>supabase</category>
      <category>python</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>unstable_cache stored my outage for a full hour</title>
      <dc:creator>Isaiah Kim</dc:creator>
      <pubDate>Sun, 02 Aug 2026 14:22:41 +0000</pubDate>
      <link>https://dev.to/kyisaiah47/unstablecache-stored-my-outage-for-a-full-hour-3njm</link>
      <guid>https://dev.to/kyisaiah47/unstablecache-stored-my-outage-for-a-full-hour-3njm</guid>
      <description>&lt;p&gt;I shipped a directory page that told visitors there were zero listings while rendering a grid of 48 of them. Fixed it. Then, about nine hours later, shipped a different bug with the exact same symptom, in the exact same function, for a completely unrelated reason.&lt;/p&gt;

&lt;p&gt;The product is SkillWorks, a scored directory of Claude Code skills, subagents, plugins and marketplace repos I'm building under Kynth. It reads from Supabase over PostgREST, and it needs a lot of row counts: total listings, counts per kind, how many are broken, how many have tracked installs. Counting rows without pulling them is the one thing PostgREST makes genuinely cheap — ask for &lt;code&gt;Prefer: count=exact&lt;/code&gt; with &lt;code&gt;Range: 0-0&lt;/code&gt; and the total comes back in the &lt;code&gt;Content-Range&lt;/code&gt; response header. No rows in the body at all.&lt;/p&gt;

&lt;p&gt;That header is where both bugs live.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first zero: the header didn't survive the cache
&lt;/h2&gt;

&lt;p&gt;Original version, in &lt;code&gt;src/lib/db.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;countRows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;col&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;'&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="kr"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&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="s2"&gt;/rest/v1/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;?&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;select=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;col&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;limit=1`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;apikey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;Prefer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;count=exact&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;Range&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0-0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;next&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;revalidate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;REVALIDATE&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;range&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-range&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;range&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;range&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next's fetch cache stores the response &lt;em&gt;body&lt;/em&gt;. On a cache hit it hands you back a &lt;code&gt;Response&lt;/code&gt; assembled with a synthetic header set, and &lt;code&gt;content-range&lt;/code&gt; is not in it. So every cache hit read &lt;code&gt;null&lt;/code&gt; and fell through to &lt;code&gt;0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;What kept this hidden for a while is that statically prerendered routes were fine. Their one fetch happens at build time and is always a miss, so the real header is right there. Only routes rendering at runtime hit the cache, and those were the ones shipping "0 indexed" next to a full grid. &lt;code&gt;/marketplaces&lt;/code&gt; read "None of the 0 marketplaces".&lt;/p&gt;

&lt;p&gt;The fix is to stop caching a thing whose value lives outside the cached payload. Make the fetch &lt;code&gt;cache: 'no-store'&lt;/code&gt; and wrap the whole function in &lt;code&gt;unstable_cache&lt;/code&gt;, which memoises the &lt;em&gt;return value&lt;/em&gt; instead. Still one request an hour, but now what's stored is a parsed number, which survives being stored.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fornexm063d7u4a2y0342.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fornexm063d7u4a2y0342.png" alt="unstable_cache stored my outage for a full hour — architecture" width="800" height="980"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The second zero: a failed read stored as an answer
&lt;/h2&gt;

&lt;p&gt;That was August 1st, early afternoon. That evening Supabase had an outage, and I watched &lt;code&gt;/skills&lt;/code&gt; render "Search all 0 listings" above cards that looked completely normal.&lt;/p&gt;

&lt;p&gt;The cards were fine because they come from Next's fetch cache and it still had them. The counts came through the new uncached path, hit a dead index, and returned &lt;code&gt;0&lt;/code&gt; — which is the last line of the function above, doing exactly what it was written to do. And &lt;code&gt;unstable_cache&lt;/code&gt; dutifully stored that &lt;code&gt;0&lt;/code&gt; for the full &lt;code&gt;REVALIDATE&lt;/code&gt; window, which is 3600 seconds.&lt;/p&gt;

&lt;p&gt;That's the part that actually bothered me. The outage was maybe twenty minutes. The wrong number would have outlived it by forty. The database could be answering perfectly and the site would still be advertising zero listings, with no error, no degraded state, nothing to look at. A directory that confidently reports emptiness is worse off than one that's visibly broken, because nobody goes looking.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;|| 0&lt;/code&gt; was defensive code from the first bug that quietly became the second one. Returning a fallback and memoising a fallback are different operations, and one function was doing both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Throw inside the cache, degrade outside it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;countRows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;col&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;'&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="kr"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;unstable_cache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* … */&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;no-store&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;range&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-range&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="c1"&gt;// THROW rather than return 0 when the index cannot be reached.&lt;/span&gt;
        &lt;span class="c1"&gt;// `unstable_cache` stores whatever the function RETURNS, for a full hour.&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;range&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`count &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;range&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
        &lt;span class="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="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isFinite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`count &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: unparseable content-range`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;total&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;count&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;col&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;revalidate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;REVALIDATE&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="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// 0 is still what the caller gets — a directory page that 500s over a stat line is worse&lt;/span&gt;
    &lt;span class="c1"&gt;// than one that under-reports for a few seconds. The difference is this 0 is NOT cached.&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The caller still gets &lt;code&gt;0&lt;/code&gt;. A browse page shouldn't throw a 500 because a stat line in a search placeholder couldn't be computed. But the fallback now lives outside the memo, so nothing gets written, the next request re-reads, and the wrong number lasts exactly as long as the outage does and not one second longer.&lt;/p&gt;

&lt;p&gt;The same weekend, in a different product, I hit the same shape from the other direction. ListRun has pages where the row &lt;em&gt;is&lt;/em&gt; the page — a directory detail, a run, a paid receipt — and they all called a helper that returned &lt;code&gt;null&lt;/code&gt; on any failed read, then called &lt;code&gt;notFound()&lt;/code&gt;. Which means a slow database told a buyer their run does not exist, and on a cached render it baked that 404 in. The fix there was &lt;code&gt;apiRequired&lt;/code&gt; in &lt;code&gt;src/lib/site.ts&lt;/code&gt;: return &lt;code&gt;null&lt;/code&gt; only on an actual 404 from the route, throw on anything else, so &lt;code&gt;notFound()&lt;/code&gt; is always a statement about the row.&lt;/p&gt;

&lt;p&gt;Both are the same mistake. A read that fails and a read that legitimately returns nothing produce the same value, and then something downstream — a cache, a router — treats that value as established fact and keeps it around. The failure gets laundered into data on the way out.&lt;/p&gt;

&lt;p&gt;I've stopped trusting any fallback that sits inside a memo boundary. If a function can be wrong and can be stored, those two facts need to be separated by a &lt;code&gt;throw&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>caching</category>
      <category>webdev</category>
      <category>debugging</category>
    </item>
    <item>
      <title>Custom element FOUC: the rule that hid nothing</title>
      <dc:creator>Isaiah Kim</dc:creator>
      <pubDate>Fri, 31 Jul 2026 14:21:44 +0000</pubDate>
      <link>https://dev.to/kyisaiah47/custom-element-fouc-the-rule-that-hid-nothing-5601</link>
      <guid>https://dev.to/kyisaiah47/custom-element-fouc-the-rule-that-hid-nothing-5601</guid>
      <description>&lt;p&gt;I shipped a CSS rule that could not possibly do its job, looked at the page, saw the flash still happening, and spent longer than I want to admit tuning the wrong thing.&lt;/p&gt;

&lt;p&gt;The product is Tearline, a small zero-dependency web component I'm building under Kynth. You wrap any HTML in one tag and it renders as a thermal receipt you can export as a PNG:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;tear-line&lt;/span&gt; &lt;span class="na"&gt;barcode=&lt;/span&gt;&lt;span class="s"&gt;"047320260726"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;MERIDIAN&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Coffee &lt;span class="ni"&gt;&amp;amp;amp;&lt;/span&gt; Provisions&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/tear-line&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Custom elements have a well-known ugly window. Until &lt;code&gt;customElements.define&lt;/code&gt; runs, those children are just ordinary HTML sitting in the page: a bare &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; and some &lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt;s in whatever the host's body font is, at a completely different height to the receipt that eventually replaces them. On a cold load that's visible long enough to read, and then the layout jumps.&lt;/p&gt;

&lt;p&gt;The standard fix is &lt;code&gt;:not(:defined)&lt;/code&gt;. &lt;code&gt;:defined&lt;/code&gt; only starts matching once the element upgrades, so hiding on &lt;code&gt;:not(:defined)&lt;/code&gt; is exactly the window you want to cover.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I put it, and why that was wrong
&lt;/h2&gt;

&lt;p&gt;I didn't want every consumer of the component to have to remember a stylesheet rule. So I had &lt;code&gt;tearline.js&lt;/code&gt; inject it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/tearline.js — what I shipped first&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;undefined&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tear-line-fouc&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;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;style&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tear-line-fouc&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tear-line:not(:defined){visibility:hidden}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;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;s&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;customElements&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tear-line&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;TearLine&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There was a second reason I liked this, and it's a real one: if the script never loads at all, the rule never lands either, so the content stays visible instead of being hidden forever by a stylesheet with nothing left to reveal it. Failure mode handled. Nice.&lt;/p&gt;

&lt;p&gt;Except the whole thing is inert. That script running is the exact moment &lt;code&gt;:not(:defined)&lt;/code&gt; stops matching. The rule and the definition arrive in the same tick, so the rule only ever exists after the window it was written to cover. It hid nothing during the only period that mattered.&lt;/p&gt;

&lt;p&gt;I found this by throttling &lt;code&gt;tearline.js&lt;/code&gt; to a 1.2s delay in devtools, which made the unstyled window long enough to actually stare at. Before that I'd been reading the rule, agreeing with the rule, and assuming the flash was coming from somewhere else.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa6rigbd35dkes6a5jmle.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa6rigbd35dkes6a5jmle.png" alt="Custom element FOUC: the rule that hid nothing — code" width="800" height="538"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule has to be present on the first frame
&lt;/h2&gt;

&lt;p&gt;Which means it has to be in the page's stylesheet, not in the component. It's now in &lt;code&gt;src/app/template.css&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.js&lt;/span&gt; &lt;span class="nt"&gt;tear-line&lt;/span&gt;&lt;span class="nd"&gt;:not&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;:defined&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;visibility&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;.js&lt;/code&gt; prefix is how I keep the property I liked about self-injection. An inline script in &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; (&lt;code&gt;src/app/layout.tsx:70&lt;/code&gt;) does one thing:&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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's inline and synchronous, so the class is on &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt; before anything can match against it. If scripting is off or the bundle dies, the class is never stamped, the rule never applies, and you get the raw markup rather than a permanently blank box. Same guarantee, moved to a place that's actually there on the first paint.&lt;/p&gt;

&lt;p&gt;This was the second time on this project I'd hit the same shape. The landing page's scroll reveals had it too: &lt;code&gt;ScrollReveals&lt;/code&gt; was setting each element's hidden opacity from a &lt;code&gt;useEffect&lt;/code&gt;, which runs after hydration, which is after the browser has already painted the server HTML fully visible. Every load was doing paint the whole page, blank the parts that reveal, fade them back in. No amount of tuning the IntersectionObserver fixes that, because the wrong frame has already been shown by the time the JS exists. Hidden state belongs in CSS; the runtime should only ever move things toward visible, which is a direction it can't be late for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hiding is only half of the flash
&lt;/h2&gt;

&lt;p&gt;Once nothing was visible during the upgrade, the page still moved on arrival, because reserving zero space and then dropping in a full receipt is its own layout shift. Two more pieces:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.panel-result&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
&lt;span class="nc"&gt;.tl-stage&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;min-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;260px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nt"&gt;tear-line&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;data-ready&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tl-arrive&lt;/span&gt; &lt;span class="m"&gt;0.4s&lt;/span&gt; &lt;span class="n"&gt;cubic-bezier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.22&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.36&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;both&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;260px is a guess. Being roughly right and stable beats being exactly right one frame late.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;data-ready&lt;/code&gt; attribute took one more correction. My first version set it at the top of the render path, and the receipt faded in half-drawn: paper present, tear and barcode not yet. The clip path and the barcode are both built from the same seeded stream and get applied later in the same method, so "ready" had to mean after all three:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The host page fades in on [data-ready]. Set on the next frame, after the&lt;/span&gt;
&lt;span class="c1"&gt;// paper, the tear and the barcode are all in place.&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hasAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data-ready&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data-ready&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The attribute is only set once, so re-renders (changing &lt;code&gt;seed&lt;/code&gt;, changing &lt;code&gt;width&lt;/code&gt;) don't re-trigger the entrance animation. And the whole thing is dropped under &lt;code&gt;prefers-reduced-motion: reduce&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Verified the same way I found it: throttle the script to 1.2s, watch the whole pre-upgrade window. Hidden throughout, no unstyled text at any point, no shift when it arrives.&lt;/p&gt;

&lt;p&gt;The part I'd keep from this: a rule whose job is to cover the window before a script runs cannot be delivered by that script. That sounds obvious written down. It did not look obvious in the diff, because the code was correct, well-commented, and answering a question about a completely different failure than the one I had.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>webcomponents</category>
      <category>css</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Ranking a dashboard by funnel depth, not counts</title>
      <dc:creator>Isaiah Kim</dc:creator>
      <pubDate>Mon, 27 Jul 2026 14:21:10 +0000</pubDate>
      <link>https://dev.to/kyisaiah47/ranking-a-dashboard-by-funnel-depth-not-counts-1npe</link>
      <guid>https://dev.to/kyisaiah47/ranking-a-dashboard-by-funnel-depth-not-counts-1npe</guid>
      <description>&lt;p&gt;I had a metrics page that told me nothing. Page views per host, signups, a couple of Stripe lines, all rendered as cards in a grid. Every morning I'd look at it, feel vaguely informed, and then go build whatever I'd already decided to build the night before. That's the tell: if the dashboard never changes what you do next, it isn't a dashboard, it's decoration.&lt;/p&gt;

&lt;p&gt;So I rewrote it around one question — how far do people actually get — and let that ordering decide what I see first.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ordering is the product
&lt;/h2&gt;

&lt;p&gt;Kynth Studios ships a lot of separate products on separate hosts: FetchDue chasing overdue invoices for agencies, BenchFile doing NYC Local Law 84 filings, CertScope determining CPSC scope per SKU, ParseRail for developers integrating against the API. They don't share a funnel shape in any interesting way, but they do share a funnel &lt;em&gt;depth&lt;/em&gt;: someone lands, someone starts the demo, someone finishes the guided run, someone starts signup, someone finishes it, someone activates.&lt;/p&gt;

&lt;p&gt;Depth is the only axis where those products are comparable. A traffic number tells me a host got attention. A depth number tells me where attention stopped.&lt;/p&gt;

&lt;p&gt;So the model is a stage list plus a rollup per product:&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;// stages are ordered; index IS the depth&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;STAGES&lt;/span&gt; &lt;span class="o"&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;demo_start&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;demo_complete&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;signup_start&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;signup_complete&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;activated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Stage&lt;/span&gt; &lt;span class="o"&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;STAGES&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ProductFunnel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="na"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Stage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// deepest stage a product has ANY reach into&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;depth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ProductFunnel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;STAGES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;STAGES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// the first stage that eats everything&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;stall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ProductFunnel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Stage&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="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;STAGES&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="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;STAGES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;STAGES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;STAGES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&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="kc"&gt;null&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;depth&lt;/code&gt; sorts the list. &lt;code&gt;stall&lt;/code&gt; is the label on each row. Everything else on the page is subordinate to those two functions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqcc1wctm9rmebpzwulea.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqcc1wctm9rmebpzwulea.png" alt="Ranking a dashboard by funnel depth, not counts — code" width="800" height="1003"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Vanity metrics are the ones with no stage below them
&lt;/h2&gt;

&lt;p&gt;The rule I ended up with for what stays on the page: a metric earns its spot if there's a next stage it's supposed to feed. Page views feed demo starts. Demo starts feed completions. Completions feed signup starts.&lt;/p&gt;

&lt;p&gt;Page views on their own feed nothing I control, so they came off. Same with a top-level "total events" number, which mostly moved when I deployed something. Anything that only ever goes up and never implies an action is a metric you can watch forever without learning anything.&lt;/p&gt;

&lt;p&gt;The one that hurt to cut was a combined cross-product total. It looked healthy. It was healthy — it just averaged a product where people finish the demo together with one where nobody gets past the landing page, and the average was never the thing I needed to know. Now every row is per-product and the sort does the summarizing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the data engine away from the rendering
&lt;/h2&gt;

&lt;p&gt;The part I'd do again on any dashboard: the thing that computes stages, predicates, sorts and rollups is framework-agnostic and knows nothing about React. It takes events, returns &lt;code&gt;ProductFunnel[]&lt;/code&gt; sorted by depth. The components take that array and draw it.&lt;/p&gt;

&lt;p&gt;That split has paid off more than once. When a number looks wrong, the first question is which half is wrong — and because the contract between them is a plain typed object, I can answer it by calling the rollup in a test and reading the output. A rendering bug costs nothing on the engine side. A rollup bug is visible without a browser.&lt;/p&gt;

&lt;p&gt;It also means the same rollup feeds a morning summary that isn't a web page at all. Same functions, different renderer.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the page actually looks like now
&lt;/h2&gt;

&lt;p&gt;One list. Products sorted deepest-first, each row showing its stall stage and the counts on either side of it. FetchDue near the top because there's a one-click live demo with no signup form in front of it, so people reach the guided run. The compliance products — BenchFile, GoodStanding, DoseTrace — sit lower, because a done-for-you filing service has a different entry shape and the interesting stage is further along.&lt;/p&gt;

&lt;p&gt;That ordering is doing real work. Last week it put a product at the bottom of the list whose landing page I thought was fine, with a stall at &lt;code&gt;demo_start&lt;/code&gt; — plenty of people arriving, nobody starting. That's a specific bug in a specific place, which is more than the old grid ever gave me.&lt;/p&gt;

&lt;p&gt;FetchDue has no customers yet and the dashboard says so; the honest zero is on the page next to a link to the live product and a recorded run. I'd rather look at a zero I can locate than a total I can't.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bit I'd tell someone rebuilding theirs
&lt;/h2&gt;

&lt;p&gt;Pick the axis before you pick the charts. I spent longer than I should have arranging cards, and none of that time mattered, because the layout question and the ranking question are different questions and only one of them changes what you build tomorrow.&lt;/p&gt;

&lt;p&gt;Sorting by depth also has a property I didn't expect: the top of the list is the least urgent thing. The products getting people deepest need the least attention. Reading the page bottom-up is the actual workflow, which means the sort could arguably be reversed — I've left it deepest-first because I want the ceiling visible, and I go to the bottom on purpose.&lt;/p&gt;

</description>
      <category>analytics</category>
      <category>product</category>
      <category>webdev</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>Seek-safe animation: one paused timeline, scrubbed</title>
      <dc:creator>Isaiah Kim</dc:creator>
      <pubDate>Sat, 25 Jul 2026 14:21:58 +0000</pubDate>
      <link>https://dev.to/kyisaiah47/seek-safe-animation-one-paused-timeline-scrubbed-4jmp</link>
      <guid>https://dev.to/kyisaiah47/seek-safe-animation-one-paused-timeline-scrubbed-4jmp</guid>
      <description>&lt;p&gt;I've been building HyperFrames, the animation engine behind the product demo videos I make at Kynth. It renders video from HTML: a composition is an HTML file whose DOM declares its own timing with &lt;code&gt;data-*&lt;/code&gt; attributes, and the renderer turns that into frames. The same pipeline cuts the walkthroughs for FetchDue, CertScope and DoseTrace, alongside the Playwright harness that drives a real browser through each product and the edge-tts narration track.&lt;/p&gt;

&lt;p&gt;The whole design collapses into one requirement: &lt;strong&gt;same input time → same pixels&lt;/strong&gt;. Everything below is what that requirement cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does a render pipeline need a paused timeline instead of playback?
&lt;/h2&gt;

&lt;p&gt;Because there is no playback. The renderer doesn't watch an animation run and grab screenshots — it takes a time value, seeks the page to it, and reads a pixel buffer. Frames can be sampled out of order, and they can be sampled in parallel across workers. Nothing is allowed to depend on having arrived at this frame &lt;em&gt;through&lt;/em&gt; the previous one.&lt;/p&gt;

&lt;p&gt;That kills a whole category of normal web animation. &lt;code&gt;requestAnimationFrame&lt;/code&gt; loops accumulate state. &lt;code&gt;setTimeout&lt;/code&gt; chains assume wall-clock progression. Scroll and hover listeners never fire, because the renderer has no input events. If a visual only looks right after the browser has been sitting there for two seconds, it will never look right in a render.&lt;/p&gt;

&lt;p&gt;So every composition registers exactly one paused GSAP timeline, built synchronously at page load, keyed by the composition's own id:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// index.html — built at load, never played&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;gsap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timeline&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;paused&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="nx"&gt;tl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#headline&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ease&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;power2.out&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#rows .row&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;stagger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.08&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-=0.2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__timelines&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;invoice-scene&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key has to match &lt;code&gt;data-composition-id&lt;/code&gt; on the root element exactly. No &lt;code&gt;-mount&lt;/code&gt; or &lt;code&gt;-host&lt;/code&gt; suffix, which I know because I spent real time staring at a blank frame that previewed perfectly.&lt;/p&gt;

&lt;h3&gt;
  
  
  The rule that makes the rest fall out
&lt;/h3&gt;

&lt;p&gt;Build the timeline &lt;strong&gt;synchronously&lt;/strong&gt;. Not inside &lt;code&gt;async&lt;/code&gt;, not in a &lt;code&gt;Promise.then&lt;/code&gt;, not in a &lt;code&gt;setTimeout&lt;/code&gt;. The renderer can seek before an async builder finishes, and when it does you get a frame of the static HTML with none of the motion applied — and no error, because nothing failed. It's just early.&lt;/p&gt;

&lt;p&gt;Once that rule holds, seeking is a one-liner (&lt;code&gt;tl.seek(t)&lt;/code&gt;), and the interesting work moves to everything that &lt;em&gt;isn't&lt;/em&gt; the timeline.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyhg6pimnqmz8fj4xsst9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyhg6pimnqmz8fj4xsst9.png" alt="Seek-safe animation: one paused timeline, scrubbed — code" width="799" height="304"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually breaks determinism in practice?
&lt;/h2&gt;

&lt;p&gt;Clocks and randomness are the obvious ones, and they're the easy ones to lint. &lt;code&gt;Date.now()&lt;/code&gt;, &lt;code&gt;performance.now()&lt;/code&gt;, unseeded &lt;code&gt;Math.random()&lt;/code&gt; — banned outright for anything that affects visual state. If you want random-looking placement, seed a PRNG so the scatter is identical on every run.&lt;/p&gt;

&lt;p&gt;The subtler ones:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Infinite loops.&lt;/strong&gt; &lt;code&gt;repeat: -1&lt;/code&gt; has no defined state at time &lt;em&gt;t&lt;/em&gt; in a parallel sampler. Compute a finite count from the clip's duration instead:&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;repeats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;duration&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;cycleDuration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;floor&lt;/code&gt;, not &lt;code&gt;ceil&lt;/code&gt;. &lt;code&gt;ceil&lt;/code&gt; overshoots the declared duration, and &lt;code&gt;Math.max(0, …)&lt;/code&gt; stops a short clip from producing &lt;code&gt;-1&lt;/code&gt; — which is infinite again, by the back door.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two timelines touching the same property.&lt;/strong&gt; GSAP's overwrite behavior is order-dependent. If two tweens both animate &lt;code&gt;opacity&lt;/code&gt; on the same element in the same window, which one wins depends on evaluation order, and that can flip between renders. This one is nasty because it usually looks fine and then produces one wrong frame in the middle of a delivered video.&lt;/p&gt;

&lt;h3&gt;
  
  
  Never measure the DOM at tween time
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;getBoundingClientRect()&lt;/code&gt; inside a tween callback is the desync I hit most while building scene templates. It reads fine in preview, where frames arrive in order. Under a parallel renderer, the element you're measuring may be in a different state than you assumed, so the number you get back depends on sampling order.&lt;/p&gt;

&lt;p&gt;The fix is boring: compute layout constants once at composition setup and reuse them. Same for text — for anything dynamic, measure through the framework's own text helpers (&lt;code&gt;fitTextFontSize&lt;/code&gt;, or the &lt;code&gt;pretext&lt;/code&gt; measurement layer under it) rather than reflowing the DOM per frame.&lt;/p&gt;

&lt;p&gt;There's an allowlist for what can be animated at all: &lt;code&gt;opacity&lt;/code&gt;, &lt;code&gt;x&lt;/code&gt;, &lt;code&gt;y&lt;/code&gt;, &lt;code&gt;scale&lt;/code&gt;, &lt;code&gt;rotation&lt;/code&gt;, &lt;code&gt;color&lt;/code&gt;, &lt;code&gt;backgroundColor&lt;/code&gt;, &lt;code&gt;borderRadius&lt;/code&gt;, transforms. Never &lt;code&gt;display&lt;/code&gt;, never raw &lt;code&gt;visibility&lt;/code&gt;, and never &lt;code&gt;width&lt;/code&gt;/&lt;code&gt;height&lt;/code&gt;/&lt;code&gt;top&lt;/code&gt;/&lt;code&gt;left&lt;/code&gt; for layout changes. Transforms compose predictably at an arbitrary time; layout properties trigger reflow and take their neighbors with them.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you seek a runtime that isn't GSAP?
&lt;/h2&gt;

&lt;p&gt;You make each runtime publish something the engine can address by time, and you give the engine one adapter per runtime. GSAP covers most motion work, but a composition can mix runtimes, and each one seeks differently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lottie&lt;/strong&gt; — every player registers itself on &lt;code&gt;window.__hfLottie&lt;/code&gt;, and the adapter calls &lt;code&gt;goToAndStop(timeMs, false)&lt;/code&gt; on &lt;code&gt;lottie-web&lt;/code&gt; instances, or the frame/percentage API on dotLottie.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Web Animations API&lt;/strong&gt; — the adapter walks &lt;code&gt;document.getAnimations()&lt;/code&gt;, sets each &lt;code&gt;currentTime&lt;/code&gt; to HyperFrames time in milliseconds, then pauses it. Author with &lt;code&gt;fill: "both"&lt;/code&gt; so seeked states persist past their active window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CSS keyframes&lt;/strong&gt; — the adapter finds elements with a computed &lt;code&gt;animation-name&lt;/code&gt;, seeks their &lt;code&gt;Animation&lt;/code&gt; handles when the browser exposes them, and otherwise falls back to pausing with a negative &lt;code&gt;animation-delay&lt;/code&gt;. Use &lt;code&gt;animation-fill-mode: both&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Three.js&lt;/strong&gt; — the adapter deliberately does &lt;em&gt;not&lt;/em&gt; own your scene. It sets &lt;code&gt;window.__hfThreeTime&lt;/code&gt; and dispatches &lt;code&gt;hf-seek&lt;/code&gt;, and your composition renders that exact time:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hf-seek&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;time&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;mixer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;           &lt;span class="c1"&gt;// authored clips&lt;/span&gt;
  &lt;span class="nx"&gt;camera&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// pure function of time&lt;/span&gt;
  &lt;span class="nx"&gt;renderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;camera&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The shape is the same every time: &lt;em&gt;time in, frame out, no memory of the previous call&lt;/em&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Duration turned out to be a separate problem from seeking
&lt;/h3&gt;

&lt;p&gt;This surprised me. The render engine needs a positive total duration before it captures a single frame, or capture fails outright. A GSAP timeline reports its own length, so that case is free. The others aren't.&lt;/p&gt;

&lt;p&gt;CSS duration is inferred from the longest &lt;code&gt;animation-delay&lt;/code&gt; + &lt;code&gt;duration&lt;/code&gt; × finite iteration count. WAAPI comes from the longest effect's &lt;code&gt;getComputedTiming().endTime&lt;/code&gt;. Lottie reads the asset's native length, which is finite even when &lt;code&gt;loop: true&lt;/code&gt;. And Three.js is &lt;strong&gt;not inferable at all&lt;/strong&gt; — the adapter only forwards time, it doesn't inspect your scene for an &lt;code&gt;AnimationClip&lt;/code&gt;. So a Three.js composition must declare &lt;code&gt;data-duration&lt;/code&gt; on its root, and the linter errors when it doesn't.&lt;/p&gt;

&lt;p&gt;Two capabilities that felt like one: &lt;em&gt;where am I on the timeline&lt;/em&gt; and &lt;em&gt;how long is the timeline&lt;/em&gt;. Every adapter answers the first. Only some can answer the second.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you catch a desync before it reaches a delivered video?
&lt;/h2&gt;

&lt;p&gt;Sample the frames a human would never scrub to. Three passes, in order of cost:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx hyperframes check                      &lt;span class="c"&gt;# lint, runtime, layout, motion, contrast&lt;/span&gt;
npx hyperframes snapshot &lt;span class="nt"&gt;--at&lt;/span&gt; 2.4,6.1,9.8  &lt;span class="c"&gt;# eyeball the seams, not the beats&lt;/span&gt;
npx hyperframes render                     &lt;span class="c"&gt;# only after the frames look right&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;check&lt;/code&gt; catches the mechanical violations — a missing duration source, a &lt;code&gt;repeat&lt;/code&gt; that overshoots, a &lt;code&gt;set&lt;/code&gt; on a clip from a later scene. Snapshots catch the silent class: a transformed element that's still inline and therefore invisible, a full-screen background painted on the root instead of a full-bleed child (which previews correctly and renders black), a pulsing decorative that clears its neighbor at rest and collides at peak scale.&lt;/p&gt;

&lt;p&gt;There's also an audit script that walks every registered timeline, enumerates the tweens, samples bounding boxes and writes an animation map — useful for finding dead zones and inconsistent stagger after the fact.&lt;/p&gt;

&lt;p&gt;Almost every bug that survived to a delivered frame had the same signature: preview looked right, render didn't. Preview plays forward, in order, in one process. The renderer doesn't do any of those three things. Anything you validate only by pressing play, you haven't validated.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>animation</category>
      <category>webdev</category>
      <category>gsap</category>
    </item>
    <item>
      <title>Why a CSS diff reads right and renders wrong</title>
      <dc:creator>Isaiah Kim</dc:creator>
      <pubDate>Thu, 23 Jul 2026 14:21:40 +0000</pubDate>
      <link>https://dev.to/kyisaiah47/why-a-css-diff-reads-right-and-renders-wrong-3ah1</link>
      <guid>https://dev.to/kyisaiah47/why-a-css-diff-reads-right-and-renders-wrong-3ah1</guid>
      <description>&lt;p&gt;I shipped a mobile fix for one of our tenant sites last week — coi-tracking, the first real app we stood back up after the catalog teardown. The change was small: restore some breakpoint rules that had gone missing in a port, and un-hide a few elements that only exist once on desktop. Reading the diff, every line was obviously correct. Reading the site at 390px before the fix, it was obviously broken. Those two facts had been true at the same time for a while.&lt;/p&gt;

&lt;p&gt;That gap — a change that reads right in the diff and wrong in the browser — is the thing I've spent the most effort building a habit around, because I'm one person and there is no second pair of eyes between my editor and production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does a CSS diff read right and render wrong?
&lt;/h2&gt;

&lt;p&gt;Because a diff shows you the rule you changed, and the browser shows you the rule that won. Those are different questions, and only one of them is answered by the text in front of you.&lt;/p&gt;

&lt;p&gt;A media query block is a self-contained, readable thing. When it's present, it reads as deliberate. When it's &lt;em&gt;absent&lt;/em&gt;, it reads as nothing at all — there's no line to notice. Deletions in CSS are silent in a way deletions in TypeScript are not. Remove a function and something fails to compile. Remove a &lt;code&gt;@media (max-width: 768px)&lt;/code&gt; block and the desktop layout stays pixel-identical, the build passes, and the site is broken at a width you weren't looking at.&lt;/p&gt;

&lt;p&gt;The coi-tracking landing came through our Framer→Next capture-and-own pipeline — we license a template, capture the live rendered DOM and CSS, and port it into code we own so it's ours to change forever. That pipeline is great, and it is also exactly the kind of transform where breakpoint blocks and &lt;code&gt;display&lt;/code&gt; toggles get dropped one at a time without any of the surviving lines looking wrong.&lt;/p&gt;

&lt;h3&gt;
  
  
  The three shapes that keep doing this
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Missing breakpoint blocks.&lt;/strong&gt; Desktop unaffected, mobile stacked wrong. Zero signal in the diff.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Orphaned &lt;code&gt;display: none&lt;/code&gt;.&lt;/strong&gt; An element hidden at one width on the assumption that a different element covers that case — and then the covering element gets removed or renamed. Now nothing renders there at all. The diff shows a deletion of "unused" markup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reveal-on-scroll that never reveals.&lt;/strong&gt; The element is in the DOM, present in view-source, passes any test that queries for it, and is invisible to a human forever because its opacity never got animated to 1.&lt;/p&gt;

&lt;p&gt;That third one is the meanest, so it's worth taking apart.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm393utxzbf8hodr7jxbn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm393utxzbf8hodr7jxbn.png" alt="Why a CSS diff reads right and renders wrong — code" width="800" height="782"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you keep a scroll reveal from leaving content invisible?
&lt;/h2&gt;

&lt;p&gt;Make the failure mode "shows too early" instead of "never shows," and add a hard failsafe that ignores your own logic.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;IntersectionObserver&lt;/code&gt; reveals fail in three ways I now check for by reflex:&lt;/p&gt;

&lt;p&gt;A negative bottom &lt;code&gt;rootMargin&lt;/code&gt; shrinks the observation box, so the callback fires &lt;em&gt;after&lt;/em&gt; the element is already visible to the reader — the animation plays late, or looks like a flicker. A positive margin fires early, which is what you actually want.&lt;/p&gt;

&lt;p&gt;A large hide-transform (&lt;code&gt;translateY(80px)&lt;/code&gt;, a big &lt;code&gt;scale&lt;/code&gt;) means the rect the observer measures is the &lt;em&gt;post-transform&lt;/em&gt; rect. If your transform pushes the element far enough, the observer's geometry and the layout geometry disagree, and the intersection can simply never happen.&lt;/p&gt;

&lt;p&gt;And anything above the fold on first paint may never trigger a scroll event at all.&lt;/p&gt;

&lt;p&gt;Every one of those leaves content permanently invisible, which is the worst possible outcome for a marketing page — worse than an ugly animation, worse than no animation.&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;// reveal.ts — the version I ship now&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;observeReveal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HTMLElement&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Above the fold on load: reveal immediately, never wait for a scroll.&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBoundingClientRect&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;top&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHeight&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;revealed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;io&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;IntersectionObserver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="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;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isIntersecting&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;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;revealed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;disconnect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="c1"&gt;// Positive margin: fire BEFORE it enters view, not after.&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;rootMargin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0px 0px 15% 0px&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Failsafe: whatever the observer thinks, this content becomes&lt;/span&gt;
  &lt;span class="c1"&gt;// visible. A broken animation beats a blank section.&lt;/span&gt;
  &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;revealed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;disconnect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The hide state pairs with a &lt;em&gt;small&lt;/em&gt; transform — 16px, not 80 — so layout and observer geometry stay in agreement:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;data-reveal&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;translateY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;16px&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;data-reveal&lt;/span&gt;&lt;span class="o"&gt;][&lt;/span&gt;&lt;span class="nt"&gt;data-revealed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"true"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;setTimeout&lt;/code&gt; is the part I'd have argued against a year ago as sloppy. I don't anymore. It converts an entire class of invisible-content bugs into a mild timing artifact, and it costs three lines.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does a browser check look like when there's no QA team?
&lt;/h2&gt;

&lt;p&gt;It's a script that opens real Chrome at the widths you actually ship at and saves a screenshot of each one, and you look at them.&lt;/p&gt;

&lt;p&gt;I already had this lying around for a different reason. Our demo recordings are driven by Playwright — a real browser walking the full product flow with no human in the loop, which I wrote up separately. Once you have a harness that can drive a live page and wait for it to settle deterministically instead of guessing at timeouts, pointing it at a landing page across breakpoints is a ten-line job.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;WIDTHS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;390&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;768&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1280&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1728&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;for &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;width&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;WIDTHS&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;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newPage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;viewport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;900&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&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;waitUntil&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;networkidle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`shots/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.png`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fullPage&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No pixel diffing, no baselines to maintain, no flaky visual-regression suite that I'd start ignoring within two weeks. Four images I look at with my eyes.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I'm actually looking for
&lt;/h3&gt;

&lt;p&gt;Large blocks of empty vertical space — that's an element that failed to render or is sitting at opacity 0. A section that appears at 1280 and vanishes at 768 without a mobile counterpart taking its place. Text that wraps to five lines in a container built for two. And any image slot that's a blank rectangle, which is the failure mode after an asset purge.&lt;/p&gt;

&lt;p&gt;That last one is worth calling out. I recently stripped a pile of unused Framer template images and the dead &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; refs pointing at them, and deleted the orphaned components that came with them. On paper that's the safest kind of change — removing code nothing uses. In practice "nothing uses it" is a claim about the whole render tree at every viewport, and the diff can only show you the deletion.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which changes get a browser check and which don't?
&lt;/h2&gt;

&lt;p&gt;Anything that touches a breakpoint, toggles visibility, deletes an asset, or moves a route. Everything else I merge on the diff.&lt;/p&gt;

&lt;p&gt;That's a deliberately narrow list, because a rule I apply to every change is a rule I'll stop applying. Backend logic, schema changes, agent prompts, API routes — I trust tests and the diff. But the four categories above share a property: their failure is invisible in text and obvious in a browser.&lt;/p&gt;

&lt;p&gt;Route moves earned their spot recently. I folded &lt;code&gt;apps/landing&lt;/code&gt; into the Artemis app so the marketing site and the builder live in one place — landing at the root, builder at &lt;code&gt;/create&lt;/code&gt;. Every individual file move was correct. Whether the &lt;em&gt;combination&lt;/em&gt; of moves left a working site is not a question source code answers. Our &lt;code&gt;deploy.sh&lt;/code&gt; verify step checks real URLs after deploy, including accepting the post-teardown redirects from the retired catalog, and it's the only thing that actually knows.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about bugs that only exist in a browser at all?
&lt;/h2&gt;

&lt;p&gt;Some errors have no source-code representation, and hydration is the clearest example.&lt;/p&gt;

&lt;p&gt;Artemis had a hydration mismatch I could not see anywhere in the code, because it wasn't in the code — it came from the DOM being modified before React hydrated, in a real browser with real extensions installed. Server HTML and client HTML disagreed for reasons no file in the repo mentioned. The fix was pinning a vendored dependency to a known-good version and gating the injected case explicitly. No amount of diff-reading would have found it; it needed a browser with the extension installed, which is to say, a normal user's browser.&lt;/p&gt;

&lt;p&gt;This is the same principle underneath all of it, and it's why Kynth Apollo — our capabilities layer, where every hard problem gets solved once and reused — holds the browser harness alongside everything else. Artemis assembles apps out of those finished pieces. A piece that renders correctly at one width and disappears at another isn't finished, and there's exactly one place you can find that out.&lt;/p&gt;

&lt;p&gt;Open the browser. Four widths. Look at the pictures.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>css</category>
      <category>playwright</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Recording product demos with Playwright and no human</title>
      <dc:creator>Isaiah Kim</dc:creator>
      <pubDate>Tue, 21 Jul 2026 14:21:06 +0000</pubDate>
      <link>https://dev.to/kyisaiah47/recording-product-demos-with-playwright-and-no-human-62f</link>
      <guid>https://dev.to/kyisaiah47/recording-product-demos-with-playwright-and-no-human-62f</guid>
      <description>&lt;p&gt;I record full product demos with no human touching the keyboard — a real Chrome instance drives the app end to end, clicks through the flow, and a recorder captures every frame. The interesting part turned out not to be the clicking. It was deciding &lt;em&gt;when&lt;/em&gt; the page was actually ready for the next step, and realizing the thing I'd built was a state machine wearing a recorder's clothes.&lt;/p&gt;

&lt;p&gt;Here's what I learned building it for Kynth, where the demos have to look hand-driven even though nobody's there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why do fixed timeouts break automated demos?
&lt;/h2&gt;

&lt;p&gt;Fixed timeouts break because they encode a guess about the network and the machine, and both change under you. The classic version is &lt;code&gt;await page.waitForTimeout(2000)&lt;/code&gt; after a navigation. On my laptop with a warm cache, 2000ms is 1500ms of dead air on camera. On a cold CI runner rendering the same page, 2000ms is 400ms short and I capture a spinner mid-flight — a demo that pauses awkwardly, then jump-cuts into a half-loaded dashboard.&lt;/p&gt;

&lt;p&gt;You can't win this by tuning the number. A bigger timeout wastes screen time on every fast run; a smaller one flakes on every slow one. The number is wrong in both directions at once because it's answering the wrong question. I don't care that 2 seconds passed. I care that the page stopped changing.&lt;/p&gt;

&lt;p&gt;So the whole recorder is built around one idea: &lt;strong&gt;settle-detection&lt;/strong&gt;. Instead of waiting a duration, wait for the DOM to go quiet.&lt;/p&gt;

&lt;h3&gt;
  
  
  What "settled" actually means
&lt;/h3&gt;

&lt;p&gt;A page is settled when nothing meaningful has mutated for a short quiet window. I watch the DOM with a &lt;code&gt;MutationObserver&lt;/code&gt; injected into the page, reset a timer on every mutation batch, and consider the page ready when the timer survives, say, 350ms untouched — plus network idle, because a settled DOM that's still waiting on an XHR is about to un-settle.&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;// runs inside the page via page.evaluate&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;waitForSettled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;quietMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;350&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;maxMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8000&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="k"&gt;new&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="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&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;obs&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;MutationObserver&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nx"&gt;obs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;childList&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;subtree&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;characterData&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;started&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&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;tick&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;last&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;quietMs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;obs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;disconnect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;started&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;maxMs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;obs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;disconnect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;// hard ceiling&lt;/span&gt;
      &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nf"&gt;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tick&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="nf"&gt;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tick&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;maxMs&lt;/code&gt; ceiling matters more than it looks. Some pages never fully go quiet — a looping CSS animation, a polling badge, an SSE connection nudging a counter. Without the ceiling, settle-detection waits forever for a page that's &lt;em&gt;never&lt;/em&gt; going to be still. So it's settle-with-a-deadline: quiet wins if it can, the clock wins if it can't. That single fallback is the difference between a recorder that's robust and one that hangs on a &lt;code&gt;&amp;lt;marquee&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why is a demo recorder really a state machine?
&lt;/h2&gt;

&lt;p&gt;A recorder is a state machine because every step depends entirely on the exact state the last step left the app in, and the hard bugs all live in the transitions, not the actions. I didn't set out to build one. I started with a flat script — click, wait, click, wait — and it worked until it didn't, and every time it broke it was because step N assumed a screen that step N-1 didn't actually produce.&lt;/p&gt;

&lt;p&gt;Once I named the states, the failures got legible. The recorder moves through explicit nodes — &lt;code&gt;boot&lt;/code&gt;, &lt;code&gt;landing&lt;/code&gt;, &lt;code&gt;demo-open&lt;/code&gt;, &lt;code&gt;flow-step-k&lt;/code&gt;, &lt;code&gt;result&lt;/code&gt;, &lt;code&gt;teardown&lt;/code&gt; — and each transition has three parts: an action (the click), a settle (wait for quiet), and an assertion (prove we landed where we meant to). If the assertion fails, I don't blindly continue into a black recording. I know exactly which edge broke.&lt;/p&gt;

&lt;h3&gt;
  
  
  The assertion is the part people skip
&lt;/h3&gt;

&lt;p&gt;The temptation is to trust that clicking a button lands you on the next screen. It usually does. But "usually" is how you get a 90-second video where seconds 40 through 55 are a modal that failed to open, recorded in full confidence. So every transition ends by asserting the target state exists before the camera is allowed to move on:&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;edge&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;edge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;act&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;            &lt;span class="c1"&gt;// e.g. click "Run demo"&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;settle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;              &lt;span class="c1"&gt;// DOM quiet + network idle&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;edge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// target state visible?&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;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;StateError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;edge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;edge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;edge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;to&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;Modeling it as a machine also gave me retries for free. A flaky edge — a cold Supabase connection that 500s the first time, an auth redirect that resolves a beat late — can retry &lt;em&gt;just that transition&lt;/em&gt; from a known state, instead of tearing down and re-recording the whole demo from &lt;code&gt;boot&lt;/code&gt;. When you're rendering these for real, re-running one edge versus the whole flow is the difference between a 3-second recovery and starting over.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you keep an automated flow from looking robotic?
&lt;/h2&gt;

&lt;p&gt;You add human-shaped pacing back in &lt;em&gt;on purpose&lt;/em&gt;, after settle-detection has removed the accidental pauses. This felt backwards at first. I spent all this effort deleting dead air, then deliberately put some back — but the two kinds of waiting are different. Settle-detection removes the pauses that come from &lt;em&gt;not knowing&lt;/em&gt; the page is ready. Then I add small, intentional dwell before a click so a viewer's eye can land on the button before it depresses.&lt;/p&gt;

&lt;p&gt;The rule I settled on: settle to zero, then dwell to taste. Settle-detection guarantees the &lt;em&gt;floor&lt;/em&gt; — we're never recording a spinner. The dwell is a fixed, small, deliberate beat layered on top, and because it sits on a known-quiet page it reads as a person deciding, not a script stalling. Crucially the dwell is constant across machines; the settle absorbs all the variance. Fast runner or slow, the &lt;em&gt;rhythm&lt;/em&gt; of the final video is identical, which is the whole point when these get cut together.&lt;/p&gt;

&lt;h3&gt;
  
  
  Settle-detection is a recording primitive, not a test trick
&lt;/h3&gt;

&lt;p&gt;The waiting strategy that makes a good recorder is the same one that makes a good end-to-end test, which is why this generalizes past demos. A test that waits for settle instead of a timeout is a test that doesn't flake and doesn't waste minutes of CI on padding. The demo harness and the test harness want the identical thing: proof that the app reached a state, not proof that time passed.&lt;/p&gt;

&lt;p&gt;That overlap is why the recorder lives next to the app in the Turborepo monorepo rather than in some separate video project. It drives the real Next 16 app against real Supabase and Stripe — the same stack a user hits. When FetchDue went live the weekend of July 18, the recorded run that proves it works — a real reply classified, a real answer drafted, a real email delivered — came out of this exact harness driving the live product. No mock, no staged screen. The camera watched Chrome do the thing a customer does.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell someone building this
&lt;/h2&gt;

&lt;p&gt;Stop tuning the timeout and start watching the DOM. Almost every flaky wait in an automated flow is a fixed duration standing in for a state you could have observed directly. Replace it with settle-with-a-deadline — quiet window plus a hard ceiling — and the whole class of "worked on my machine, flaked in CI" problems goes away, because you stopped guessing at time and started measuring readiness.&lt;/p&gt;

&lt;p&gt;The state-machine framing is the other half. The moment you write down the states and make each transition assert its target, your failures tell you which edge broke instead of leaving you a black video and a shrug. And you get per-edge retries as a bonus, which is what makes recording against a real backend — where a cold connection occasionally hiccups — tolerable instead of maddening.&lt;/p&gt;

&lt;p&gt;This recorder is one of the pieces inside Kynth Apollo, the layer where I solve a hard problem once and never solve it again. Every demo, every product screenshot, every proof-it-works run pulls from the same harness now. That's the quiet luxury of building this way: the second time you need a driven browser, you're not building — you're composing.&lt;/p&gt;

</description>
      <category>playwright</category>
      <category>automation</category>
      <category>testing</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
