<?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: Liam</title>
    <description>The latest articles on DEV Community by Liam (@liam-dev).</description>
    <link>https://dev.to/liam-dev</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%2F4042866%2F7af70a81-8b3e-4f20-9901-3c3c17485ed5.png</url>
      <title>DEV Community: Liam</title>
      <link>https://dev.to/liam-dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/liam-dev"/>
    <language>en</language>
    <item>
      <title>9,104 rows in, 5,000 out: the silent cap that made my dashboard lie</title>
      <dc:creator>Liam</dc:creator>
      <pubDate>Sun, 02 Aug 2026 15:22:07 +0000</pubDate>
      <link>https://dev.to/liam-dev/9104-rows-in-5000-out-the-silent-cap-that-made-my-dashboard-lie-2hbk</link>
      <guid>https://dev.to/liam-dev/9104-rows-in-5000-out-the-silent-cap-that-made-my-dashboard-lie-2hbk</guid>
      <description>&lt;p&gt;I sat down to add a small thing to an internal dashboard: a &lt;code&gt;NEW&lt;/code&gt; badge on a search-rank table, so you could tell "this keyword just broke into the rankings" from "this keyword didn't move." An afternoon of work.&lt;/p&gt;

&lt;p&gt;Then I went looking for the data to base it on, and found that the table had been reading 55% of its own rows for the last four days, without saying so.&lt;/p&gt;

&lt;h2&gt;
  
  
  What was actually happening
&lt;/h2&gt;

&lt;p&gt;The page pulled raw daily rows through a generic query endpoint and folded them in the browser. The request asked for 8,000 rows. The server had this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;_MAX_LIMIT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;
&lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="n"&gt;limit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_MAX_LIMIT&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;min()&lt;/code&gt;. Not a 400, not a warning header, not a &lt;code&gt;truncated: true&lt;/code&gt; in the response body. It hands you 5,000 rows and a 200 and lets you assume that's everything.&lt;/p&gt;

&lt;p&gt;The table had 9,104 rows — 9 days of collection at roughly 1,000 keywords a day. So 4,104 rows never arrived. And because the query carried no &lt;code&gt;ORDER BY&lt;/code&gt;, &lt;em&gt;which&lt;/em&gt; 5,000 arrived was entirely up to the planner.&lt;/p&gt;

&lt;p&gt;Both halves are needed for it to really hurt. A cap with a deterministic sort truncates the tail, which is at least a bug you can reason about: you know you're missing the oldest, or the lowest-ranked, or whatever your sort key implies. A cap with no sort hands you an arbitrary sample and presents it as the whole population.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why nobody noticed
&lt;/h2&gt;

&lt;p&gt;The dashboard didn't render an error. It rendered a dash.&lt;/p&gt;

&lt;p&gt;Rank change is computed by comparing the current window against the previous one. If the previous window's rows happen to be the ones that fell off the truncated edge, there's nothing to compare against, so the cell shows &lt;code&gt;—&lt;/code&gt;. Which is exactly what a keyword whose rank didn't change looks like.&lt;/p&gt;

&lt;p&gt;That's the part worth stealing from this: the failure didn't surface as &lt;em&gt;missing data&lt;/em&gt;, it surfaced as &lt;em&gt;"no change."&lt;/em&gt; Absence and zero rendered identically, and one of them is boring enough that your eye slides right over it.&lt;/p&gt;

&lt;p&gt;There's a second reason it stayed hidden, and it's the one I keep thinking about. At 1,000 rows a day, this code was correct on launch day. Day 4 fits under the cap. Day 5 fits. Day 6 crosses 5,000 and the dashboard starts lying, a little more each morning. There is no deploy to correlate it with, no error rate to alert on, no bad commit to bisect to. The bug arrives on a schedule you set months earlier and forgot about.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix is boring, and the payload got smaller
&lt;/h2&gt;

&lt;p&gt;Stop shipping raw daily rows to the browser at all. Fold the window in SQL. Postgres &lt;a href="https://www.postgresql.org/docs/current/sql-select.html#SQL-DISTINCT" rel="noopener noreferrer"&gt;&lt;code&gt;DISTINCT ON&lt;/code&gt;&lt;/a&gt; gives you the latest measurement per key in a single pass:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="k"&gt;ON&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="n"&gt;keyword&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="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;day&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;naver_site_rank&lt;/span&gt;
   &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;day&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="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;day&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;lt;=&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;      &lt;span class="c1"&gt;-- current window&lt;/span&gt;
   &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;day&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;prv&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="k"&gt;ON&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="n"&gt;keyword&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="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;day&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;prev_day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;prev_rank&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;naver_site_rank&lt;/span&gt;
   &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;day&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="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;day&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;lt;=&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;      &lt;span class="c1"&gt;-- previous window&lt;/span&gt;
   &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;day&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;c&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="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prev_rank&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prev_day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prev_day&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;measured_prev&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
  &lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;prv&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keyword&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keyword&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Windows are half-open — &lt;code&gt;(from, to]&lt;/code&gt; — so the current and previous window can't both claim the boundary day.)&lt;/p&gt;

&lt;p&gt;One row per key means the response size is bounded by &lt;em&gt;how many keywords exist&lt;/em&gt;, not by how long you've been collecting. About 1,300 rows and 275 KB, whether you ask for a week or a month or next year. That's smaller than the broken 5,000-row response, and it's complete.&lt;/p&gt;

&lt;p&gt;The rule I wrote into the repo's guide afterward: &lt;strong&gt;if the row count grows with the date range, fold it on the server.&lt;/strong&gt; Anything else is a cap waiting to be crossed, and you won't be watching on the day it happens.&lt;/p&gt;

&lt;h2&gt;
  
  
  The second bug, which was worse than the first
&lt;/h2&gt;

&lt;p&gt;With real data finally reaching the page, I could compute the &lt;code&gt;NEW&lt;/code&gt; badge. It came out wrong anyway.&lt;/p&gt;

&lt;p&gt;The keyword sample rotates. There are 1,308 keywords total and the collector measures about 1,000 of them per day, so any given keyword is simply &lt;em&gt;absent&lt;/em&gt; from some days. If you read absent as "wasn't ranking," every gap in the rotation becomes a triumphant new entry. On a weekly window that inflated &lt;code&gt;NEW&lt;/code&gt; from 10 to 47.&lt;/p&gt;

&lt;p&gt;So the state isn't a boolean, it's three-valued:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_is_new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;has_baseline&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Ranked now, and last window we LOOKED and it wasn&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;t there.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;has_baseline&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;measured_prev&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;   &lt;span class="c1"&gt;# not measured is unknown, not "wasn't there"
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rank&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rank_prev&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;it was ranked&lt;/li&gt;
&lt;li&gt;it was measured, and it wasn't ranked&lt;/li&gt;
&lt;li&gt;we didn't look&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only the middle one can produce a &lt;code&gt;NEW&lt;/code&gt;. The third has to stay unknown, which the UI draws as &lt;code&gt;?&lt;/code&gt; rather than as an arrow.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;has_baseline&lt;/code&gt; covers the degenerate case: if the previous window contains no measurements at all — pick "month" on day 9 of collection and it won't — then the honest answer isn't "everything is new," it's "can't compare." The page says that in words and draws no arrows at all.&lt;/p&gt;

&lt;p&gt;I pulled that predicate into its own function specifically so I could pin it in a test. It's the kind of rule that gets quietly re-broken by the next person who thinks a &lt;code&gt;NULL&lt;/code&gt; rank means rank zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A cap that returns 200 is a lie generator.&lt;/strong&gt; If you clamp, say so in the response. &lt;code&gt;{"truncated": true, "cap": 5000}&lt;/code&gt; costs you one key and saves someone a day.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;LIMIT&lt;/code&gt; without &lt;code&gt;ORDER BY&lt;/code&gt; is sampling, not truncation.&lt;/strong&gt; The &lt;a href="https://www.postgresql.org/docs/current/queries-limit.html" rel="noopener noreferrer"&gt;Postgres docs&lt;/a&gt; say it plainly: without an &lt;code&gt;ORDER BY&lt;/code&gt; that constrains the rows into a unique order, you get "an unpredictable subset". Which is indistinguishable from complete data at the call site.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch for bugs whose trigger is a row count.&lt;/strong&gt; They ship green and go red on their own schedule, long after the deploy that caused them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing and zero are different values, and they must render differently.&lt;/strong&gt; Every place you collapse them, you're choosing to make an unknown look like a measurement.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;This is from an internal analytics dashboard I built to track a handful of my own sites — mostly &lt;a href="https://toolio.pongvn.com" rel="noopener noreferrer"&gt;Toolio&lt;/a&gt;, a set of small browser-based tools — which is how a rank table ended up growing by a thousand rows a day in the first place.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>sql</category>
      <category>api</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The checklist item that made 90 files permanently unfinished</title>
      <dc:creator>Liam</dc:creator>
      <pubDate>Thu, 23 Jul 2026 23:00:00 +0000</pubDate>
      <link>https://dev.to/liam-dev/the-checklist-item-that-made-90-files-permanently-unfinished-4a3i</link>
      <guid>https://dev.to/liam-dev/the-checklist-item-that-made-90-files-permanently-unfinished-4a3i</guid>
      <description>&lt;p&gt;I have a pipeline that generates document scaffolds — skeletons with blanks for a human to fill in. Every generated file looks roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- draft-stage: scaffold --&amp;gt;&lt;/span&gt;

&lt;span class="gu"&gt;## Hook&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; [✏️ one line that stops the scroll]

&lt;span class="gu"&gt;## Caption&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; [✏️ 1-2 sentences, facts only]

&lt;span class="gu"&gt;## Before you publish&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Zero &lt;span class="sb"&gt;`[✏️]`&lt;/span&gt; left → delete the &lt;span class="sb"&gt;`&amp;lt;!-- draft-stage: scaffold --&amp;gt;`&lt;/span&gt; line above
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two markers carry the state. &lt;code&gt;[✏️]&lt;/code&gt; marks a blank nobody filled yet. The HTML comment marks "this file is still a skeleton." A dashboard reads both to decide whether a document is done:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;SCAFFOLD_MARK&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;!-- draft-stage: scaffold --&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;PEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;✏️&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;stage_of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&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;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;text&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;read_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scaffold&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="nf"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SCAFFOLD_MARK&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;PEN&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;draft&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple enough. Read the file, look for either marker, done.&lt;/p&gt;

&lt;p&gt;Except every single file came back &lt;code&gt;scaffold&lt;/code&gt;. All 90 of them. Files that had been filled in weeks earlier, reviewed, and published still showed up as unfinished work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The last line of the file
&lt;/h2&gt;

&lt;p&gt;Look at the checklist again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;-&lt;/span&gt; [ ] Zero &lt;span class="sb"&gt;`[✏️]`&lt;/span&gt; left → delete the &lt;span class="sb"&gt;`&amp;lt;!-- draft-stage: scaffold --&amp;gt;`&lt;/span&gt; line above
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That line — the one telling you how to mark the file &lt;em&gt;complete&lt;/em&gt; — contains both markers verbatim. So the moment you follow the instruction and delete the HTML comment, &lt;code&gt;stage_of()&lt;/code&gt; still finds &lt;code&gt;[✏️]&lt;/code&gt;… in the sentence explaining that there should be no &lt;code&gt;[✏️]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The exit condition was unsatisfiable. Not "hard to satisfy" — &lt;strong&gt;unsatisfiable&lt;/strong&gt;, because the document could not describe its own completion criteria without violating them.&lt;/p&gt;

&lt;p&gt;The generator wrote that line. Every file it produced was born permanently unfinished.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why nobody noticed for weeks
&lt;/h2&gt;

&lt;p&gt;This is the part I find more interesting than the bug.&lt;/p&gt;

&lt;p&gt;The dashboard showed a big number of pending documents. That number never went down. And a number that never goes down stops being information — people stop reading it. When I finally asked &lt;em&gt;why&lt;/em&gt; it was stuck, I'd already been ignoring it for a while, which is exactly what always-red signals train you to do.&lt;/p&gt;

&lt;p&gt;The fix took one line. Rewrite the instruction so it &lt;em&gt;describes&lt;/em&gt; the markers instead of &lt;em&gt;containing&lt;/em&gt; them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;-&lt;/span&gt; [ ] No pencil slots left → delete the &lt;span class="sb"&gt;`draft-stage`&lt;/span&gt; comment at the top of the file
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Re-running the check flipped 37 of 90 files from "unfinished" to "done" instantly. Nothing about those files changed. They had been done the whole time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The general shape
&lt;/h2&gt;

&lt;p&gt;This is &lt;a href="https://en.wikipedia.org/wiki/In-band_signaling" rel="noopener noreferrer"&gt;in-band signaling&lt;/a&gt;: control information travelling in the same channel as the payload. Telephone networks hit it in the 1960s — &lt;a href="https://en.wikipedia.org/wiki/Phreaking" rel="noopener noreferrer"&gt;the 2600 Hz tone&lt;/a&gt; that told a trunk line it was idle could be whistled into the handset by a person, and the network could not tell the difference between "the switch says this line is free" and "a human made that sound." Same class of problem, seventy years apart.&lt;/p&gt;

&lt;p&gt;Once you have the shape in your head, you see it everywhere:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A log parser that greps for &lt;code&gt;ERROR&lt;/code&gt; — and logs &lt;code&gt;ERROR&lt;/code&gt; when it fails to parse a line.&lt;/li&gt;
&lt;li&gt;A linter whose README documents the pattern it flags, and which is run over its own repo.&lt;/li&gt;
&lt;li&gt;A test that asserts "no &lt;code&gt;TODO&lt;/code&gt; in source" while its own source explains what a &lt;code&gt;TODO&lt;/code&gt; is.&lt;/li&gt;
&lt;li&gt;Markdown-based feature flags where the flag name appears in the docs describing the flag.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The common failure mode isn't that the marker is a bad choice. &lt;code&gt;[✏️]&lt;/code&gt; is fine. It's that the &lt;strong&gt;same file&lt;/strong&gt; serves two readers — a parser and a human — and the humans need to talk &lt;em&gt;about&lt;/em&gt; the marker while the parser only knows how to find it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;p&gt;Three options, roughly in order of how much I'd trust them:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Move state out of the payload.&lt;/strong&gt; A sidecar file, frontmatter field, or database column means the prose can say whatever it wants. This is the real fix; the rest are mitigations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Make the sentinel unspeakable.&lt;/strong&gt; Pick something a human writing documentation would never type, and if you must mention it, mention it by name (&lt;code&gt;the draft-stage comment&lt;/code&gt;) rather than by value. This is what I actually shipped, because the marker also has to be visually obvious in a rendered document — that was the whole point of using an emoji.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Exclude the region.&lt;/strong&gt; Skip fenced code blocks, or everything after a &lt;code&gt;&amp;lt;!-- parser:stop --&amp;gt;&lt;/code&gt; line. Works, but now you have two parsers that must agree, and the failure is silent when they drift.&lt;/p&gt;

&lt;p&gt;There's a fourth option I want to flag because it's tempting and wrong: parse "smarter" — only count &lt;code&gt;[✏️]&lt;/code&gt; when it's inside brackets at the start of a list item, etc. Every heuristic you add makes the rule harder to state, and a state machine you can't state in one sentence is one nobody can reason about. The bug came from a rule that was too clever by half already.&lt;/p&gt;

&lt;h2&gt;
  
  
  The check that would have caught it
&lt;/h2&gt;

&lt;p&gt;The cheapest guard is a self-test on the generator, not the documents:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_generated_scaffold_can_be_completed&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;A freshly generated file must be completable by following its own instructions.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_scaffold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sample_item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# do what the instructions say: fill every slot, drop the marker
&lt;/span&gt;    &lt;span class="n"&gt;filled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\[✏️[^\]]*\]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SCAFFOLD_MARK&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;stage_of_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;draft&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scaffold cannot be completed as instructed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That test fails on the old template and passes on the new one. It encodes the property that actually matters — &lt;em&gt;the exit condition is reachable&lt;/em&gt; — instead of testing the parser against inputs I thought of.&lt;/p&gt;




&lt;p&gt;For context: the pipeline generates publishing copy for a set of small browser-based tools I build (&lt;a href="https://toolio.pongvn.com" rel="noopener noreferrer"&gt;toolio.pongvn.com&lt;/a&gt;), which is why the documents have slots for hooks and captions in the first place. The bug had nothing to do with the tools and everything to do with letting a document describe its own state — but it did mean a couple of months of "you have 90 things to do" that were, in fact, 37 things already done.&lt;/p&gt;

&lt;p&gt;If you have a marker-based state machine anywhere, go check whether your docs mention the marker. It takes about thirty seconds and the answer is occasionally embarrassing.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>python</category>
      <category>lessonslearned</category>
      <category>codequality</category>
    </item>
    <item>
      <title>The one-line bug that broke Open Graph cards on 836 pages</title>
      <dc:creator>Liam</dc:creator>
      <pubDate>Thu, 23 Jul 2026 02:17:49 +0000</pubDate>
      <link>https://dev.to/liam-dev/the-one-line-bug-that-broke-open-graph-cards-on-836-pages-4akd</link>
      <guid>https://dev.to/liam-dev/the-one-line-bug-that-broke-open-graph-cards-on-836-pages-4akd</guid>
      <description>&lt;p&gt;For about a month, every social preview card on a site I build was silently cut off mid-word. Not truncated with an ellipsis — cut &lt;em&gt;inside&lt;/em&gt; a word. &lt;code&gt;PDF &amp;amp; image tools, con&lt;/code&gt;. The &lt;code&gt;con&lt;/code&gt; is &lt;code&gt;converters&lt;/code&gt;, sliced in half.&lt;/p&gt;

&lt;p&gt;Nobody reported it. Preview cards are the one thing you never see on your own site — they only show up when &lt;em&gt;someone else&lt;/em&gt; shares your link on Slack, Discord, or X. So it just sat there, on every page, in every language.&lt;/p&gt;

&lt;p&gt;Here's the line that did it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// OG card subtitle&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;text&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;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;esc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sub&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;60&lt;/span&gt;&lt;span class="p"&gt;))}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/text&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;slice(0, 60)&lt;/code&gt;. Take the first 60 characters. Simple, obvious, and wrong — because "60 characters" has no idea where words end. If character 60 lands in the middle of &lt;code&gt;converters&lt;/code&gt;, you ship &lt;code&gt;conv&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it survived so long
&lt;/h2&gt;

&lt;p&gt;Two reasons, and both are worth internalizing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The output is invisible to the author.&lt;/strong&gt; OG cards render off-site. My build succeeded, the page looked fine, the card image generated without error. Everything green. The bug only exists in the one context I never look at.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;It was in a shared template.&lt;/strong&gt; One &lt;code&gt;ogSvg()&lt;/code&gt; function renders the card for every tool page. So the bug wasn't on one page — it was on 164 tools × the locales each ships. 836 cards, all the same slice, all cut mid-word.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That second point is the real lesson. A bug in a leaf component hurts one page. A bug in the thing that generates &lt;em&gt;every&lt;/em&gt; page hurts all of them at once, and it does it quietly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The naive fix, and why it isn't enough
&lt;/h2&gt;

&lt;p&gt;The obvious fix is "cut at the last space instead of at character 60":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cut&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;s&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;n&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;cut&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;cut&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lastIndexOf&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="o"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works great for English. It falls apart the moment you have more than one language.&lt;/p&gt;

&lt;p&gt;Japanese, Chinese, and Korean don't put spaces between words. &lt;code&gt;無料オンラインツール&lt;/code&gt; is one run of characters with no space to find. &lt;code&gt;lastIndexOf(" ")&lt;/code&gt; returns &lt;code&gt;-1&lt;/code&gt;, &lt;code&gt;slice(0, -1)&lt;/code&gt; chops off the last character, and now your CJK subtitle is missing a character &lt;em&gt;and&lt;/em&gt; has a floating ellipsis. You traded a visible English bug for a subtle CJK one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually shipped
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;clip&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;n&lt;/span&gt;&lt;span class="p"&gt;)&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="nc"&gt;String&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="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;s&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="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="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;n&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;s&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;cut&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;s&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;n&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;sp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cut&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lastIndexOf&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="c1"&gt;// Latin scripts break on the space. CJK has no spaces, so if there's no&lt;/span&gt;
  &lt;span class="c1"&gt;// space — or it's too early to be a real word boundary — just hard-cut.&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;sp&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;cut&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;sp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cut&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;,·、，&lt;/span&gt;&lt;span class="se"&gt;\s]&lt;/span&gt;&lt;span class="sr"&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="o"&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;sp &amp;gt; n * 0.5&lt;/code&gt; guard is the whole trick. If the last space sits in a reasonable spot, break there (Latin). If there's no space, or it's suspiciously early — meaning it's probably not a real word boundary — hard-cut the character run (CJK). The trailing &lt;code&gt;replace&lt;/code&gt; strips a dangling comma or CJK punctuation so you never get &lt;code&gt;image tools,…&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It's not linguistically perfect. It doesn't need to be. It needs to never cut &lt;code&gt;converters&lt;/code&gt; into &lt;code&gt;conv&lt;/code&gt; and never eat a Japanese character. Those two failures are what people actually see.&lt;/p&gt;

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

&lt;p&gt;If you generate anything from a shared template — OG cards, emails, PDFs, filenames — the failure mode isn't "one broken thing." It's "one broken thing, replicated everywhere, in the one place you never look." Two habits catch it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Look at the artifact, not the build log.&lt;/strong&gt; The build passing tells you nothing about whether the card reads correctly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;slice()&lt;/code&gt; is not a text-truncation function.&lt;/strong&gt; It's a character-count function. The moment human-readable text meets a non-space-delimited language, they stop being the same thing.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;I hit this building &lt;a href="https://toolio.pongvn.com" rel="noopener noreferrer"&gt;Toolio&lt;/a&gt;, a set of small browser tools that ship in 16 languages — which is exactly why the CJK edge case wasn't optional.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>i18n</category>
      <category>seo</category>
    </item>
  </channel>
</rss>
