<?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: Daniel Pertu</title>
    <description>The latest articles on DEV Community by Daniel Pertu (@daniel_pertu).</description>
    <link>https://dev.to/daniel_pertu</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%2F3981169%2F83f3f6c9-9d75-47a7-9b7c-c28d13594696.jpg</url>
      <title>DEV Community: Daniel Pertu</title>
      <link>https://dev.to/daniel_pertu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/daniel_pertu"/>
    <language>en</language>
    <item>
      <title>Three of our parallel workers were interrupted mid-file, and their unreviewed output shipped with 22 lines of edits</title>
      <dc:creator>Daniel Pertu</dc:creator>
      <pubDate>Tue, 10 Nov 2026 09:37:45 +0000</pubDate>
      <link>https://dev.to/daniel_pertu/three-of-our-parallel-workers-were-interrupted-mid-file-and-their-unreviewed-output-shipped-with-5057</link>
      <guid>https://dev.to/daniel_pertu/three-of-our-parallel-workers-were-interrupted-mid-file-and-their-unreviewed-output-shipped-with-5057</guid>
      <description>&lt;p&gt;Four days ago we shipped 50 pages, one per named assessment, as a trial. This weekend we did the other 281. The site now has 331 of them, one per test, across 56 vendors, at &lt;code&gt;/games/&amp;lt;vendor&amp;gt;/&amp;lt;test&amp;gt;&lt;/code&gt;, and the live sitemap went from 530 URLs to 835.&lt;/p&gt;

&lt;p&gt;The content is not the interesting part. The geometry is.&lt;/p&gt;

&lt;h2&gt;
  
  
  One file per vendor, because 31 branches had to land in one directory
&lt;/h2&gt;

&lt;p&gt;All 331 pages come out of one registry, and that registry is 59 files in a directory with an index that does nothing but concatenate them:&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;/**
 * Every per-test page, one content file per provider.
 *
 * One file per provider (two for TestGorilla) so that the workers filling them
 * in never edit the same file. Everything that needs to know about these pages
 * (the routes, the sitemap, the provider hubs' link lists, the guard test)
 * reads from here.
 */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="no"&gt;TEST_PAGES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;TestPageEntry&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="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;span class="n"&gt;acerTestPages&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;span class="n"&gt;alvaLabsTestPages&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;span class="n"&gt;amberjackTestPages&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;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;59 files, 40,183 lines, 2.38 MB of TypeScript. The index adds one &lt;code&gt;import&lt;/code&gt; line and one spread line per file, which means two branches working on two vendors produce a conflict only in the import block, where git resolves both sides cleanly because they are different lines in alphabetical order. 18 named branches, 10 unnamed worktree branches and 3 recovery branches merged into the integration branch over an evening, and the conflicts were all in that index.&lt;/p&gt;

&lt;p&gt;This is a rule we arrived at the hard way: &lt;strong&gt;if parallel work has to touch a shared file, make the shared file a list of one-line entries.&lt;/strong&gt; Anything richer, and you spend the evening resolving.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three workers were interrupted, and we committed their work anyway
&lt;/h2&gt;

&lt;p&gt;Partway through, three of the workers stopped mid-file. Not failed, not finished: interrupted. The run that recovered them made three commits with the same message, in the same second:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;4abfafa2 WIP: interrupted game-pages worker (unreviewed, unfinished)
  lib/games/test-pages/faa-atsa.ts | 815 +++++++++++++++++++++++-
dd57670e WIP: interrupted game-pages worker (unreviewed, unfinished)
  lib/games/test-pages/criteria.ts         | 497 ++++++++-------
  lib/games/test-pages/revelian.ts         | 323 +++++++-----
  lib/games/test-pages/test-partnership.ts | 593 ++++++++++--------
c743dac2 WIP: interrupted game-pages worker (unreviewed, unfinished)
  lib/games/test-pages/harver.ts | 1054 ++++++++++++++++++++++++++-
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The commit message is the whole technique. A dirty worktree from an interrupted job is a thing you are afraid to touch, and so is a branch whose state nobody can describe. A commit that says "unreviewed, unfinished" in its subject line is neither: it is a checkpoint you can diff, review, fix, or throw away, and nothing silently merges it while you are not looking.&lt;/p&gt;

&lt;p&gt;The part I did not expect: all three were nearly shippable. Measured from the WIP commits to today's &lt;code&gt;main&lt;/code&gt;, those five files took &lt;strong&gt;22 insertions and 20 deletions&lt;/strong&gt; between them to go live. &lt;code&gt;faa-atsa.ts&lt;/code&gt; needed exactly one changed line out of 815.&lt;/p&gt;

&lt;p&gt;That is not an argument that unreviewed output is fine. It is an argument for committing it before you judge it, because the alternative, on the night, is deleting 2,000 lines you would have kept.&lt;/p&gt;

&lt;h2&gt;
  
  
  331 route files, all exactly ten lines
&lt;/h2&gt;

&lt;p&gt;Every page's route file is this, and there are 331 of them:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;TestPageShell&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;testPageMetadata&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s1"&gt;'@/components/games/TestPageShell'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;requireTestPage&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s1"&gt;'@/lib/games/test-pages'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;requireTestPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'aon'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'gapchallenge'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;testPageMetadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nc"&gt;Page&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;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;TestPageShell&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Normalise away the two string arguments and all 331 files hash to the same value. That is deliberate, and &lt;code&gt;requireTestPage&lt;/code&gt; is the reason it works: it throws rather than returning &lt;code&gt;undefined&lt;/code&gt;, so a typo in a route file is a build failure and not a page that renders an empty shell in production.&lt;/p&gt;

&lt;p&gt;A dynamic &lt;code&gt;[test]&lt;/code&gt; segment would have been fewer files. We had one, for the &lt;code&gt;/tests&lt;/code&gt; cluster, and deleted it last week. Static folders make the route space enumerable, which is what the guard test needs and what the next section is about.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failures a content registry has, and the near miss
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; * Nothing renders these pages until someone visits them, so the mistakes that
 * matter are invisible in review: a registry entry with no route folder (no
 * page, but a sitemap URL that 404s), a folder with no entry (a build that
 * throws), a game id from another provider, a title that blows the length
 * budget, or a path that robots.txt blocks because a game route happens to be
 * its prefix.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last one nearly happened. Our robots.txt is 850 lines, of which 780 are plain &lt;code&gt;Disallow&lt;/code&gt; prefixes, and 372 of those are the individual practice game routes that sit directly under &lt;code&gt;/games/&lt;/code&gt;. Eight of the new page slugs are byte-identical to one of those disallowed ids:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/games/arctic-shores/analysis
/games/arctic-shores/arrows
/games/arctic-shores/balance
/games/arctic-shores/order
/games/arctic-shores/patterns
/games/arctic-shores/team-sales
/games/arctic-shores/tickets
/games/arctic-shores/token-search
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Disallow: /games/balance&lt;/code&gt; does not prefix-match &lt;code&gt;/games/arctic-shores/balance&lt;/code&gt;, so all eight are crawlable. They are safe entirely because of which segment the word sits in. One rule written as &lt;code&gt;/*balance&lt;/code&gt; instead and eight pages quietly leave the index, which is why the test runs the real &lt;code&gt;robots()&lt;/code&gt; export over the real route list rather than trusting anybody's reading of 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="n"&gt;blocker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;plainPrefixes&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="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&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;route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&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="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blocker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sb"&gt;`${route} is blocked by "${blocker}"`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBeUndefined&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 other check worth copying is the one for internal links. Each entry has a &lt;code&gt;related&lt;/code&gt; list, 855 links across the set, and the test resolves each href to a folder on disk and asserts a &lt;code&gt;page.tsx&lt;/code&gt; exists. Writing a link to a page a parallel branch has not created yet is the single most common mistake in this kind of round.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ledger
&lt;/h2&gt;

&lt;p&gt;Counts I pulled out of the registry by importing it rather than grepping it, because regex counting over TypeScript has lied to me twice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;331 pages over 56 vendors, 2 to 17 pages each, median 6&lt;/li&gt;
&lt;li&gt;1,971 source URLs, 804 of them distinct, over 208 hosts. The biggest single host is a web archive with 99&lt;/li&gt;
&lt;li&gt;one page rests on one source, another rests on 22&lt;/li&gt;
&lt;li&gt;1,857 fact rows, 1,649 sections, 1,277 FAQs, 197,861 words&lt;/li&gt;
&lt;li&gt;1,943 keywords, 1,934 distinct, so nine collisions in all&lt;/li&gt;
&lt;li&gt;every &lt;code&gt;metaTitle&lt;/code&gt; between 33 and 48 characters, every description between 124 and 158, enforced rather than trimmed&lt;/li&gt;
&lt;li&gt;359 references to practice games, 358 distinct, which leaves &lt;strong&gt;14 of our 372 games with no page of their own&lt;/strong&gt; and 15 pages covering more than one game, because some vendors sell levels of a test that candidates search for as one thing&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  See it
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://cogniprep.app/games/aon/gapchallenge" rel="noopener noreferrer"&gt;&lt;code&gt;/games/aon/gapchallenge&lt;/code&gt;&lt;/a&gt; is one of them; the index of a vendor's set is its hub, for instance &lt;a href="https://cogniprep.app/games/arctic-shores" rel="noopener noreferrer"&gt;&lt;code&gt;/games/arctic-shores&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The contrast worth checking yourself is what happens when you make a path up. Ask for &lt;code&gt;/games/aon/zzz&lt;/code&gt; and you get a real 404 with &lt;code&gt;noindex, follow&lt;/code&gt;, because that URL space is 331 static folders and nothing else. Ask for &lt;code&gt;/games/zzz&lt;/code&gt;, one segment up, and you get &lt;strong&gt;200&lt;/strong&gt; with the title "Game Not Found" and two conflicting robots tags in the same document, because that space has a dynamic segment which reads the session and commits its head before the body can decide the game does not exist. Same site, two URL spaces, and only one of them tells a crawler the truth. The one-segment case is on the fix list and the folders are the reason the new one never had the problem.&lt;/p&gt;

&lt;p&gt;The machine-readable summary of the whole library is at &lt;a href="https://cogniprep.app/llms.txt" rel="noopener noreferrer"&gt;cogniprep.app/llms.txt&lt;/a&gt;, which is generated now rather than typed, and its first line will tell you the same 372 and 56 this post does.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>nextjs</category>
      <category>git</category>
      <category>seo</category>
    </item>
    <item>
      <title>We built a buy-everything pass, deleted it 26 minutes later, and the thing that replaced it needed no new table</title>
      <dc:creator>Daniel Pertu</dc:creator>
      <pubDate>Tue, 10 Nov 2026 09:34:52 +0000</pubDate>
      <link>https://dev.to/daniel_pertu/we-built-a-buy-everything-pass-deleted-it-26-minutes-later-and-the-thing-that-replaced-it-needed-40ad</link>
      <guid>https://dev.to/daniel_pertu/we-built-a-buy-everything-pass-deleted-it-26-minutes-later-and-the-thing-that-replaced-it-needed-40ad</guid>
      <description>&lt;p&gt;At 19:34 one evening last weekend we merged an All Access pass: one payment for every provider on sale plus the premium feature set, priced per region, open to every discount programme. 26 files, 2,029 insertions. A 279-line checkout route, a 240-line pricing card, 463 lines of tests, a rebuilt pricing page, a new clause in the terms.&lt;/p&gt;

&lt;p&gt;At 20:01 it was deleted. 39 files, 828 insertions against 1,413 deletions: the route, the card, the pricing module, the tests, the purchase intent, every type, and both the terms and FAQ wording.&lt;/p&gt;

&lt;p&gt;It never reached production. Both merges happened inside a feature branch, and that branch reached &lt;code&gt;main&lt;/code&gt; as a single commit a day later, so no deployed state ever contained it. I checked rather than assuming, because "we shipped and reverted" and "we never shipped" are different stories and only one of them is true here: walk &lt;code&gt;main&lt;/code&gt;'s first-parent chain, ask &lt;code&gt;git cat-file -e &amp;lt;commit&amp;gt;:app/api/all-access/checkout/route.ts&lt;/code&gt; at each step, and the file is in none of them.&lt;/p&gt;

&lt;p&gt;The reason for the delete was commercial, not technical: a pass that bundles everything prices our most motivated customer at a discount and teaches everyone else to wait for the bundle. What the owner wanted instead was a reason to come back.&lt;/p&gt;

&lt;h2&gt;
  
  
  The replacement is a row in a table that already existed
&lt;/h2&gt;

&lt;p&gt;Every paid purchase now mints a personal code for 20% off the next one, good for 30 days. The implementation is one new module plus a terms file, and zero migrations, because the code is a row in &lt;code&gt;nurture_discount_codes&lt;/code&gt;, the table that already holds the codes we email people:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; * The code is a row in `nurture_discount_codes`, so it is reserved, claimed and
 * released by exactly the same paths as an emailed nurture code.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two fields make it a different kind of code. &lt;code&gt;campaign&lt;/code&gt; is &lt;code&gt;next_purchase&lt;/code&gt;, and the table's &lt;code&gt;(user_id, campaign)&lt;/code&gt; unique index is the whole of "one live code per buyer". &lt;code&gt;product_type&lt;/code&gt; is &lt;code&gt;'any'&lt;/code&gt;, where an emailed code names the single product it discounts.&lt;/p&gt;

&lt;p&gt;The entire minting rule is one upsert:&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="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="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nurtureDiscountCodesTable&lt;/span&gt;&lt;span class="p"&gt;)&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="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&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="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;campaign&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;NEXT_PURCHASE_CAMPAIGN&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;span class="n"&gt;fresh&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="nf"&gt;onConflictDoUpdate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;nurtureDiscountCodesTable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nurtureDiscountCodesTable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;campaign&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;set&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;span class="c1"&gt;// Only a spent or lapsed code is replaced; a working one is left alone.&lt;/span&gt;
    &lt;span class="n"&gt;setWhere&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nurtureDiscountCodesTable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'claimed'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nf"&gt;lte&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nurtureDiscountCodesTable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&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;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;returning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;nurtureDiscountCodesTable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&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;Three behaviours out of one statement: no row yet, mint one; a claimed or expired row, replace it with a fresh code and clear the reservation and Stripe columns; a row that is live and unspent, leave it exactly as it is, because handing somebody a second code while their first one still works is a support ticket waiting to happen. &lt;code&gt;setWhere&lt;/code&gt; on a conflict target is the feature that makes that a single round trip, and it is the one bit of &lt;code&gt;ON CONFLICT&lt;/code&gt; I always have to look up.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;mintNextPurchaseCode&lt;/code&gt; never throws. It runs on the webhook's post-grant path, where the sale is already delivered, so the worst outcome of a database hiccup has to be a missing code and not a failed webhook.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/**
 * Gives a paying buyer their code for a discount on the next purchase. Runs after
 * the nurture claim above, so a purchase made with the previous code finds it
 * claimed and earns a fresh one. On every delivery, like the settlement: the
 * upsert keeps a live code, so a retry changes nothing. Never throws.
 */&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mint after the purchase's own code claim settles, and the thing chains: spend your code, the row goes to &lt;code&gt;claimed&lt;/code&gt;, the upsert sees a spent row and replaces it. Mint before, and the buyer's live-but-about-to-be-claimed code is kept and they get nothing for the purchase they just made. Same two function calls, opposite behaviour, and no test would have told us which order was right without somebody writing down what should happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cost of reusing the table was one discriminator in five places
&lt;/h2&gt;

&lt;p&gt;This is the part I would want to read if somebody else wrote it, because reuse is never free.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;product_type: 'any'&lt;/code&gt; is a new value in a column every code path compared with &lt;code&gt;===&lt;/code&gt;. So the match check gained a second arm:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;-  if (row.product_type !== params.productType) {&lt;/span&gt;
&lt;span class="gi"&gt;+  // A next-purchase code names no product and is good for any of them.&lt;/span&gt;
&lt;span class="gi"&gt;+  if (row.product_type !== params.productType &amp;amp;&amp;amp; row.product_type !== ANY_PRODUCT_TYPE) {&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the eligibility query gained an &lt;code&gt;or&lt;/code&gt;, and the label in the UI became conditional on the campaign rather than a constant string.&lt;/p&gt;

&lt;p&gt;The sharper one was invisible. A row in &lt;code&gt;nurture_discount_codes&lt;/code&gt; doubles as the marker for "we emailed this person recently", and every lifecycle campaign skips anyone who has one. Dropping a next-purchase code in that table would therefore have &lt;strong&gt;silently muted every lifecycle email for a month after each purchase&lt;/strong&gt;. The fix is a &lt;code&gt;&amp;lt;&amp;gt;&lt;/code&gt; in five candidate queries, and the test reads the generated SQL rather than trusting the builder:&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="n"&gt;query&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;PgDialect&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="nf"&gt;sqlToQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mocks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;calls&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="no"&gt;SQL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toMatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;nurture_discount_codes&lt;/span&gt;\&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;campaign&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; \$\&lt;span class="n"&gt;d&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="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toContain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;NEXT_PURCHASE_CAMPAIGN&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;describe.each&lt;/code&gt; over the five campaigns, so adding a sixth campaign that forgets the exclusion fails in a file whose name says what it is about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two small things
&lt;/h2&gt;

&lt;p&gt;The code alphabet is shared with the emailed codes and omits the characters people mistype:&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;/** Same unambiguous alphabet as the emailed codes (no I, O, 0, 1). */&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="no"&gt;CODE_ALPHABET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;32 characters, drawn with &lt;code&gt;crypto.getRandomValues&lt;/code&gt; into a &lt;code&gt;Uint32Array&lt;/code&gt;, so &lt;code&gt;byte % 32&lt;/code&gt; carries no modulo bias: 32 divides 2^32 exactly. Pick an alphabet whose length is a power of two and that whole class of nitpick disappears.&lt;/p&gt;

&lt;p&gt;And the percentage is a constant with a pointed comment:&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;/** Percentage off the next purchase. Quoted in copy; never type the number. */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="no"&gt;NEXT_PURCHASE_DISCOUNT_PERCENT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="cm"&gt;/** How long a minted code can be spent for. Terms section 5.4 states the same; change both. */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="no"&gt;NEXT_PURCHASE_CODE_VALIDITY_DAYS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"Change both" is a comment admitting there is no test pinning the constant to the legal text. There should be. It is on the list.&lt;/p&gt;

&lt;h2&gt;
  
  
  See it
&lt;/h2&gt;

&lt;p&gt;Open &lt;a href="https://cogniprep.app/pricing" rel="noopener noreferrer"&gt;the pricing page&lt;/a&gt; and search for "All Access": zero hits, in the rendered text and in the raw HTML. Search for "20% off" and you get the offer stated twice, once in the explainer and once in the pricing FAQ, both of them interpolating the two constants above rather than typing the numbers. &lt;a href="https://cogniprep.app/terms" rel="noopener noreferrer"&gt;Clause 5.4 of the terms&lt;/a&gt; names the code and the 30 days, and the policy version reads 1.13.0.&lt;/p&gt;

&lt;p&gt;The code itself appears on the purchase confirmation and under "What you own" in the dashboard, served by a route that returns it only while it can still be spent, so there is nothing to see there without buying something.&lt;/p&gt;

&lt;p&gt;An evening's work, 2,029 insertions deleted, and the shipped version is one upsert and a &lt;code&gt;&amp;lt;&amp;gt;&lt;/code&gt;. That is a better trade than it sounds.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>postgres</category>
      <category>stripe</category>
      <category>typescript</category>
    </item>
    <item>
      <title>We sign the content type and the byte length into the upload URL, then re-read the file anyway</title>
      <dc:creator>Daniel Pertu</dc:creator>
      <pubDate>Tue, 10 Nov 2026 09:26:45 +0000</pubDate>
      <link>https://dev.to/daniel_pertu/we-sign-the-content-type-and-the-byte-length-into-the-upload-url-then-re-read-the-file-anyway-49ln</link>
      <guid>https://dev.to/daniel_pertu/we-sign-the-content-type-and-the-byte-length-into-the-upload-url-then-re-read-the-file-anyway-49ln</guid>
      <description>&lt;p&gt;We shipped a form this week for the one support ticket we most want people to open: "the practice did not match my real test". It is also how a buyer claims our refund guarantee, so the fields are structured rather than free text, and it accepts up to three screenshots.&lt;/p&gt;

&lt;p&gt;Three design decisions in it are worth writing down.&lt;/p&gt;

&lt;h2&gt;
  
  
  No schema change, because the fields live in a message
&lt;/h2&gt;

&lt;p&gt;A mismatch report is an ordinary support ticket. Type &lt;code&gt;bug&lt;/code&gt;, category &lt;code&gt;practice-mismatch&lt;/code&gt;, and the structured fields ride in the &lt;code&gt;meta&lt;/code&gt; column of the opening message:&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="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;MismatchReportMeta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;MISMATCH_CATEGORY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;gameId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;gameName&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;gameId&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="n"&gt;employer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;satOn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;providerUnlocked&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;dbUser&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unlocked_providers&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;screenshots&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;stored&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 whole feature added zero migrations. Every existing thing a ticket can do (threading, email replies, the admin queue, cascade on account deletion) works on day one because it is a ticket.&lt;/p&gt;

&lt;p&gt;The field I would point at is &lt;code&gt;providerUnlocked&lt;/code&gt;. It is a copy, at report time, of whether that account had bought that provider. The live answer is one query away, so copying it is redundant by every normal rule. But the report may become a refund claim, and the thing that matters then is whether they had bought it &lt;em&gt;when they reported the mismatch&lt;/em&gt;, not whether they still have it when somebody reads the ticket three days later. A denormalised boolean is the right call when the question is about a moment.&lt;/p&gt;

&lt;h2&gt;
  
  
  The upload binds type and length, and the server does not trust that
&lt;/h2&gt;

&lt;p&gt;Screenshots go straight from the browser to private object storage through a presigned PUT, so a 5 MB file never passes through a serverless function. The signature covers more than the key:&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;return&lt;/span&gt; &lt;span class="nf"&gt;getSignedUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;getClient&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="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;expiresIn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;UPLOAD_URL_TTL_SECONDS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;signableHeaders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'content-type'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'content-length'&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;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;Content type and exact byte length are both signed, so the upload has to be the file the server already approved. That is the part most tutorials stop at, and it is not enough, because it only holds if the storage provider enforces the signed headers and if the client is the one we wrote. So when the report itself arrives, the route reads the object back:&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;// Re-read what is actually in storage. The upload URL bound type and size,&lt;/span&gt;
&lt;span class="c1"&gt;// but this is the check that does not depend on the client or on R2&lt;/span&gt;
&lt;span class="c1"&gt;// enforcing the signed headers.&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="n"&gt;inspections&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="n"&gt;inspections&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;screenshots&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&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;inspectScreenshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&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;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="n"&gt;error&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;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;inspectScreenshot&lt;/code&gt; is a &lt;code&gt;HEAD&lt;/code&gt; for the size plus a 12-byte ranged &lt;code&gt;GET&lt;/code&gt; for the magic bytes, which is about as cheap as a read gets. Then the sniffed type has to equal the type that key was issued for:&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="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;found&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;found&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sniffedType&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;found&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sniffedType&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="n"&gt;found&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;MISMATCH_LIMITS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;screenshotMaxBytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;invalidKeys&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="n"&gt;key&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;Anything that fails is deleted from storage before the 400 goes back, and the response names the failing keys so the dialog can mark those three thumbnails rather than making the person start again.&lt;/p&gt;

&lt;p&gt;The sniffer itself is 38 lines of byte comparisons for the three types we accept, with the only comment that matters sitting on top of it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; * Identifies a PNG, JPEG or WebP from its first bytes, or null for anything
 * else. The declared content type is only a claim; this is what the file is.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It lives in a module with no server-only imports, which means the browser dialog runs the same &lt;code&gt;checkScreenshot&lt;/code&gt; the route runs. One definition of "acceptable image", two call sites, no drift.&lt;/p&gt;

&lt;p&gt;Two smaller choices in the same area. The admin thumbnail is a presigned &lt;code&gt;GET&lt;/code&gt; with a ten minute life that forces &lt;code&gt;ResponseContentType&lt;/code&gt; to the type we verified and &lt;code&gt;ResponseContentDisposition: 'inline'&lt;/code&gt;, so whatever the stored object claims about itself is irrelevant to the browser rendering it. And if one screenshot cannot be signed, it keeps a null URL and the admin view says so, rather than a storage hiccup failing the whole ticket.&lt;/p&gt;

&lt;h2&gt;
  
  
  A cascade cannot reach object storage
&lt;/h2&gt;

&lt;p&gt;This is the bug we nearly shipped. Ticket rows cascade when an account is deleted. The files those rows point at do not.&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;/**
 * Deletes every screenshot a user has uploaded. For account deletion: the
 * ticket rows cascade with the account, but the objects they point at do not.
 * Returns how many objects were removed.
 */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;deleteUserScreenshots&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&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="kt"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It pages &lt;code&gt;ListObjectsV2&lt;/code&gt; by the user's own prefix and batch-deletes, and it is wired into both the self-serve delete and the admin delete, as a best-effort step before the database work:&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;// Support screenshots live in object storage, which the cascade cannot&lt;/span&gt;
&lt;span class="c1"&gt;// reach. Same best-effort rule as the calendar revoke above.&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;deleteUserScreenshots&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&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="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&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;logError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Failed to delete support screenshots before account deletion'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error&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;Every object lives under &lt;code&gt;support/mismatch/&amp;lt;userId&amp;gt;/&lt;/code&gt;, which is what makes a prefix delete correct and also how the route proves a key in a submitted report belongs to the person submitting it. The feature's flat layout is doing two jobs.&lt;/p&gt;

&lt;p&gt;That commit also did the part that is not code: a new paragraph in the privacy policy naming the new category of data, the storage provider entry extended, the retention line amended, and the policy version moved from 1.9.0 to 1.10.0. If you add a place where personal data lands and you do not move that number, the policy is now wrong and nothing will tell you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two details only a phone exposes
&lt;/h2&gt;

&lt;p&gt;The file input's &lt;code&gt;accept&lt;/code&gt; attribute lists exactly the three types we take:&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;/** The `accept` attribute for the file input. Listing only these makes iOS hand over HEIC photos as JPEG. */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="no"&gt;SCREENSHOT_ACCEPT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;SCREENSHOT_TYPES&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="s1"&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;An iPhone screenshot of an assessment is a HEIC file. Name the types you want and iOS converts on the way out; name &lt;code&gt;image/*&lt;/code&gt; and you get HEIC, which fails the sniffer, which is a dead end for the person reporting a real problem.&lt;/p&gt;

&lt;p&gt;And the date field validates one day into the future:&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;/**
 * Whether a YYYY-MM-DD string is a real calendar date no later than tomorrow
 * (UTC). One day of slack covers anyone whose local date is ahead of UTC.
 */&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Someone in Auckland reporting a test they sat today should not be told they are reporting a test from the future.&lt;/p&gt;

&lt;h2&gt;
  
  
  See it
&lt;/h2&gt;

&lt;p&gt;Open &lt;a href="https://cogniprep.app/privacy" rel="noopener noreferrer"&gt;our privacy policy&lt;/a&gt; and search for "screenshots". Four hits: the new paragraph describing what a mismatch report stores, the storage provider entry, the line that says screenshots are deleted with your account, and the retention row. The header reads "Last updated: 8 November 2026 · Version: 1.10.0". The refund guarantee the reports feed is clause 5.3 of &lt;a href="https://cogniprep.app/terms" rel="noopener noreferrer"&gt;the terms&lt;/a&gt;, at version 1.13.0.&lt;/p&gt;

&lt;p&gt;The form itself sits behind an account on the help and support pages, and there is a link to it on the screen at the end of a practice run. Everything it reports about is on the open side of the site: &lt;a href="https://cogniprep.app/games" rel="noopener noreferrer"&gt;the test library&lt;/a&gt; and &lt;a href="https://cogniprep.app/tests" rel="noopener noreferrer"&gt;the guides&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The honest summary: the presigned upload took an afternoon, the second check took an hour, and the twenty lines that delete the objects when an account goes are the only part of it I would have been embarrassed to leave out.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>security</category>
      <category>nextjs</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Our employer pages now name the tests, and three different sentences say who told us</title>
      <dc:creator>Daniel Pertu</dc:creator>
      <pubDate>Tue, 10 Nov 2026 09:24:49 +0000</pubDate>
      <link>https://dev.to/daniel_pertu/our-employer-pages-now-name-the-tests-and-three-different-sentences-say-who-told-us-2cjh</link>
      <guid>https://dev.to/daniel_pertu/our-employer-pages-now-name-the-tests-and-three-different-sentences-say-who-told-us-2cjh</guid>
      <description>&lt;p&gt;A candidate interviewing at UBS wants to know which tasks UBS sends, not which tasks the vendor sells. We have 180 employer guides and until this week none of them answered that, because answering it honestly is hard: employers almost never publish the names of the tests they use.&lt;/p&gt;

&lt;p&gt;So the new module is small on purpose. 21 lineups, covering 21 of the 180 employer pages, 10 providers, 50 named tasks. The component returns &lt;code&gt;null&lt;/code&gt; for the other 159.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nc"&gt;EmployerTaskLineups&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;employer&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="n"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;employer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;lineups&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;taskLineupsForEmployer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;slug&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="n"&gt;lineups&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&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="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="p"&gt;.&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  One field decides the heading and the attribution
&lt;/h2&gt;

&lt;p&gt;The interesting part of the type is a three-value union:&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;/**
 * - `named`: the employer or the vendor names the tasks.
 * - `described`: the employer describes each task in detail without naming it,
 *   and the descriptions match the vendor's own task descriptions.
 * - `reported`: a third party (candidate reports or a prep site) names them.
 */&lt;/span&gt;
&lt;span class="n"&gt;basis&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'named'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="s1"&gt;'described'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="s1"&gt;'reported'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is not metadata. It changes what the page says, in two places. The heading:&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="n"&gt;lineup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;basis&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;'reported'&lt;/span&gt;
  &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="sb"&gt;`${provider} tasks reported at ${employer}`&lt;/span&gt;
  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sb"&gt;`${provider} tasks ${employer} uses`&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and the line under the list:&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="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;lead&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="n"&gt;lineup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;basis&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;'reported'&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="s1"&gt;'Reported by '&lt;/span&gt;
  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;lineup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;basis&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;'described'&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="s1"&gt;'Described, not named, in the '&lt;/span&gt;
  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Source: '&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Of the 21 lineups, 19 are &lt;code&gt;named&lt;/code&gt;, one is &lt;code&gt;reported&lt;/code&gt; and one is &lt;code&gt;described&lt;/code&gt;. The &lt;code&gt;described&lt;/code&gt; one is the reason the field exists at all. Procter and Gamble's careers material walks through three interactive assessments in detail without ever naming them: fill the gaps in equations, pick the branch that reorders the symbols, recall the dot positions. Those are three named tasks from a vendor we cover, and matching the descriptions is a judgement call, so the page says it is a judgement call. The heading still says P&amp;amp;G uses them. The line underneath says "Described, not named, in the P&amp;amp;G Interactive Assessment guide, checked 2026".&lt;/p&gt;

&lt;p&gt;Compare the single &lt;code&gt;reported&lt;/code&gt; entry, where it reads "Reported by WikiJob, 2025", and the heading changes to "Arctic Shores tasks reported at PwC". Same component, different claim, different sentence.&lt;/p&gt;

&lt;h2&gt;
  
  
  "checked 2026" rather than a year we made up
&lt;/h2&gt;

&lt;p&gt;Every lineup carries a source with a year, and the year means different things:&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="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="cm"&gt;/** Year the source dates from, or the year it was checked live if undated. */&lt;/span&gt;
  &lt;span class="n"&gt;year&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;undated&lt;/span&gt;&lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="kt"&gt;boolean&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;18 of the 21 sources are undated careers pages, so the rendered line says &lt;code&gt;checked 2026&lt;/code&gt;, not &lt;code&gt;2026&lt;/code&gt;. One line of code, and it stops a careers page with no date on it from acquiring one in our markup.&lt;/p&gt;

&lt;p&gt;The other number worth knowing: those 21 sources sit on only 13 hosts, and nine of the 21 come from one vendor's own customer case studies. The pattern across the whole sweep is that the vendor will tell you which employers it sells to long before the employer will tell you which vendor it buys from.&lt;/p&gt;

&lt;h2&gt;
  
  
  The list degrades in two steps and looks the same either way
&lt;/h2&gt;

&lt;p&gt;Each task optionally carries the id of our practice game for it, and the name becomes a link to that test's page if one exists:&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="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gameId&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;testPageForGame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gameId&lt;/span&gt;&lt;span class="p"&gt;)&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Of the 50 tasks, 31 render as links. Ten carry no game id at all, because the employer names a stage we have no practice for and leaving it out would misrepresent their process. Another nine do have a practice game but no per-test page yet, so &lt;code&gt;testPageForGame&lt;/code&gt; returns &lt;code&gt;undefined&lt;/code&gt; and they render as plain text. Eight of those nine are the same aptitude test, reused through a shared constant across eight employers:&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="no"&gt;GIA&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;LineupTask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Aptitude assessment (GIA)'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gameId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'thm-gia'&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 reader cannot tell the two cases apart, which is the point. A task you cannot practise with us still belongs on the list.&lt;/p&gt;

&lt;h2&gt;
  
  
  The guard test generates one case per lineup
&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;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;lineup&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="no"&gt;TASK_LINEUPS&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="n"&gt;label&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sb"&gt;`${lineup.employerSlug} / ${lineup.provider}`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`${label}: belongs to an existing employer page`&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="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="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;path&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="nb"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cwd&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="s1"&gt;'app'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'employers'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lineup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;employerSlug&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'page.tsx'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;existsSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sb"&gt;`missing ${page}`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBe&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="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;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;A loop that emits an &lt;code&gt;it&lt;/code&gt; per entry rather than one &lt;code&gt;it&lt;/code&gt; with a loop inside costs nothing and buys you a failure message that names the employer. The assertions are the ones a sourced claim needs: an https source with a non-empty label, a year between 2015 and this year, at least one task, no em dashes anywhere in the entry, at most one lineup per employer and provider, and the one that catches real mistakes:&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="n"&gt;game&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;GAMES&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;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gameId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;game&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sb"&gt;`unknown game id ${task.gameId}`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBeDefined&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="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;game&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lineup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;provider&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;A task cannot point at a game belonging to a different vendor. That is exactly the slip you make at 1am when two providers both sell something called a numerical reasoning test.&lt;/p&gt;

&lt;p&gt;One last detail, since every list item here is a tappable link on a phone: the anchors carry &lt;code&gt;min-h-11 sm:min-h-0&lt;/code&gt;, so they are 44px targets on a phone and ordinary inline text on a desktop.&lt;/p&gt;

&lt;h2&gt;
  
  
  See it
&lt;/h2&gt;

&lt;p&gt;Open &lt;a href="https://cogniprep.app/employers/ubs" rel="noopener noreferrer"&gt;the UBS guide&lt;/a&gt; and you get four named tasks with "Source: UBS early careers page, checked 2026" under them. &lt;a href="https://cogniprep.app/employers/pwc" rel="noopener noreferrer"&gt;PwC&lt;/a&gt; says "reported at" in the heading and "Reported by WikiJob, 2025" underneath. &lt;a href="https://cogniprep.app/employers/procter-and-gamble" rel="noopener noreferrer"&gt;Procter and Gamble&lt;/a&gt; is the described one, and its list is an &lt;code&gt;&amp;lt;ol&amp;gt;&lt;/code&gt; rather than a &lt;code&gt;&amp;lt;ul&amp;gt;&lt;/code&gt;, because that source gives the tasks in the order candidates sit them.&lt;/p&gt;

&lt;p&gt;Then open &lt;a href="https://cogniprep.app/employers/amazon" rel="noopener noreferrer"&gt;Amazon&lt;/a&gt; and search for "Source:". Nothing, because there is no lineup for it and the section does not render. That is 159 pages waiting for somebody to publish something.&lt;/p&gt;

&lt;p&gt;The full set is linked from &lt;a href="https://cogniprep.app/employers" rel="noopener noreferrer"&gt;/employers&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Worth contrasting with the other content type we shipped the same day: on our per-company interview question pages, every question carries the URL it was reported at and the page renders none of them. Here the source is the first thing under the list. The difference is what the claim is about. "These are the questions people report" is a claim about an aggregate. "This employer uses these tests" is a claim about a named third party's hiring process, and a reader deserves to see who said it before deciding whether to believe us.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>typescript</category>
      <category>writing</category>
      <category>seo</category>
    </item>
    <item>
      <title>One wrong sentence about a vendor lived in eleven files on our site</title>
      <dc:creator>Daniel Pertu</dc:creator>
      <pubDate>Tue, 10 Nov 2026 09:22:44 +0000</pubDate>
      <link>https://dev.to/daniel_pertu/one-wrong-sentence-about-a-vendor-lived-in-eleven-files-on-our-site-1i1j</link>
      <guid>https://dev.to/daniel_pertu/one-wrong-sentence-about-a-vendor-lived-in-eleven-files-on-our-site-1i1j</guid>
      <description>&lt;p&gt;Our site explains assessments that other companies sell. That means most of our content is a claim about somebody else's product: how long their test runs, how many items it has, whether you can retake it. Every one of those claims can go stale without us touching a line of code, because the vendor ships a new version and says so on their own help centre.&lt;/p&gt;

&lt;p&gt;Last weekend we read the vendors' own documents again and fixed what they contradicted. Thirteen commits, 160 distinct files, 1,525 insertions against 1,371 deletions, and not one line of behaviour changed. It was all prose.&lt;/p&gt;

&lt;p&gt;Here is what the sweep actually looked like.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one that spread the furthest
&lt;/h2&gt;

&lt;p&gt;We said that an AssessFirst result stays valid for twelve months and that an early reset needs the recruiter to ask. The vendor's current documentation says something different: a recruiter can invite you to retake at any time, and results no longer expire after a year. One of their older help articles still says they do, which is presumably where ours came from.&lt;/p&gt;

&lt;p&gt;One sentence, and the diff touched eleven files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app/blogs/assessfirst-brain-swipe-drive-explained/page.tsx
app/cheating/assessfirst/page.tsx
app/employers/lcl/page.tsx
app/employers/sncf-reseau/page.tsx
app/games/assessfirst/page.tsx
components/games/af-swipe/AFSwipeGame.tsx
docs/providers/assessfirst.md
lib/cheating/registry.ts
lib/games/providers/assessfirst/insights.ts
__tests__/assessfirst/af-registration.test.ts
__tests__/assessfirst/af-scorers-feedback.test.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A hub page, two employer guides, a blog article, a strategy guide, the instruction screen a candidate reads before a practice run, the registry that feeds several of those, the research dossier it all came from, and two tests. Two test assertions moved with it. One of them used to check only that the copy mentioned a year, which is exactly the assertion that would have let the wrong sentence sit there forever:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;-      expect(text).toMatch(/year/);&lt;/span&gt;
&lt;span class="gi"&gt;+      expect(text).toMatch(/recruiter invites you to retake/);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The one we invented
&lt;/h2&gt;

&lt;p&gt;This is the one I would rather not publish. On the Cappfinity strengths assessment we had written a confident, actionable mechanism:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Cappfinity records not only what you choose but how quickly and confidently you choose it. The reasoning is that fast, decisive answers to options fitting your genuine strengths signal authentic preference, whereas a slow answer suggests you are constructing a response rather than reporting one, so it can carry less weight.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The vendor's own candidate help says its behavioural questions are untimed, apart from video and written responses, and that where an assessment is time recorded you are told in advance. So our paragraph was not a stale figure. It was a theory, written with the cadence of a fact, that told readers to hurry when the vendor tells them not to overthink. The section heading went from "How you answer is part of the answer" to "Check whether you are being timed", and the advice is now: read the instructions, and if a section says it is time recorded then work quickly, otherwise take the time you need.&lt;/p&gt;

&lt;p&gt;A wrong number is embarrassing. A wrong mechanism changes what somebody does in a real assessment.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one where our numbers described a product generation that had moved on
&lt;/h2&gt;

&lt;p&gt;Our SHL figures were not invented. They were right about a previous generation. "Around eighteen questions, roughly seventeen to twenty-five minutes, roughly 75 seconds a question" described the older multiple-choice Verify tests; the current Interactive versions allow 18 minutes for up to ten questions and have you build charts and fill in spreadsheets. "Inductive Reasoning is 24 shape-sequence items in about 24 minutes" became "up to 15 items in 18 minutes on shape and symbol sequences, and the older multiple-choice version up to 18 in 24 minutes".&lt;/p&gt;

&lt;p&gt;Two habits came out of that one. Name the generation, because both are in the field and a candidate's invitation decides which they get. And write "up to", because the tests are adaptive and a fixed count is a promise we are not in a position to make. The page now says so in plain words: "These are SHL's maximums. The tests adapt to your answers, so you may see fewer questions."&lt;/p&gt;

&lt;h2&gt;
  
  
  The one that went the other way
&lt;/h2&gt;

&lt;p&gt;Not every correction removed a claim. One added one back.&lt;/p&gt;

&lt;p&gt;For the LTP G-test our copy was carefully hedged: "LTP does not publish its time limit; preparation sites report 20 minutes, which is what our version uses." The hedge was wrong. LTP's own candidate portal does publish it, in Polish, and the figure is the same 20 minutes. The code comment that used to warn the next reader off now carries the quote instead:&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;// LTP's portal instruction: "50 zadan, na ktorych rozwiazanie masz 20&lt;/span&gt;
&lt;span class="c1"&gt;// minut" (see lib/games/test-pages/ltp.ts for the source).&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reverse case also turned up: on one Arctic Shores task we had "Rounds: reported as 10, 20 or 30". Those three numbers are a prep site's own practice settings, not a fact about the real task. They are now "Not published", which is less satisfying and true. Elsewhere a single word went: an unsourced "free" came off a card, because nobody had checked whether that one is free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it was 160 files and not one constant
&lt;/h2&gt;

&lt;p&gt;The obvious engineering reaction is that a figure restated in eleven places is a missing constant. We thought about it and left it alone.&lt;/p&gt;

&lt;p&gt;The 160 files break down as 55 instruction components, 36 modules under &lt;code&gt;lib/games&lt;/code&gt;, 19 files under &lt;code&gt;app/games&lt;/code&gt;, 19 blog articles, 14 employer guides, 7 strategy guides, a research dossier and 4 test files, plus five singletons. None of those surfaces wants the same sentence. A hub needs one line of orientation, a per-test page needs the format in detail, an employer guide needs it in the context of that employer's process, an instruction screen needs the one number that matters in the next thirty seconds. A shared &lt;code&gt;SHL_VERIFY_NUMERICAL_DURATION&lt;/code&gt; would be interpolated into eleven sentences whose framing is the thing that actually goes wrong.&lt;/p&gt;

&lt;p&gt;What is missing is not a constant. It is the review date, which these pages now carry per entry, and the dossier that every line traces back to. The sweep is the cost of writing prose about somebody else's product, and it is a cost that recurs.&lt;/p&gt;

&lt;h2&gt;
  
  
  See it
&lt;/h2&gt;

&lt;p&gt;Open &lt;a href="https://cogniprep.app/cheating/assessfirst" rel="noopener noreferrer"&gt;the AssessFirst strategy guide&lt;/a&gt; and search the page for "twelve months". Nothing. Search for "recruiter invites you" and you get nine hits, which is the new rule restated in every place the page needs it.&lt;/p&gt;

&lt;p&gt;Then open &lt;a href="https://cogniprep.app/games/shl" rel="noopener noreferrer"&gt;the SHL hub&lt;/a&gt; and search for "24 questions in 24 minutes": gone. "Verify Interactive" appears eleven times. On &lt;a href="https://cogniprep.app/blogs/cappfinity-strengths-assessment-tips" rel="noopener noreferrer"&gt;the Cappfinity tips article&lt;/a&gt;, "time recorded" appears five times and "how quickly and confidently" appears zero. On &lt;a href="https://cogniprep.app/games/ltp/g-test" rel="noopener noreferrer"&gt;the LTP G-test page&lt;/a&gt;, "preparation sites report" is gone and the 20 minutes stayed.&lt;/p&gt;

&lt;p&gt;The practice itself is at &lt;a href="https://cogniprep.app" rel="noopener noreferrer"&gt;cogniprep.app&lt;/a&gt;. The thing I would take from this if you run a content site about other people's products: the vendors publish more than you think, they change it without telling you, and the copy you are proudest of is the copy most likely to be a theory.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>writing</category>
      <category>seo</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Every question on our 20 new pages carries the URL it was reported at, and the pages show none of them</title>
      <dc:creator>Daniel Pertu</dc:creator>
      <pubDate>Tue, 10 Nov 2026 09:20:27 +0000</pubDate>
      <link>https://dev.to/daniel_pertu/every-question-on-our-20-new-pages-carries-the-url-it-was-reported-at-and-the-pages-show-none-of-fj7</link>
      <guid>https://dev.to/daniel_pertu/every-question-on-our-20-new-pages-carries-the-url-it-was-reported-at-and-the-pages-show-none-of-fj7</guid>
      <description>&lt;p&gt;Candidates who get invited to a recorded video interview search for it by the name of the firm, not the name of the platform. "goldman sachs hirevue questions" is a real query; "video interview platform best practices" is not. Our general guide could not answer the first one, so this weekend we shipped 20 pages that do, one per firm, at &lt;a href="https://cogniprep.app/interview/hirevue-interview-questions" rel="noopener noreferrer"&gt;/interview/hirevue-interview-questions&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The interesting part is not the pages. It is the field we built, shipped, and then stopped rendering 62 minutes later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Every question is a claim, so every question got a URL
&lt;/h2&gt;

&lt;p&gt;The content type for these pages has one unusual rule. A question is not prose we wrote; it is something a candidate reported somewhere, so each one is a record with the page it was reported 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="cm"&gt;/** One reported question, with where it was seen (recorded, not rendered). */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;CompanyQuestion&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="cm"&gt;/** The question as reported. Light paraphrase allowed. */&lt;/span&gt;
  &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="cm"&gt;/** The division or track it was reported for, only where the source says so. */&lt;/span&gt;
  &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="cm"&gt;/** Site the question was reported on, e.g. "eFinancialCareers". Not rendered. */&lt;/span&gt;
  &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="cm"&gt;/** Year of the report, where the page is dated. Not rendered. */&lt;/span&gt;
  &lt;span class="n"&gt;year&lt;/span&gt;&lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="kt"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="cm"&gt;/** Exact URL of the page the question appears on. https only. */&lt;/span&gt;
  &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&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;Across the 20 firms that comes to 271 questions. They resolve to 44 distinct URLs on 18 hosts, with a long tail: 84 questions come from one interview-advice blog, 90 from the two country editions of a finance careers site, 27 from a third, and eight hosts contribute 16 questions between them, two of them one each. 255 of the 271 carry a year, running from 2018 to 2026.&lt;/p&gt;

&lt;p&gt;Separately from the questions, each page carries its own &lt;code&gt;sources&lt;/code&gt; list for the stage facts and the advice: 121 URLs over 49 hosts, all https, every one opened by hand. That is where you find out how little the firms publish. The 20 pages have 146 rows describing where the interview sits in the process, how long it runs, how many questions, how much thinking time, whether you can retake. &lt;strong&gt;68 of those 146 rows read "Not published."&lt;/strong&gt; The rule in the type is that a gap stays a gap:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The format (question count, thinking and answer time, retakes) only as sources say it. Never fill a gap with the platform default.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Then we deleted the citation links
&lt;/h2&gt;

&lt;p&gt;The first version rendered each question with its source underneath as a small &lt;code&gt;rel="nofollow"&lt;/code&gt; link, because if you can cite it you should. One commit later, that came out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;-                  {count} questions from {entry.company} HireVue interviews, each linked to where it&lt;/span&gt;
&lt;span class="gd"&gt;-                  was reported. Prepare a story for each type rather than a script for each question.&lt;/span&gt;
&lt;span class="gi"&gt;+                  {count} questions from {entry.company} HireVue interviews. Prepare a story for each&lt;/span&gt;
&lt;span class="gi"&gt;+                  type rather than a script for each question.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Net effect of that commit: 17 insertions, 25 deletions, four files. The doc comment on the shell now says why.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; * Sources are kept in the data but never rendered: the owner wants only the
 * questions on the page, with no links out to the sites that reported them.
 * Each question's `url` and `entry.sources` are the record behind the page.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That reads like vanity until you look at what the page is for. Someone landing on it has an interview in three days. A row of citations under every question is 271 chances to leave for a competing prep site, and it answers a question nobody on that page is asking. The URL is not there for the reader. It is there so that the sentence above it can be checked by us, and so that nobody on the team can add a question they half remember.&lt;/p&gt;

&lt;p&gt;So the guard test does the checking instead of the reader:&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;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'cites an https source on every question'&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="o"&gt;=&amp;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="n"&gt;entry&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="no"&gt;COMPANY_QUESTIONS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;companyQuestionCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&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="nf"&gt;toBeGreaterThan&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="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="n"&gt;group&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;questionGroups&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="n"&gt;item&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;questions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toMatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;/^&lt;/span&gt;&lt;span class="n"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;\&lt;span class="o"&gt;/&lt;/span&gt;\&lt;span class="c1"&gt;//);&lt;/span&gt;
        &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&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;span class="ow"&gt;not&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="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;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rest of that file is the same set of failures every content registry needs guarding against: a registry entry with no route folder (a sitemap URL that 404s), a folder with no entry (a build that throws, because the lookup throws rather than returning &lt;code&gt;undefined&lt;/code&gt;), a title over 48 characters (the layout appends 12 more), the same question listed twice on one page, a "read next" link to a page that does not exist, a path that robots.txt happens to block, and an em dash anywhere in the content.&lt;/p&gt;

&lt;h2&gt;
  
  
  See it
&lt;/h2&gt;

&lt;p&gt;Open &lt;a href="https://cogniprep.app/interview/hirevue-interview-questions/goldman-sachs" rel="noopener noreferrer"&gt;the Goldman Sachs page&lt;/a&gt;, view source, and search the HTML for &lt;code&gt;efinancialcareers&lt;/code&gt;. Zero hits. Search for any of the hosts above and you get zero again. I ran that sweep over all 20 live pages just now: the only external hostnames anywhere in the served HTML of any of them are &lt;code&gt;js.stripe.com&lt;/code&gt; and &lt;code&gt;api.stripe.com&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then count the questions in the rendered page: 20 on that one, which is exactly what the data holds. The page with the fewest has four, because &lt;a href="https://cogniprep.app/interview/hirevue-interview-questions/oliver-wyman" rel="noopener noreferrer"&gt;Oliver Wyman&lt;/a&gt; is the firm with the least reported and the rule is that it gets a shorter page rather than a padded one. Six of the 20 firms also have an employer guide on the site, and only those six link to one, because the test fails if a link points at a folder with no &lt;code&gt;page.tsx&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The practice the pages hand off to is at &lt;a href="https://cogniprep.app/interview" rel="noopener noreferrer"&gt;/interview&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Keeping the citation in the data and off the page felt wrong for about a day. It stopped feeling wrong when the first factual correction arrived and I could open the exact URL the line came from in four seconds.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>nextjs</category>
      <category>seo</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Our app says: the site didn't say, so we assumed. Then it rings the field each assumption is about</title>
      <dc:creator>Daniel Pertu</dc:creator>
      <pubDate>Sun, 08 Nov 2026 11:22:01 +0000</pubDate>
      <link>https://dev.to/daniel_pertu/our-app-says-the-site-didnt-say-so-we-assumed-then-it-rings-the-field-each-assumption-is-about-1fpg</link>
      <guid>https://dev.to/daniel_pertu/our-app-says-the-site-didnt-say-so-we-assumed-then-it-rings-the-field-each-assumption-is-about-1fpg</guid>
      <description>&lt;p&gt;When software writes a document for you, the hard part is not writing it. It is telling you which bits it made up.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://nakodo.app" rel="noopener noreferrer"&gt;Nakodo&lt;/a&gt; starts a campaign by writing a brief, which &lt;a href="https://nakodo.app/how-it-works" rel="noopener noreferrer"&gt;how it works&lt;/a&gt; describes with the line that matters here: everything in it can be changed before anything happens. A brief written from a website always contains guesses, because websites do not state everything, and the app keeps those guesses alongside it. The question is what the screen does with them.&lt;/p&gt;

&lt;p&gt;The answer is 133 lines, a panel above the document and a ring around the fields inside it, and most of the decisions in it are about not nagging.&lt;/p&gt;

&lt;h2&gt;
  
  
  One state, two places, no prop drilling
&lt;/h2&gt;

&lt;p&gt;The panel is at the top of the page and the marks are deep inside an 859-line form, so both read from one context:&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="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Marks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;BriefPart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Marked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;createContext&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;marks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Marks&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;mark&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;guesses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Assumption&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;void&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;marks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;mark&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The default value is a working no-op: an empty map and a function that does nothing. That is not defensive code, it is what lets the marked fields be used on a page with no panel at all, with nothing to configure and no "if a provider exists" branch anywhere.&lt;/p&gt;

&lt;p&gt;The marked field is the same idea again:&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Highlight&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;part&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;className&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;notes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Marked&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;marks&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;part&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;notes&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;&amp;lt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&amp;gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;data-guess&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;cn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rounded-md ring-2 ring-offset-4 ring-highlight ring-offset-card motion-safe:animate-in motion-safe:fade-in&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"mt-2.5 space-y-1 text-[13px] text-muted-foreground"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;n&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"flex gap-2"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AttentionMark&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Guess to check"&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"mt-[7px]"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
            &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unmarked, it renders its children and nothing else: no wrapper element, no class, no layout change. So all eight places in the form that can carry a guess are wrapped (six or seven of them render, depending on whether the campaign is looking for creators or for businesses), and in the normal case the component is invisible in the output as well as on screen. The ring is additive, which is the only way a highlight can be switched on inside a form without moving anything.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ring-offset-4&lt;/code&gt; with &lt;code&gt;ring-offset-card&lt;/code&gt; is the detail that makes it look deliberate: the ring sits four pixels off the field with the card's own colour in the gap, so it reads as a halo rather than a border somebody added to an input.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two buttons, both final
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;settle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;approved&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Looks good&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt; &lt;span class="na"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"outline"&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;settle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fixing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Make changes&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no dismiss cross. A cross invites the one behaviour this panel exists to prevent, which is making it go away without reading it. Instead there are two answers, and the important property is that they are equally final:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;settle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;how&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;approved&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;fixing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;setDone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;how&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;how&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fixing&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;mark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;guesses&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;placed&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="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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[data-guess]&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nf"&gt;scrollIntoView&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;behavior&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;smooth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;block&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;center&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nf"&gt;start&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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;checkedGuessesAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;campaignId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One server action for both answers, because the thing being recorded is not which button you pressed. It is that you have been asked. A panel that reappears until you press the "right" button is a panel that gets dismissed reflexively on the second viewing.&lt;/p&gt;

&lt;p&gt;Note the order: the local state moves first, the write goes after, inside a transition, and the result is thrown away. The comment above it is the policy:&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="c1"&gt;// Saved as soon as either button is pressed, so the panel is gone next time&lt;/span&gt;
&lt;span class="c1"&gt;// too. Nothing is shown if it fails: the guesses simply come back.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the nicest kind of error handling to write: the failure mode of "we could not record that you read this" is "you are asked again", which is the behaviour you would have chosen anyway. Showing a red toast for it would be telling the user about a problem that is not theirs and has already been handled.&lt;/p&gt;

&lt;h2&gt;
  
  
  The scroll uses an attribute, not a ref
&lt;/h2&gt;

&lt;p&gt;The panel has to scroll to the first marked field, which lives in a different component several levels down. The usual answers are a ref passed through context or a ref callback registry keyed by part. This does neither: the ring sets &lt;code&gt;data-guess&lt;/code&gt;, and the panel queries for it.&lt;/p&gt;

&lt;p&gt;That is fine here for a specific reason. The marks render in the same tick as the state change, so a &lt;code&gt;requestAnimationFrame&lt;/code&gt; is enough to let them exist before the query runs, and "the first &lt;code&gt;[data-guess]&lt;/code&gt; in the document" is exactly the definition wanted: the earliest part of the form with something to check. A ref registry would have given the same answer and a sorting problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two caps on how loud it gets
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Two notes a part at most: past that the brief reads as a page of warnings&lt;/span&gt;
&lt;span class="c1"&gt;// instead of fields to change.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MAX_PER_PART&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and, for a guess that does not name a part, nothing is ringed at all:&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="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;g&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;guesses&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;part&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ringing everything for an unplaced guess would point at the whole document, which tells the reader nothing. Those guesses stay in the panel as text instead, and the follow-up copy branches on how many of each there were:&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;placed&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="s2"&gt;Change these in the brief below.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;loose&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;We've highlighted the parts you need to change.&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;We've highlighted where most of these go. The rest:&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;Three sentences for three states, and the third one is the only reason the split exists: a mixed case where some guesses are ringed and some are not would otherwise produce a confident sentence that is partly a lie. The whole state lives under &lt;code&gt;role="status"&lt;/code&gt;, so a screen reader hears the change rather than silently losing a panel.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two findings
&lt;/h2&gt;

&lt;p&gt;The list keys are the note text: &lt;code&gt;key={n}&lt;/code&gt; in the marks and &lt;code&gt;key={g.note}&lt;/code&gt; in the panel list. Two identical guesses about the same part of the brief would collide, which is unlikely and not impossible, and an index key would be fine in a list that never reorders.&lt;/p&gt;

&lt;p&gt;The bigger one is that "Make changes" settles the guesses permanently, so somebody who presses it, gets called away and comes back tomorrow has a brief with no rings on it and no way to ask for them again. The honest fix is to record the two answers as different things after all: approved means never again, fixing means show it until the brief is saved. That is one more column and a condition, and it would mean the panel is the only part of this screen that remembers what you meant rather than only what you did.&lt;/p&gt;

</description>
      <category>react</category>
      <category>ux</category>
      <category>typescript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Our setup wizard keeps the step in the URL and the answers nowhere, so Back works and a reload starts over</title>
      <dc:creator>Daniel Pertu</dc:creator>
      <pubDate>Sun, 08 Nov 2026 11:20:28 +0000</pubDate>
      <link>https://dev.to/daniel_pertu/our-setup-wizard-keeps-the-step-in-the-url-and-the-answers-nowhere-so-back-works-and-a-reload-2no0</link>
      <guid>https://dev.to/daniel_pertu/our-setup-wizard-keeps-the-step-in-the-url-and-the-answers-nowhere-so-back-works-and-a-reload-2no0</guid>
      <description>&lt;p&gt;Setting up a campaign in &lt;a href="https://nakodo.app" rel="noopener noreferrer"&gt;Nakodo&lt;/a&gt; is a wizard, which is a word that usually comes with a drafts table, an autosave, a half-finished row somebody has to clean up, and a resume link. This one has none of that, and the reason is one line of policy: nothing exists in the database until the last step before the review.&lt;/p&gt;

&lt;p&gt;What that buys is worth the trade I will come to at the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  The number of steps is a function
&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;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Step&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;kind&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;where&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;businesses&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;emails&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;website&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;review&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&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;setupStepList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CampaignKind&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="nx"&gt;emails&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Step&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;kind&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;where&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;kind&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;businesses&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;businesses&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="kd"&gt;as const&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;span class="nx"&gt;emails&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;emails&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="kd"&gt;as const&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="s2"&gt;website&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;review&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two conditional steps, so the wizard is four, five or six steps long. "Businesses and places" appears only for a campaign looking for businesses rather than creators. "Daily emails" appears only on a plan that can run more than one campaign at a time, because that step is about splitting a day's allowance between campaigns and there is nothing to split when there is one. On the &lt;a href="https://nakodo.app/pricing" rel="noopener noreferrer"&gt;Free plan&lt;/a&gt; it never appears at all.&lt;/p&gt;

&lt;p&gt;The list lives in its own module rather than inside the component, and the file comment says why: the review page at the end is a server component on its own route, and it has to number the steps the same way. One function, two renderers, no chance of the stepper saying "Step 4 of 5" on one page and "4 of 6" on the next.&lt;/p&gt;

&lt;h2&gt;
  
  
  The step is in the URL, written by the History API
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;go&lt;/span&gt; &lt;span class="o"&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;Step&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="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;href&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;s&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;kind&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;step&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;else&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;searchParams&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;step&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="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pushState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="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="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;}${&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;search&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;No &lt;code&gt;router.push&lt;/code&gt;. The App Router supports the native history methods and keeps &lt;code&gt;useSearchParams&lt;/code&gt; in step with them, so a &lt;code&gt;pushState&lt;/code&gt; updates the component that reads the step without touching the server. Moving between steps is therefore free: no request, no server render, no loading state, no revalidation of a page whose data has not changed. And because the step is a real history entry, the browser's Back button steps back through the wizard with no code of ours involved.&lt;/p&gt;

&lt;p&gt;The first step deletes the parameter instead of setting &lt;code&gt;step=kind&lt;/code&gt;, so the start of the wizard has one canonical URL rather than two.&lt;/p&gt;

&lt;h2&gt;
  
  
  The URL cannot name a step you have not reached
&lt;/h2&gt;

&lt;p&gt;Putting the step in the URL means anyone can type one. The gate is three lines:&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;reached&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setReached&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;steps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setupStepList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;daily&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;requested&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;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&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="s2"&gt;step&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;as &lt;/span&gt;&lt;span class="nx"&gt;Step&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;at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;requested&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;requested&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;reached&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;requested&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An unknown step gives &lt;code&gt;indexOf&lt;/code&gt; of minus one, which fails the first test. A step further along than you have got fails the second. Either way you land on the first step, which is the only step that is always valid.&lt;/p&gt;

&lt;p&gt;A client-side gate would be unacceptable if it were protecting anything. It is not: no step writes to the database, no step authorises anything, and the campaign does not exist yet. The gate is there so the screen is never asked to render a question that depends on an answer it does not have. The real validation is the server action at the end, which gets the whole set of answers in one form submission.&lt;/p&gt;

&lt;p&gt;The reload case is handled explicitly, and it is the one place the design shows its seam:&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="c1"&gt;// A reload loses the choices, so a step in the URL that can't be shown yet&lt;/span&gt;
&lt;span class="c1"&gt;// is dropped rather than left pointing somewhere else.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;params&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;step&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;at&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="nf"&gt;useEffect&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="nx"&gt;stale&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;history&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replaceState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;stale&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;replaceState&lt;/code&gt;, not &lt;code&gt;pushState&lt;/code&gt;, because a wiped parameter should not become a history entry you can press Back into.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one URL that is allowed to skip ahead
&lt;/h2&gt;

&lt;p&gt;The home page's hero is a single input. You can read it on &lt;a href="https://nakodo.app" rel="noopener noreferrer"&gt;the live page&lt;/a&gt;:&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;input&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"hero-site"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"site"&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"yourbrand.com"&lt;/span&gt;
       &lt;span class="na"&gt;inputMode=&lt;/span&gt;&lt;span class="s"&gt;"url"&lt;/span&gt; &lt;span class="na"&gt;autoComplete=&lt;/span&gt;&lt;span class="s"&gt;"url"&lt;/span&gt; &lt;span class="na"&gt;maxLength=&lt;/span&gt;&lt;span class="s"&gt;"253"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That address survives sign-up, through a route whose entire job is deciding where a new account lands, and arrives as &lt;code&gt;/campaigns/new?step=website&amp;amp;site=yourbrand.com&lt;/code&gt;. The flow then seeds &lt;code&gt;reached&lt;/code&gt; so the deep link is allowed:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;site&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;params&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="s2"&gt;site&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nf"&gt;slice&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="mi"&gt;253&lt;/span&gt;&lt;span class="p"&gt;)&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;reached&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setReached&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&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;site&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;chosen&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;kind&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;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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;daily&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;setupStepList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;website&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="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;Read that condition as a sentence: if we were given a website, and the first two steps have answers that are correct by default on this plan, and there is no daily-emails step to ask about, then start at the website step with it filled in. A visitor who typed their address on a marketing page gets a one-field setup instead of a five-screen one, and the moment any of those conditions is false the whole wizard comes back.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;253&lt;/code&gt; is the maximum length of a domain name, and it appears twice: once as &lt;code&gt;maxLength&lt;/code&gt; on the public input, once as a slice in the client that receives it. Neither is the real check, which is on the server, and both exist so the string in a URL bar cannot be a paragraph.&lt;/p&gt;

&lt;h2&gt;
  
  
  A disabled button that says why
&lt;/h2&gt;

&lt;p&gt;Every step's Continue is disabled until that step is answered, and the reason is a value rather than a boolean:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;missing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nx"&gt;step&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;kind&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;kind&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Choose who to look for&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;where&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;platformsChosen&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="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Choose at least one place to look&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;businesses&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;targeting&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;types&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Choose a kind of business&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
          &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;targeting&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;locations&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="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Add at least one place&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;emails&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="nx"&gt;daily&lt;/span&gt;
          &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;shares&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;daily&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;limit&lt;/span&gt;
            &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Lower your other campaigns to fit your plan&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;own&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Give this campaign at least one new email a day&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
          &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A string or &lt;code&gt;false&lt;/code&gt;. One expression gives both "may we continue" and the sentence printed beside the button, so a greyed-out button is never a mystery, and the two can never disagree with each other because they are the same value. Five reasons for five ways to be stuck, including two different ones inside the businesses step.&lt;/p&gt;

&lt;p&gt;The step's choices also hold a third state worth stealing. Each platform tile is offered, locked, or absent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;PlatformChoice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Platform&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;lockedTo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Locked means it exists but your plan does not include it, so the tile renders with a lock and names the plan that has it. Absent means it is not configured on this deployment, so it never reaches the client at all. Those are genuinely different facts and a boolean would have flattened them into one sad tile.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trade
&lt;/h2&gt;

&lt;p&gt;A reload loses your answers. That is the whole cost, and it is a real cost: five screens of choices live in &lt;code&gt;useState&lt;/code&gt; and nowhere else, so a refresh at step four puts you back at step one with the URL quietly cleaned up behind you.&lt;/p&gt;

&lt;p&gt;What it buys is that the database has no concept of a half-made campaign. No &lt;code&gt;status: 'draft'&lt;/code&gt;, no nightly job deleting abandoned rows, no query anywhere that has to remember to exclude them, no half-set-up campaign appearing in a list or a count or a plan limit. Every campaign row in the product is one somebody finished and launched, and &lt;a href="https://nakodo.app/how-it-works" rel="noopener noreferrer"&gt;what happens after that&lt;/a&gt; is the part worth writing code for.&lt;/p&gt;

&lt;p&gt;If I were to spend anything here it would be &lt;code&gt;sessionStorage&lt;/code&gt; on the answers, keyed per tab, cleared on submit. That keeps the reload honest without teaching the database a new state, which is the part I would still refuse.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>react</category>
      <category>webdev</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Two emails about a new campaign, and whichever one goes first cancels the other for good</title>
      <dc:creator>Daniel Pertu</dc:creator>
      <pubDate>Sun, 08 Nov 2026 11:18:40 +0000</pubDate>
      <link>https://dev.to/daniel_pertu/two-emails-about-a-new-campaign-and-whichever-one-goes-first-cancels-the-other-for-good-3gaj</link>
      <guid>https://dev.to/daniel_pertu/two-emails-about-a-new-campaign-and-whichever-one-goes-first-cancels-the-other-for-good-3gaj</guid>
      <description>&lt;p&gt;A campaign in &lt;a href="https://nakodo.app" rel="noopener noreferrer"&gt;Nakodo&lt;/a&gt; is meant to be launched and then left alone, which &lt;a href="https://nakodo.app/how-it-works" rel="noopener noreferrer"&gt;how it works&lt;/a&gt; describes from the outside. The awkward part of that promise is the first hour. You press launch, a progress line starts moving, and then you close the tab, and whatever happens next has to reach you by email.&lt;/p&gt;

&lt;p&gt;Two things can happen next. Either the first good fits arrive, or a day passes and none do. So there are two emails, written by two functions in the same file, and most of the design is in making sure you never get both.&lt;/p&gt;

&lt;h2&gt;
  
  
  The good one waits for a quorum, or an hour
&lt;/h2&gt;

&lt;p&gt;The naive version sends as soon as there is one result. That is a worse email than it sounds: a campaign's first fit often arrives minutes after launch, while you are still looking at the screen that produced it, and an email telling you about one creator when thirty are coming is an email that trains you to ignore the next one.&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;ENOUGH_TO_EMAIL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// good fits; fewer is still sent an hour after launch&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;WAIT_FOR_MORE_MS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&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;ready&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rows&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="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&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;fit&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;ENOUGH_TO_EMAIL&lt;/span&gt;
    &lt;span class="o"&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="nx"&gt;launchedAt&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;Date&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="o"&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;launchedAt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&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;WAIT_FOR_MORE_MS&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;A quorum with a timeout. Five good fits is worth an email immediately. Fewer is worth an email once the first hour is over, because by then the number is news rather than a progress indicator. Both senders run at the tail of every pipeline run, which is itself on a cron, so both branches are arithmetic on a timestamp the campaign already has:&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;// Best effort: a failed email mustn't fail the run.&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;notifyFirstResults&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="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Results email 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;e&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;notifyNoFits&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="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;No-fits email 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;e&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The query that feeds it also refuses to look back very far:&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;gte&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;campaigns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;launchedAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&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="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;DAY_MS&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anything launched more than a fortnight ago is not a new campaign, and nothing that happens to it now should produce a "your first results are in" email. That line is also the safety rail for the day somebody runs a backfill: the blast radius of this email is two weeks wide, by construction.&lt;/p&gt;

&lt;h2&gt;
  
  
  The exclusion lives in the log, not in a column
&lt;/h2&gt;

&lt;p&gt;There is no &lt;code&gt;resultsEmailedAt&lt;/code&gt; column on the campaign. Both senders ask the audit log whether this campaign has already been written about, and the no-fits one asks about both actions at once:&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;sql&lt;/span&gt;&lt;span class="s2"&gt;`not exists (
  select 1 from &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;auditLog&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; a
  where a.action in ('campaign.results_emailed', 'campaign.no_fits_emailed')
    and a.entity_id = &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;campaigns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;::text
)`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one &lt;code&gt;in (...)&lt;/code&gt; is the whole mutual exclusion. A campaign that got the good email is permanently ineligible for the bad one, which matters because a campaign can genuinely go from five fits to a dry spell. And a campaign that got the bad one is ineligible for both, so the apology arrives once and is never followed by a cheerful correction a day later.&lt;/p&gt;

&lt;p&gt;The two functions also take the same advisory lock:&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;due&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;withLock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;results_email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&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="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not one lock each. One lock for the pair, named after the subject rather than the sender, because the thing that must not interleave is not "two runs of this function" but "two statements about the same campaign". Inside it, the rows are selected and the audit rows are inserted in the same transaction, so a second worker arriving mid-flight sees the row that makes it skip. The audit row doubling as the dedupe key is a pattern these emails all share, including the ones that are turned off in settings, which stops a switch being turned back on from releasing a flood of late mail.&lt;/p&gt;

&lt;h2&gt;
  
  
  The good email ends with whatever the next real step is
&lt;/h2&gt;

&lt;p&gt;An email that says "you have results" and links to them is fine. An email that says what to do next has to know what the campaign is waiting for, and there are three different answers:&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;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;auto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`Open the campaign to see why each one fits, and choose who Nakodo writes to.`&lt;/span&gt;
  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hasOffer&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`Nakodo is writing to the best of them, a few each day.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;
        &lt;span class="nf"&gt;autoSendsSaved&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;approval&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="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; Their first emails wait for your approval on the Outreach tab.&lt;/span&gt;&lt;span class="dl"&gt;"&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="s2"&gt;`Save your proposal on the campaign's Outreach tab, and Nakodo starts writing to the best of them.`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three branches and a nested fourth, from two facts the query already had to fetch: whether the campaign picks its own recipients, and whether the offer it would make has been saved yet. The worst of the three, by a distance, is the last one: a campaign that found good fits and cannot act on them because something is missing. That sentence is the only reason this email is worth sending at all to that user, and it is one &lt;code&gt;hasOffer&lt;/code&gt; away from being lost in a cheerful generic paragraph.&lt;/p&gt;

&lt;p&gt;Both facts come straight out of SQL on a JSONB column, which is why they cost nothing:&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;hasOffer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;hasOfferSql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;campaigns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outreach&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="nx"&gt;approval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;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;campaigns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outreach&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&amp;gt;&amp;gt;'approval'`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The bad email names a number and a next move
&lt;/h2&gt;

&lt;p&gt;The no-fits email is the one I rewrote most, because the first version was a shrug. It now says how far the search actually got, whether it is still widening by itself, and the single change that helps most for that kind of campaign:&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;looked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;checked&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="s2"&gt;`has checked &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;plural&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;checked&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;one&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;many&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;has been looking&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="s2"&gt;`Nakodo &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;looked&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; for &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; since you launched it, but none fit well enough to write to yet. It keeps looking`&lt;/span&gt;
  &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;widen&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;, and widens the search by itself when it runs dry.&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;.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;businesses&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;What helps most is more kinds of business or more places to look, under Businesses and places on the brief, or a looser Ideal business.&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;What helps most is broader search keywords on the brief: the words creators use about their own posts, rather than your product's. A looser Ideal creator helps too.&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;&lt;code&gt;checked&lt;/code&gt; is a correlated subquery counting the rows that have been scored at all, which turns "nothing fits" into "it looked at 180 and none fit", and those are different emails to receive. The widen clause exists because the sentence would otherwise be a lie for half the accounts: a campaign with widening on is already doing the obvious thing, and telling that user to go and do it by hand is worse than saying nothing.&lt;/p&gt;

&lt;p&gt;Both emails link to a specific tab rather than the app: the good one to the campaign filtered to its fits, the bad one straight to the brief, which is the page the email is asking you to change.&lt;/p&gt;

&lt;h2&gt;
  
  
  The finding
&lt;/h2&gt;

&lt;p&gt;The two sends are not written the same way, and only one of them is right:&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="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="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;No-fits email 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;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the no-fits loop. The first-results loop awaits &lt;code&gt;sendEmail&lt;/code&gt; with no catch of its own, so a single failure throws out of the loop and lands in the call site's catch above, which keeps the run alive and abandons the rest of the batch. The audit rows for that batch are already committed by then, so one provider hiccup on one address does two bad things: it marks every remaining campaign in the batch as emailed without emailing it, and it does it to the one email in the product that only ever gets one chance. The fix is the sibling's catch, and the better fix is writing the audit row per campaign as each send succeeds, at the cost of the single-statement dedupe that makes the pair safe to run twice at once. I would take the catch now and think about the second part later.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>node</category>
      <category>typescript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>A paid source ran out of credit and our own code hid it from us for nine days</title>
      <dc:creator>Daniel Pertu</dc:creator>
      <pubDate>Sun, 08 Nov 2026 11:16:42 +0000</pubDate>
      <link>https://dev.to/daniel_pertu/a-paid-source-ran-out-of-credit-and-our-own-code-hid-it-from-us-for-nine-days-53g6</link>
      <guid>https://dev.to/daniel_pertu/a-paid-source-ran-out-of-credit-and-our-own-code-hid-it-from-us-for-nine-days-53g6</guid>
      <description>&lt;p&gt;&lt;a href="https://nakodo.app" rel="noopener noreferrer"&gt;Nakodo&lt;/a&gt; leans on a handful of external sources, and two of them are paid, prepaid, and can therefore simply stop. On 12 October one of them ran out of balance. The work that depended on it stopped that day and did not start again for nine days. Nothing crashed, no alert fired, and the only record anywhere was a row in a counters table with a timestamp in it.&lt;/p&gt;

&lt;p&gt;Two separate mistakes made that possible, and they are worth separating, because only one of them is the obvious one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake one: an exhaustion flag that renewed itself
&lt;/h2&gt;

&lt;p&gt;Every external source has a daily row, and a refusal from the source writes &lt;code&gt;exhaustedAt&lt;/code&gt; on it. Every request first asks whether the row is usable:&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;usable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;providerDays&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exhaustedAt&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; is null`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is correct for a source whose budget is a calendar day. A daily API quota resets at a fixed hour, so "it refused us, stop asking until the day turns over" is exactly right, and it saves a few thousand pointless requests.&lt;/p&gt;

&lt;p&gt;It is wrong for a prepaid balance, and wrong in a way that is almost funny. The day turns over. The first job of the new day asks the source. The source, still unpaid, refuses. The new day's row is marked exhausted, and every other request that day is skipped locally. Each midnight bought one request, which was spent proving the outage would continue. Nine days of that.&lt;/p&gt;

&lt;p&gt;The fix is a per-source retry window rather than a global rule:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;retryAfter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;RETRY_AFTER_MS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;nextUtcMidnight&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&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="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;RETRY_AFTER_MS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The map has two entries, both one hour, for the two sources whose balance can be topped up during the day. Every other source is absent from it and keeps the old behaviour, which for a daily quota is the honest model of what the provider actually does.&lt;/p&gt;

&lt;p&gt;The window lives inside the same statement that spends the allowance, so there is no cron and no second round trip:&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;usable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nx"&gt;retryAfterMs&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;providerDays&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exhaustedAt&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; is null`&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="s2"&gt;`(&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;providerDays&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exhaustedAt&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; is null
           or &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;providerDays&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exhaustedAt&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &amp;lt; now() - make_interval(secs =&amp;gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;retryAfterMs&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;))`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;providerDays&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="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;requestsUsed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onConflictDoUpdate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;providerDays&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;providerDays&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;day&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;set&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;requestsUsed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;providerDays&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;requestsUsed&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;n&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;setWhere&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;limit&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="nx"&gt;usable&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;providerDays&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;requestsUsed&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;n&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &amp;lt;= &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; and &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;usable&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;returning&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;used&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;providerDays&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;requestsUsed&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An upsert whose &lt;code&gt;WHERE&lt;/code&gt; refuses to update returns no rows, so "may I spend one request" and "is this source worth asking again yet" are one question with one answer, decided by the database. Jobs that get a no are deferred rather than failed, with the retry time attached, so the queue holds them and nothing is lost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake two: nobody was told
&lt;/h2&gt;

&lt;p&gt;The deeper problem is that a source refusing us is not a bug anyone will find in the logs. It is not an exception. Both of ours answer with an HTTP 402 and a sentence of JSON explaining themselves, handled politely by code written to handle it politely. The product keeps working. It just quietly does less.&lt;/p&gt;

&lt;p&gt;So refusals now email the operator, and the only interesting part is making sure that happens once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;noteBlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NotifiedSource&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;providerState&lt;/span&gt;&lt;span class="p"&gt;)&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="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;blockNotifiedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="s2"&gt;`now()`&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onConflictDoUpdate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;providerState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;set&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;blockNotifiedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="s2"&gt;`coalesce(&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;providerState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;blockNotifiedAt&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, now())`&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="nf"&gt;returning&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;first&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="o"&gt;&amp;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;providerState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;blockNotifiedAt&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; = now()`&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;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;first&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;coalesce(blockNotifiedAt, now())&lt;/code&gt; is a write that only writes when the column is null: an existing notice keeps its original timestamp. Then the returning clause asks whether the value that is now in the row equals &lt;code&gt;now()&lt;/code&gt;, which inside a single statement is a fixed instant, so the comparison means "I am the request that set this". One statement answers both "latch it" and "was I first", with no read, no transaction and no race between two server instances arriving together.&lt;/p&gt;

&lt;p&gt;The caller does nothing clever with the answer except send one email:&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;paragraphs&lt;/span&gt; &lt;span class="o"&gt;=&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;outage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;what&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; Nakodo noticed at &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&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;outage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;effect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Nakodo tries the source again about an hour after each refusal, so the work picks up by itself once it can go through. The jobs waiting on it stay in the queue rather than failing.&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;You get this email once. The next one comes only after a request has worked again.&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;Each caller passes an &lt;code&gt;Outage&lt;/code&gt; with three fields, and the types are the whole design of the alert:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Outage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;what&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="c1"&gt;// "the balance is used up"&lt;/span&gt;
  &lt;span class="nl"&gt;effect&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="c1"&gt;// what stops, and what carries on&lt;/span&gt;
  &lt;span class="nl"&gt;link&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;label&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="c1"&gt;// where to fix it&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An alert that says a source is down is a notification. An alert that says what stopped, what is still running, and which page takes a card is a thing you can act on from a phone. The whole function is wrapped in a try that logs and swallows, because the caller is already on its way to failing a request and an email service having a bad minute must not turn one outage into two.&lt;/p&gt;

&lt;h2&gt;
  
  
  Forgetting is the other half
&lt;/h2&gt;

&lt;p&gt;A latch you cannot clear is a latch that fires once per deployment and then never again. The clearing side is the mirror image:&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;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;providerState&lt;/span&gt;&lt;span class="p"&gt;)&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="na"&gt;blockNotifiedAt&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;and&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;providerState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;isNotNull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;providerState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;blockNotifiedAt&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;isNotNull&lt;/code&gt; in the &lt;code&gt;WHERE&lt;/code&gt; means the statement is a no-op when there was no notice, which is what lets every successful request call it without thinking. And because a busy invocation might make hundreds of successful requests, there is a module-level set in front of 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;healthy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;NotifiedSource&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;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;noteSourceWorking&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NotifiedSource&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="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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;healthy&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;source&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;healthy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// ... clear the day's refusal and the email notice&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The set is a hint, not a lock. It is per instance and dies with the process, the database statements are idempotent anyway, and a failure removes the source from the set so the next request tries again. A later outage takes the source back out.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the table looks like now
&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;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;providerState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pgTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;provider_state&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;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;$type&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;NotifiedSource&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;primaryKey&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;nextRequestAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;withTimezone&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="nf"&gt;notNull&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;defaultNow&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;blockNotifiedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;withTimezone&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;updatedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;updatedAt&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;enableRLS&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One row per source, keyed by its name, holding two facts: when the next request may start, and whether we have already said it stopped working. Both are things every server instance has to agree on, which is why neither lives in memory.&lt;/p&gt;

&lt;p&gt;The thing I would take from the nine days is not the SQL. It is that the condition had no symptom. The product's plans are &lt;a href="https://nakodo.app/pricing" rel="noopener noreferrer"&gt;metered by what it does for you&lt;/a&gt;, and in that incident it did less for everybody while looking entirely healthy. Code that handles a dependency's failure gracefully, with no alert attached to the graceful path, is how an outage gets to be nine days long.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>node</category>
      <category>webdev</category>
      <category>typescript</category>
    </item>
    <item>
      <title>We import 53 icons from one library, and six of them ever reach a public page</title>
      <dc:creator>Daniel Pertu</dc:creator>
      <pubDate>Sun, 08 Nov 2026 11:14:53 +0000</pubDate>
      <link>https://dev.to/daniel_pertu/we-import-53-icons-from-one-library-and-six-of-them-ever-reach-a-public-page-15ba</link>
      <guid>https://dev.to/daniel_pertu/we-import-53-icons-from-one-library-and-six-of-them-ever-reach-a-public-page-15ba</guid>
      <description>&lt;p&gt;&lt;a href="https://nakodo.app" rel="noopener noreferrer"&gt;Nakodo&lt;/a&gt; imports its interface icons from &lt;code&gt;lucide-react&lt;/code&gt;. Counting the named imports across the repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;files importing lucide-react      52
distinct icons imported           53
imported by exactly one file      30
icon elements rendered in JSX    132
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Thirty icons with a single call site each is the shape you would expect from a library: you reach for the one that fits and you never need it again. The interesting number is how many of those 53 a visitor can actually see, so I swept all 76 URLs in &lt;a href="https://nakodo.app/sitemap.xml" rel="noopener noreferrer"&gt;the sitemap&lt;/a&gt; and counted the rendered class names.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://nakodo.app/pricing | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s1"&gt;'lucide-[a-z-]*'&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;
&lt;span class="c"&gt;#   25 lucide-check&lt;/span&gt;
&lt;span class="c"&gt;#   10 lucide-plus&lt;/span&gt;
&lt;span class="c"&gt;#    1 lucide-menu&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Across all 76 pages, six distinct icons ever leave the server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;menu    76 pages    1 per page
plus    66 pages    4 to 10 per page
copy     8 pages    1 to 6 per page
x        3 pages    3 per page
check    2 pages    21 and 25
mail     1 page     4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Forty-seven of the 53 live entirely behind the login. The public site is a static content site, and a static content site needs almost no iconography: a word is cheaper to render and easier to translate than a glyph that has to be explained.&lt;/p&gt;

&lt;h2&gt;
  
  
  What each of the six is doing
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;menu&lt;/code&gt; is the mobile navigation toggle, which is why it is on every single page, including the ten that ship nothing else: the eight hub pages such as &lt;a href="https://nakodo.app/guides" rel="noopener noreferrer"&gt;/guides&lt;/a&gt; and &lt;a href="https://nakodo.app/youtubers" rel="noopener noreferrer"&gt;/youtubers&lt;/a&gt;, plus &lt;a href="https://nakodo.app/privacy" rel="noopener noreferrer"&gt;/privacy&lt;/a&gt; and &lt;a href="https://nakodo.app/terms" rel="noopener noreferrer"&gt;/terms&lt;/a&gt;. Those ten pages have no FAQ and nothing to copy, so one icon is their entire icon budget.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;plus&lt;/code&gt; is the marker on an FAQ question, and on the four calculators it is also the add-a-row button. The FAQ one is my favourite icon in the app, because it is two symbols for the price of one:&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;svg&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"lucide lucide-plus size-4 shrink-0 text-muted-foreground
            transition-transform group-open:rotate-45"&lt;/span&gt; &lt;span class="na"&gt;aria-hidden=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The questions are native &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt; elements, so &lt;code&gt;group-open:rotate-45&lt;/code&gt; turns the plus into a cross when the answer opens, with the transition doing the turn. No second icon, no state in JavaScript, and it works with JavaScript off.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;copy&lt;/code&gt; sits on the copy-this-template buttons, which is why it appears on exactly the eight guides that carry a template, with a count that matches how many templates each one has: six on &lt;a href="https://nakodo.app/guides/youtube-outreach-email-templates" rel="noopener noreferrer"&gt;the outreach templates guide&lt;/a&gt;, five on &lt;a href="https://nakodo.app/guides/instagram-and-tiktok-outreach-templates" rel="noopener noreferrer"&gt;the Instagram and TikTok one&lt;/a&gt;, four on &lt;a href="https://nakodo.app/guides/how-to-follow-up-with-a-youtuber" rel="noopener noreferrer"&gt;the follow-up guide&lt;/a&gt;, and one each on five others.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;x&lt;/code&gt; appears three times on three of the four &lt;a href="https://nakodo.app/tools/engagement-rate-calculator" rel="noopener noreferrer"&gt;free tools&lt;/a&gt;: one on each of the three video rows the engagement calculators start with, as a remove button that disables itself when a single row is left. The fourth tool is the sponsorship cost calculator, which has no rows to remove, so it ships none.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;check&lt;/code&gt; and &lt;code&gt;mail&lt;/code&gt; are the two highest counts in the sweep and the two rarest pages: 25 ticks on the plan features on &lt;a href="https://nakodo.app/pricing" rel="noopener noreferrer"&gt;/pricing&lt;/a&gt;, and 21 ticks plus four envelopes on the home page.&lt;/p&gt;

&lt;h2&gt;
  
  
  The size attribute in the markup is never the size on screen
&lt;/h2&gt;

&lt;p&gt;Every rendered icon carries this:&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;svg&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"24"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"24"&lt;/span&gt; &lt;span class="na"&gt;viewBox=&lt;/span&gt;&lt;span class="s"&gt;"0 0 24 24"&lt;/span&gt; &lt;span class="na"&gt;stroke-width=&lt;/span&gt;&lt;span class="s"&gt;"2"&lt;/span&gt;
     &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"lucide lucide-check mt-0.5 size-4 shrink-0 ..."&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;24&lt;/code&gt; is the library's default and the &lt;code&gt;size-4&lt;/code&gt; is what you actually see. Eighty of the 132 icon elements in the source carry no &lt;code&gt;className&lt;/code&gt; at all, because the button variants size them from the outside:&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;// base&lt;/span&gt;
&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[&amp;amp;_svg]:pointer-events-none [&amp;amp;_svg]:shrink-0 [&amp;amp;_svg:not([class*='size-'])]:size-4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="c1"&gt;// size variants&lt;/span&gt;
&lt;span class="nx"&gt;xs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;h-7 gap-1 px-2 text-xs [&amp;amp;_svg:not([class*='size-'])]:size-3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="nx"&gt;sm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;h-8 gap-1.5 px-3 text-[13px] [&amp;amp;_svg:not([class*='size-'])]:size-3.5&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 &lt;code&gt;:not([class*='size-'])&lt;/code&gt; is the part worth stealing. The rule sizes any icon in the button to match the button, and steps aside the moment the icon names a size itself, which it detects by looking for the substring in the class attribute. So an icon has no size by default, inherits one from whatever it sits in, and can override it locally with one class. Across the app the overrides are &lt;code&gt;size-4&lt;/code&gt; 27 times, &lt;code&gt;size-3.5&lt;/code&gt; 14, &lt;code&gt;size-3&lt;/code&gt; 7 and &lt;code&gt;size-5&lt;/code&gt; twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exactly one icon is addressed to a screen reader
&lt;/h2&gt;

&lt;p&gt;I expected to find a rule about &lt;code&gt;aria-hidden&lt;/code&gt; on decorative icons, and found instead that the library has one. From &lt;code&gt;lucide-react&lt;/code&gt; 1.49.0:&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="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hasA11yProp&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;false&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="nf"&gt;getAttributeName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aria-hidden&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;true&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every icon is hidden from assistive technology unless you pass an accessibility prop, in which case the hiding switches itself off. That is the correct default for interface icons, which are nearly always a duplicate of a word next to them, and it means 35 of our 132 elements are repeating something the library already did.&lt;/p&gt;

&lt;p&gt;One icon in the entire app passes an accessibility prop, in the support thread view:&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;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;byEmail&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;MailIcon&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"size-3"&lt;/span&gt; &lt;span class="na"&gt;aria-label&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"By email"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That envelope is the only glyph in the product carrying information that no neighbouring text carries: this message arrived as an email rather than being typed into the app. One icon out of 132, and it is the only one where the default stepped aside.&lt;/p&gt;

&lt;p&gt;A live check on the rendered page agrees from the other direction: &lt;a href="https://nakodo.app/pricing" rel="noopener noreferrer"&gt;/pricing&lt;/a&gt; ships 62 &lt;code&gt;&amp;lt;svg&amp;gt;&lt;/code&gt; elements, 36 of them lucide, and 61 of the 62 carry &lt;code&gt;aria-hidden="true"&lt;/code&gt;. The one that does not is a nested group inside a drawing:&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;svg&lt;/span&gt; &lt;span class="na"&gt;x=&lt;/span&gt;&lt;span class="s"&gt;"82"&lt;/span&gt; &lt;span class="na"&gt;y=&lt;/span&gt;&lt;span class="s"&gt;"110"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"96"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"96"&lt;/span&gt; &lt;span class="na"&gt;viewBox=&lt;/span&gt;&lt;span class="s"&gt;"0 0 96 96"&lt;/span&gt; &lt;span class="na"&gt;overflow=&lt;/span&gt;&lt;span class="s"&gt;"visible"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which is the other half of the inventory. The 26 non-library svgs on that page are ours: 27 files and 1,622 lines in the illustration directory, drawn on a shared 96 unit grid, and the platform marks are hand-drawn too because YouTube, Instagram and TikTok each require their own logo next to their own data. The icon library covers the verbs. It does not cover the brand, and it is not allowed to cover anybody else's.&lt;/p&gt;

&lt;h2&gt;
  
  
  The finding
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;aria-label&lt;/code&gt; on a bare &lt;code&gt;&amp;lt;svg&amp;gt;&lt;/code&gt; is not reliably announced: the element needs &lt;code&gt;role="img"&lt;/code&gt; for the label to be treated as its name, and an &lt;code&gt;&amp;lt;span class="sr-only"&amp;gt;&lt;/code&gt; next to a hidden icon is the version that works everywhere. The app already has that pattern in its own attention mark, so the one labelled icon in the codebase is the one that should stop being clever. The 35 redundant &lt;code&gt;aria-hidden&lt;/code&gt; attributes can go at the same time, since the library is already doing it.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>a11y</category>
      <category>react</category>
      <category>css</category>
    </item>
    <item>
      <title>Seven useMemo calls in 72,683 lines, six of them in one file, and the one that matters is not about speed</title>
      <dc:creator>Daniel Pertu</dc:creator>
      <pubDate>Sun, 08 Nov 2026 11:10:59 +0000</pubDate>
      <link>https://dev.to/daniel_pertu/seven-usememo-calls-in-72683-lines-six-of-them-in-one-file-and-the-one-that-matters-is-not-about-ij7</link>
      <guid>https://dev.to/daniel_pertu/seven-usememo-calls-in-72683-lines-six-of-them-in-one-file-and-the-one-that-matters-is-not-about-ij7</guid>
      <description>&lt;p&gt;&lt;a href="https://nakodo.app" rel="noopener noreferrer"&gt;Nakodo&lt;/a&gt; is a Next.js 16 app on React 19.2.8: 582 TypeScript files, 72,683 lines. Most advice about React performance is about memoisation, so I grepped the whole thing to see how much of it we actually use.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;useMemo                7 occurrences, 2 files
useCallback            2 occurrences, 2 files
React.memo / memo()    0
useDeferredValue       0
useLayoutEffect        0
useImperativeHandle    0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For scale, the same grep over the same tree finds &lt;code&gt;useEffect&lt;/code&gt; in 23 files, &lt;code&gt;useState&lt;/code&gt; in 33, &lt;code&gt;useTransition&lt;/code&gt; in 11 and &lt;code&gt;useActionState&lt;/code&gt; in 11. So the app is not short of client state. It is short of memoisation, and the reason turns out to be structural rather than principled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why there is so little to memoise
&lt;/h2&gt;

&lt;p&gt;226 of the files are &lt;code&gt;.tsx&lt;/code&gt;. Eighty of them start with &lt;code&gt;"use client"&lt;/code&gt;, and 17 of those eighty are the shadcn primitives in &lt;code&gt;src/components/ui&lt;/code&gt;. Everything else renders on the server, which includes almost every list in the product: the campaign list, the conversations screen, the creator page, all 76 public pages. A &lt;code&gt;.map()&lt;/code&gt; over rows appears in 103 files, and in most of them it runs once, on a server, into HTML.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;React.memo&lt;/code&gt; protects a child from a parent that re-renders. If the parent is a server component, there is no parent render to protect against, so there is nothing for the wrapper to do. That is the whole explanation for the zero.&lt;/p&gt;

&lt;p&gt;One of the seven &lt;code&gt;useMemo&lt;/code&gt; calls is in &lt;code&gt;src/components/ui/chart.tsx&lt;/code&gt;, which is vendored shadcn code. We did not write it, and it is the only memo in the repo that is not in the file below.&lt;/p&gt;

&lt;h2&gt;
  
  
  The six that are ours
&lt;/h2&gt;

&lt;p&gt;They are all in &lt;code&gt;src/app/(app)/campaigns/[id]/results-view.tsx&lt;/code&gt;, 1,153 lines, the biggest client component in the app and the one screen where a long list genuinely lives in the browser: filters, sorting, selection and a search box that must not wait on a round trip. You cannot look at it without an account, which the server will tell you itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ curl -sI https://nakodo.app/campaigns
HTTP/2 307
location: /login?next=%2Fcampaigns
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://nakodo.app/how-it-works" rel="noopener noreferrer"&gt;How it works&lt;/a&gt; describes what that screen holds from the outside. From the inside it is six derivations in a chain:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;urlFilters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMemo&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;parseFilters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="c1"&gt;// The search box is local so typing never waits on the URL; it is written&lt;/span&gt;
&lt;span class="c1"&gt;// to the URL with the other filters.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setQuery&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;urlFilters&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMemo&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="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;urlFilters&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;q&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;urlFilters&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;then a pass that compares the rows the server just sent against the snapshot the screen started with, then:&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;optimistic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setOptimistic&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useOptimistic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;shown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;change&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="cm"&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;counts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMemo&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;viewCounts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;optimistic&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;optimistic&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;list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMemo&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;applyFilters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;optimistic&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;markets&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;optimistic&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;markets&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interesting one is &lt;code&gt;shown&lt;/code&gt;, the memo in the middle, because its dependencies are &lt;code&gt;[rows, snapshot]&lt;/code&gt; and neither of those changes when you type. The search box is local state, so a keystroke re-renders the whole 1,153-line component. What recomputes is one derivation out of five: &lt;code&gt;shown&lt;/code&gt; is identical, so &lt;code&gt;optimistic&lt;/code&gt; is identical, so &lt;code&gt;counts&lt;/code&gt; is identical, and only &lt;code&gt;list&lt;/code&gt; runs again because &lt;code&gt;f&lt;/code&gt; changed.&lt;/p&gt;

&lt;p&gt;That is what the memo buys. Not milliseconds in a profiler: a dependency chain where the base array of &lt;code&gt;useOptimistic&lt;/code&gt; keeps its identity through renders that have nothing to do with it. The other two &lt;code&gt;useOptimistic&lt;/code&gt; call sites in the app pass a boolean, in a settings toggle and in an operator form, and neither needs a memo, because a primitive has no identity to lose.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is deliberately not memoised
&lt;/h2&gt;

&lt;p&gt;Three lines further down the same component:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;visible&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&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;limit&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;chosen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;list&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;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="nx"&gt;selected&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;channelId&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;fitCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;optimistic&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;isFit&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three derived values, all recomputed on every render, none of them wrapped. The rule that fell out of writing this file is that memoising a single pass over an array you are already holding costs more than it saves: you pay a dependency array, a cache slot and the chance of a stale value, to skip one loop that was going to run in the time it takes to allocate the comparison.&lt;/p&gt;

&lt;p&gt;What is worth memoising is &lt;code&gt;applyFilters&lt;/code&gt;, and it is worth it because of what it does rather than because of the loop:&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;return&lt;/span&gt; &lt;span class="nx"&gt;list&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&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;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="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;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;v&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;sortValue&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;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sort&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="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="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="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;x&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;x&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A filter, then a decorate, sort and undecorate: four array allocations and a comparison sort, with a stable tie-break on the server's original order. That is the one derivation in the screen that is not linear, and it is the one that is cached.&lt;/p&gt;

&lt;h2&gt;
  
  
  Both useCallback calls are for useEffect
&lt;/h2&gt;

&lt;p&gt;Neither of the two is about a child component. They exist because the function is an effect dependency:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useCallback&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Part&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="cm"&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;campaignId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="nf"&gt;useEffect&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="nx"&gt;part&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;asked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;part&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;part&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="nx"&gt;part&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without the wrapper, &lt;code&gt;ask&lt;/code&gt; is a new function on every render, the effect's dependency array changes on every render, and the effect fires on every render, which here means asking the server to write part of a campaign setup over and over. The second one, in the support panel, is &lt;code&gt;useCallback(() =&amp;gt; { /* thirteen setState calls */ }, [])&lt;/code&gt;, used the same way.&lt;/p&gt;

&lt;p&gt;That is a different tool from the one the performance posts describe. &lt;code&gt;useCallback&lt;/code&gt; for a prop is an optimisation. &lt;code&gt;useCallback&lt;/code&gt; for a dependency array is a correctness fix, and it is the only kind this codebase has needed. 161 inline arrow handlers are passed straight into elements across the app's client components, and because no child is wrapped in &lt;code&gt;React.memo&lt;/code&gt;, a stable identity for any of them would buy exactly nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap
&lt;/h2&gt;

&lt;p&gt;The results screen caps the DOM at &lt;code&gt;PAGE = 50&lt;/code&gt; rows and pages in more on demand, which is why the unmemoised passes stay cheap: the list can be long, the render is not. But &lt;code&gt;applyFilters&lt;/code&gt; runs over every row in the campaign on every keystroke, and &lt;code&gt;useDeferredValue&lt;/code&gt; appears nowhere in these 582 files. If a campaign's list ever grows to the point where typing feels sticky, that is the hook to reach for, and the memo chain above is already shaped to make it a two-line change.&lt;/p&gt;

&lt;p&gt;Seven memos, two callbacks, no &lt;code&gt;React.memo&lt;/code&gt;. The audit's real finding is that moving list rendering to the server does not reduce the amount of memoisation you need by a bit. It removes the reason for most of it.&lt;/p&gt;

</description>
      <category>react</category>
      <category>performance</category>
      <category>nextjs</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
