<?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: Zugo</title>
    <description>The latest articles on DEV Community by Zugo (@zugodev).</description>
    <link>https://dev.to/zugodev</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%2F4050088%2Fa92093cd-ba40-4c06-96db-26d4a4cfffe9.png</url>
      <title>DEV Community: Zugo</title>
      <link>https://dev.to/zugodev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zugodev"/>
    <language>en</language>
    <item>
      <title>Our AI builder said "done" when the output matched a regex</title>
      <dc:creator>Zugo</dc:creator>
      <pubDate>Sun, 02 Aug 2026 07:04:40 +0000</pubDate>
      <link>https://dev.to/zugodev/our-ai-builder-said-done-when-the-output-matched-a-regex-agi</link>
      <guid>https://dev.to/zugodev/our-ai-builder-said-done-when-the-output-matched-a-regex-agi</guid>
      <description>&lt;p&gt;We build &lt;a href="https://zugo.dev" rel="noopener noreferrer"&gt;Zugo&lt;/a&gt;, which turns one written sentence into a running game, site or app. For a while, the thing that decided whether a build had succeeded was this:&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;function&lt;/span&gt; &lt;span class="nf"&gt;htmlComplete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&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="sr"&gt;/&amp;lt;&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;html&amp;gt;/i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&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 closing tag. That was the gate. If the model emitted &lt;code&gt;&amp;lt;/html&amp;gt;&lt;/code&gt;, the turn was a success, the green tick appeared, and the credits were charged. Whether the page actually opened was not part of the decision, because at the moment we charged, nothing had mounted the document anywhere.&lt;/p&gt;

&lt;p&gt;If you are building anything that generates code, this is the trap. Writing the file is easy to observe. Whether the file works is not, so it quietly stops being what "done" means.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a tag balance check does not save you
&lt;/h2&gt;

&lt;p&gt;Our second-line check counted opening and closing tags. Its own header admitted what it was: a balance check, not "it renders." A document can be perfectly balanced, end in &lt;code&gt;&amp;lt;/html&amp;gt;&lt;/code&gt;, and throw on the first line of its first script. That build billed as a complete success.&lt;/p&gt;

&lt;p&gt;Worse, the failure was invisible in exactly the way that costs the most trust: the tick went green the instant the stream finished, and the real answer arrived seconds later. Measured on our own paths, that gap is 2.6s for a site, 4.2s for a game and 6.2s for a React app, plus one silent re-mount if the first verdict is bad. So a broken build showed a green tick over a blank white frame for up to about twelve seconds. Long enough to read it as the product lying to you and close the tab.&lt;/p&gt;

&lt;h2&gt;
  
  
  You cannot watch the document from outside
&lt;/h2&gt;

&lt;p&gt;The obvious fix is to wrap the preview in an error boundary and listen. That does not work here, and the reason is worth knowing if you sandbox generated code.&lt;/p&gt;

&lt;p&gt;The preview frame is &lt;code&gt;sandbox="allow-scripts"&lt;/code&gt; with &lt;strong&gt;no&lt;/strong&gt; &lt;code&gt;allow-same-origin&lt;/code&gt;. That gives it an opaque origin, which is the whole point: a generated document must not be able to reach the host app, its storage or its session. But the same wall means the host cannot see inside. A React error boundary around the preview catches nothing from the document. If a user ever sees our error boundary, the &lt;strong&gt;editor&lt;/strong&gt; crashed, not their build.&lt;/p&gt;

&lt;p&gt;So the listener has to live inside the frame, which means we inject it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The harness, and the two details that make it work
&lt;/h2&gt;

&lt;p&gt;We prepend a small script to the generated document that reports &lt;code&gt;error&lt;/code&gt;, &lt;code&gt;unhandledrejection&lt;/code&gt; and &lt;code&gt;console.error&lt;/code&gt; back to the host via &lt;code&gt;postMessage&lt;/code&gt;. Two details decide whether it is worth anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It has to be in &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;, ahead of everything the build wrote.&lt;/strong&gt; Our first version appended the hook before &lt;code&gt;&amp;lt;/body&amp;gt;&lt;/code&gt;. A script that threw during load ran before any listener existed, so the loudest failures were exactly the ones nothing reported. The ordering is now pinned by a test that asserts the hook's index is lower than both &lt;code&gt;&amp;lt;/head&amp;gt;&lt;/code&gt; and any script the build placed in the body.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A &lt;code&gt;console.error&lt;/code&gt; is not a failure.&lt;/strong&gt; React logs ordinary development warnings through &lt;code&gt;console.error&lt;/code&gt;, and "each child in a list should have a unique key" is not a broken app. So the harness reports it as a separate field:&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;var&lt;/span&gt; &lt;span class="nx"&gt;ce&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;rep&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;logErr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[].&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;arguments&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;join&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="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;400&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;ce&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;arguments&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;Everything downstream that decides pass or fail checks &lt;code&gt;if (d.logErr) return;&lt;/code&gt; before it counts anything. The warning is still recorded, it just never fails a healthy build. Collapsing these two into one channel is how you get a verifier that cries wolf until people stop reading it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A verdict may get worse, never better
&lt;/h2&gt;

&lt;p&gt;The last rule is the one I would keep if I had to throw the rest away. Verdicts only travel in one direction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;files&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;vFiles&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rendered&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;tn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rendered&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;tn&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once a build has been marked bad, no later signal is allowed to mark it good. A second render, a retry, a re-mount, an error that happens not to repeat: none of them can promote a failure back to a success. Combined with a 9 second window after mount during which errors still count, this closes the obvious escape hatch where a flaky build eventually reports clean and the user is told everything is fine.&lt;/p&gt;

&lt;p&gt;And the green tick now waits. Between "the stream finished" and "the frame answered" the UI says it is checking whether the build opens. Neutral, not a claim. We would rather show uncertainty for three seconds than certainty that turns out to be wrong.&lt;/p&gt;

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

&lt;p&gt;The bug was not the regex. The regex was fine at what it did. The bug was letting the cheap observable stand in for the expensive one, because the expensive one arrives late and the cheap one is available right now.&lt;/p&gt;

&lt;p&gt;Three questions worth asking about your own pipeline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What does your success signal actually observe? Not what it is named after.&lt;/li&gt;
&lt;li&gt;Does anything irreversible, like billing, happen before the real answer arrives?&lt;/li&gt;
&lt;li&gt;Can a failure ever be promoted back to a success by a later, quieter signal?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you want to watch the version with the harness in it, &lt;a href="https://zugo.dev" rel="noopener noreferrer"&gt;Zugo&lt;/a&gt; is free to try, and the &lt;a href="https://zugo.dev/templates" rel="noopener noreferrer"&gt;templates&lt;/a&gt; are all real builds you can open and edit.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>We published 1,360 blog posts in five days. Google indexed 69.</title>
      <dc:creator>Zugo</dc:creator>
      <pubDate>Sun, 02 Aug 2026 06:37:53 +0000</pubDate>
      <link>https://dev.to/zugodev/we-published-1360-blog-posts-in-a-day-google-indexed-69-2ded</link>
      <guid>https://dev.to/zugodev/we-published-1360-blog-posts-in-a-day-google-indexed-69-2ded</guid>
      <description>&lt;p&gt;We took a blog from 84 posts to 1,360 in five days, across six languages. The biggest single day was 515 posts. Google's verdict, measured in Search Console the morning after the last batch: &lt;strong&gt;69 indexed, 43 discovered and not indexed, the rest not even discovered.&lt;/strong&gt; Publishing is not the bottleneck. Here is what actually was.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Discovered, currently not indexed" is a verdict, not a bug
&lt;/h2&gt;

&lt;p&gt;That status is Google saying it knows the URL exists and has decided not to spend crawl budget on it. No console setting changes that. I went through every togglable thing in Google Search Console, Bing Webmaster Tools and Yandex Webmaster to be sure. What each console gave us:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;Effect&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Resubmit sitemap&lt;/td&gt;
&lt;td&gt;Bing went from 11 known URLs to 1.1K&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Raise Bing crawl rate to max&lt;/td&gt;
&lt;td&gt;more requests allowed, not more indexing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IndexNow ping&lt;/td&gt;
&lt;td&gt;1,068 URLs accepted in seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Yandex recrawl queue&lt;/td&gt;
&lt;td&gt;150 URLs per day, hard cap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google request-indexing&lt;/td&gt;
&lt;td&gt;~10 URLs per day, hard cap&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All of it is discovery. None of it is authority. A thousand pages that nothing links to are a thousand pages Google will get around to eventually, or not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mistake that cost the most
&lt;/h2&gt;

&lt;p&gt;Every one of those 1,360 pages was reachable only from one hub. The hub listed all of them, so it looked fine. It is not fine.&lt;/p&gt;

&lt;p&gt;A hub with 250 cards divides whatever authority it has by 250. To reach a post, a crawler loads a 257 KB page and walks a flat list. There is no path &lt;em&gt;between&lt;/em&gt; posts, so nothing pulls a crawler deeper than one hop from the hub.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we changed: a ring, not a top-six
&lt;/h2&gt;

&lt;p&gt;The obvious fix is a "related posts" block. The obvious implementation is wrong.&lt;/p&gt;

&lt;p&gt;If every post links to the top six in its group, six pages collect a thousand inbound links and the rest collect zero. That is a funnel, not a mesh.&lt;/p&gt;

&lt;p&gt;A ring fixes it. Sort the group, then post &lt;code&gt;i&lt;/code&gt; links to posts &lt;code&gt;i+1 … i+6&lt;/code&gt;, wrapping around:&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="nx"&gt;list&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="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;localeCompare&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="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&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="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;picked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;k&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="nx"&gt;k&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;picked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="nx"&gt;related&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;list&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="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;picked&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 post now has exactly six outgoing and about six incoming links. A crawl that starts anywhere reaches everything. Internal links per page went from 10 to 16, and no page is an orphan by construction rather than by care.&lt;/p&gt;

&lt;p&gt;Sort by slug, not by date. Dates change when you move a post, and a date-ordered ring rebuilds itself completely on the next publish, changing every internal link on the site at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  The other thing worth doing
&lt;/h2&gt;

&lt;p&gt;Structured data you already qualify for. Our engine supported &lt;code&gt;FAQPage&lt;/code&gt; from day one through a frontmatter field. Out of 1,360 posts, 40 had filled it, carrying 42 questions between them. A capability that 97% of your own content walks past is close enough to a missing one.&lt;/p&gt;

&lt;p&gt;The articles already had question-shaped subheadings with an answer paragraph under each. Google's rule for &lt;code&gt;FAQPage&lt;/code&gt; is exactly one thing: the question and the answer must be visible on the page. They were. So the schema is now derived from the rendered article instead of a field:&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;re&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/&amp;lt;h&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;23&lt;/span&gt;&lt;span class="se"&gt;][^&lt;/span&gt;&lt;span class="sr"&gt;&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;*&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;([\s\S]&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;?)&lt;/span&gt;&lt;span class="sr"&gt;&amp;lt;&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;h&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;23&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;*&amp;lt;p&lt;/span&gt;&lt;span class="se"&gt;[^&lt;/span&gt;&lt;span class="sr"&gt;&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;*&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;([\s\S]&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;?)&lt;/span&gt;&lt;span class="sr"&gt;&amp;lt;&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;p&amp;gt;/gi&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only headings that actually end in a question mark qualify. Turning a statement into a question would mean marking up something the page does not say. Result: 1,157 pages carrying 8,217 questions, none of it invented.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we are still waiting on
&lt;/h2&gt;

&lt;p&gt;Honest ending: none of this has moved rankings yet, and it would be a lie to claim otherwise a day later. Discovery is fixed, the mesh is built, the schema is real. The remaining gap is external links, which no amount of engineering produces.&lt;/p&gt;

&lt;p&gt;If you are in the same position, the order that matters is: make every page reachable from more than one place, submit properly once, then go get links. The consoles are the smallest of the three.&lt;/p&gt;

&lt;p&gt;We write this stuff up while building &lt;a href="https://zugo.dev" rel="noopener noreferrer"&gt;Zugo&lt;/a&gt;, which turns a written description into a working game, site or app. More of the measured kind on the blog: &lt;a href="https://zugo.dev/blog/what-can-ai-not-build/" rel="noopener noreferrer"&gt;what AI builders cannot do&lt;/a&gt; and &lt;a href="https://zugo.dev/blog/how-long-does-it-take/" rel="noopener noreferrer"&gt;how long a build actually takes&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Two h1 tags on 500 pages: the bug that lives between two correct halves</title>
      <dc:creator>Zugo</dc:creator>
      <pubDate>Sat, 01 Aug 2026 22:20:43 +0000</pubDate>
      <link>https://dev.to/zugodev/two-h1-tags-on-500-pages-the-bug-that-lives-between-two-correct-halves-2aha</link>
      <guid>https://dev.to/zugodev/two-h1-tags-on-500-pages-the-bug-that-lives-between-two-correct-halves-2aha</guid>
      <description>&lt;p&gt;Our blog grew from 84 posts to 1,360 in a day. Then I read one live page instead of the source, counted tags, and found two &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; elements on 500 of them. Both halves of the code were correct. Their sum was not, and no test could see it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the second heading came from
&lt;/h2&gt;

&lt;p&gt;The post template prints the title from frontmatter:&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;h1&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="nx"&gt;fm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&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;/h1&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;Every article starts with its own heading, because that is what a markdown document looks like:&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="gh"&gt;# How to build a CRM without code&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;marked&lt;/code&gt; turns that into another &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt;. Nothing in either file is wrong. A markdown document that opens with a level-one heading is correct markdown. A template that prints the page title as the page heading is a correct template. The defect exists only in the output, which is the one artifact nobody was testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the tests were green
&lt;/h2&gt;

&lt;p&gt;The suite had guards for the corpus and guards for the renderer:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Guard&lt;/th&gt;
&lt;th&gt;What it read&lt;/th&gt;
&lt;th&gt;Would it catch two h1?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;corpus checker&lt;/td&gt;
&lt;td&gt;the &lt;code&gt;.md&lt;/code&gt; files&lt;/td&gt;
&lt;td&gt;no, markdown was valid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;template tests&lt;/td&gt;
&lt;td&gt;the component&lt;/td&gt;
&lt;td&gt;no, template emits one&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;build&lt;/td&gt;
&lt;td&gt;exit code&lt;/td&gt;
&lt;td&gt;no, build succeeded&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every one of them passed, forever, while a thousand pages shipped a broken heading hierarchy. Search engines get to pick which heading is the page title in that situation, and screen readers announce a structure that does not exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix is one line, in one place
&lt;/h2&gt;

&lt;p&gt;Not in 500 files. The markdown is not at fault, so editing it would be fixing the wrong thing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;marked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&amp;lt;h1&lt;/span&gt;&lt;span class="se"&gt;(\s[^&lt;/span&gt;&lt;span class="sr"&gt;&amp;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;)?&lt;/span&gt;&lt;span class="sr"&gt;&amp;gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;h2$1&amp;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;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&amp;lt;&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;h1&amp;gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/h2&amp;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;Demoted rather than deleted: if the body heading ever diverges from the frontmatter title, the text stays on the page instead of vanishing silently.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part worth stealing
&lt;/h2&gt;

&lt;p&gt;The gate runs on the finished page, not on the inputs:&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;h1s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&amp;lt;h1&lt;/span&gt;&lt;span class="se"&gt;[\s&lt;/span&gt;&lt;span class="sr"&gt;&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/g&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="nx"&gt;length&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;h1s&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;fail&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;slug&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;h1s&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; h1 tags, expected exactly one`&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 three lines and it catches a whole class. I verified it by mutation: remove the demotion and the build fails on the first article, which is the only evidence that a green gate means anything.&lt;/p&gt;

&lt;p&gt;Counting things that must be unique is cheap and it works for more than headings. &lt;code&gt;canonical&lt;/code&gt;, &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;og:url&lt;/code&gt; all have the same property: exactly one, always, and a duplicate is invisible until someone looks at the output.&lt;/p&gt;

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

&lt;p&gt;When both sides of a seam look correct and the result is wrong, check the &lt;strong&gt;output&lt;/strong&gt;, not the inputs. I hit the same shape twice more the same week: a checker that read the source and passed while the built HTML was broken, and a guard that fired on correct text because it matched a substring instead of a structure.&lt;/p&gt;

&lt;p&gt;If your site generator has a template and a content directory, you probably have a version of this. It costs one command to find out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;h1'&lt;/span&gt; dist/some/page/index.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I write about this kind of thing while building &lt;a href="https://zugo.dev" rel="noopener noreferrer"&gt;Zugo&lt;/a&gt;, which turns a written description into a working game, site or app. The blog has more of the measured-not-guessed variety, for example &lt;a href="https://zugo.dev/blog/what-can-ai-not-build/" rel="noopener noreferrer"&gt;what AI builders cannot do&lt;/a&gt; and &lt;a href="https://zugo.dev/blog/accessibility/" rel="noopener noreferrer"&gt;whether AI-built sites are accessible&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>seo</category>
      <category>testing</category>
    </item>
  </channel>
</rss>
