<?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: Made Me The Dev</title>
    <description>The latest articles on DEV Community by Made Me The Dev (@mademethedev).</description>
    <link>https://dev.to/mademethedev</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3817639%2F1c6f5c1e-2a3e-421d-80fa-ab81dbddeea6.png</url>
      <title>DEV Community: Made Me The Dev</title>
      <link>https://dev.to/mademethedev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mademethedev"/>
    <language>en</language>
    <item>
      <title>I launched my typing test on Product Hunt today — it has a Song mode for some reason</title>
      <dc:creator>Made Me The Dev</dc:creator>
      <pubDate>Wed, 03 Jun 2026 08:46:38 +0000</pubDate>
      <link>https://dev.to/mademethedev/i-launched-my-typing-test-on-product-hunt-today-it-has-a-song-mode-for-some-reason-1f8p</link>
      <guid>https://dev.to/mademethedev/i-launched-my-typing-test-on-product-hunt-today-it-has-a-song-mode-for-some-reason-1f8p</guid>
      <description>&lt;p&gt;I got annoyed by typing sites that don't work well on mobile, so I built my own. That was a few months ago. It's now a whole thing with 4 modes, badges, XP, and a rhythm game.&lt;/p&gt;

&lt;p&gt;The rhythm game (Song mode) is the weird part — notes fall down 4 lanes and you press the matching key when they hit the zone. Started as a joke. Kept it because it's actually fun.&lt;/p&gt;

&lt;p&gt;Also has a Daily Challenge where every player globally gets the same 10 sentences, computed client-side from a hash of the UTC date. No server, just math. Same prompts for everyone, every day.&lt;/p&gt;

&lt;p&gt;Built it solo, pure HTML/CSS/JS, no frameworks. Lighthouse 100/100/100/100.&lt;/p&gt;

&lt;p&gt;Just launched on Product Hunt today — if you want to check it out or throw an upvote:&lt;br&gt;
&lt;a href="https://www.producthunt.com/products/typevelocity?launch=typevelocity" rel="noopener noreferrer"&gt;https://www.producthunt.com/products/typevelocity?launch=typevelocity&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Or play it directly:&lt;br&gt;
&lt;a href="https://typevelocity-nu.vercel.app" rel="noopener noreferrer"&gt;https://typevelocity-nu.vercel.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Honest feedback welcome too, good or bad.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>I Broke My Own Typing Game and Git History Saved Me</title>
      <dc:creator>Made Me The Dev</dc:creator>
      <pubDate>Sat, 30 May 2026 17:30:30 +0000</pubDate>
      <link>https://dev.to/mademethedev/i-broke-my-own-typing-game-and-git-history-saved-me-136m</link>
      <guid>https://dev.to/mademethedev/i-broke-my-own-typing-game-and-git-history-saved-me-136m</guid>
      <description>&lt;p&gt;On May 13, 2026, seven images silently disappeared from &lt;a href="https://typevelocity-nu.vercel.app" rel="noopener noreferrer"&gt;TypeVelocity&lt;/a&gt;. Not with an error. Not with a warning. They just stopped being deployed — and the two blog posts referencing them served broken image slots to anyone who visited for 17 days straight.&lt;/p&gt;

&lt;p&gt;I found out today, May 30. Not from monitoring. Not from an alert. Someone pointed it out while I was working on something else.&lt;/p&gt;

&lt;h2&gt;
  
  
  The deploy pipeline
&lt;/h2&gt;

&lt;p&gt;TypeVelocity doesn't use &lt;code&gt;git push&lt;/code&gt; to deploy. It uses a custom Node.js script that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Minifies CSS and JS&lt;/li&gt;
&lt;li&gt;Copies files to a &lt;code&gt;results/&lt;/code&gt; folder&lt;/li&gt;
&lt;li&gt;Pushes &lt;strong&gt;only&lt;/strong&gt; the files in a hardcoded &lt;code&gt;FILES&lt;/code&gt; array to GitHub via the API&lt;/li&gt;
&lt;li&gt;Vercel picks it up from there&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;code&gt;FILES&lt;/code&gt; list looks like 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;const&lt;/span&gt; &lt;span class="nx"&gt;FILES&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="na"&gt;local&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;results/index.html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;index.html&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;local&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;styles.min.css&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;styles.min.css&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;local&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;script.min.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;script.min.js&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;local&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;blogs/assets/some-image.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;blogs/assets/some-image.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="c1"&gt;// ... 50+ more entries&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every file that goes live has to be in this list. It's maintained by hand. When you add a blog post or an image, you add it to the array. If you forget, the file doesn't deploy. The script doesn't warn you. It just skips it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happened
&lt;/h2&gt;

&lt;p&gt;On May 13, I deployed the Daily Challenge update — a big feature, lots of new files. Somewhere in that deploy, seven image entries got dropped from the &lt;code&gt;FILES&lt;/code&gt; list.&lt;/p&gt;

&lt;p&gt;The images had been on the site since March:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;4 Lighthouse before/after screenshots&lt;/li&gt;
&lt;li&gt;3 UI comparison images&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All seven were present in the &lt;strong&gt;April 18&lt;/strong&gt; deploy. Gone by &lt;strong&gt;May 13&lt;/strong&gt;. The script ran fine. No errors. The images it skipped just quietly stopped being included.&lt;/p&gt;

&lt;p&gt;The blogs referencing them — the &lt;a href="https://typevelocity-nu.vercel.app/blogs/typevelocity-lighthouse-100-march-2026" rel="noopener noreferrer"&gt;Lighthouse 100 post&lt;/a&gt; and the &lt;a href="https://typevelocity-nu.vercel.app/blogs/typevelocity-update-lives-ranks-march-2026" rel="noopener noreferrer"&gt;lives and ranks update&lt;/a&gt; — kept loading fine. Text, styles, everything. Just empty &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; boxes where the screenshots should have been.&lt;/p&gt;

&lt;p&gt;Nobody reported it. I didn't catch it in review. It was silently broken for 17 days.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding the exact commit
&lt;/h2&gt;

&lt;p&gt;I needed to know when it broke. Git history has the answer.&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="c"&gt;# Check each commit between Mar 20 and May 26&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;sha &lt;span class="k"&gt;in &lt;/span&gt;4aee7f5b c3da2958 4361a4f6 d4934aa1 9f0bef3a 027ed418 e594fa8d dc6a6e96
&lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;has&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;gh api repos/mademethedev0/TypeVelocity/git/trees/&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;sha&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;?recursive&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="se"&gt;\&lt;/span&gt;
    | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"typevelocity-100-on-lighthouse"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$sha&lt;/span&gt;&lt;span class="s2"&gt;: images=&lt;/span&gt;&lt;span class="nv"&gt;$has&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;4aee7f5b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;images=1  # Apr 18 — present&lt;/span&gt;
&lt;span class="py"&gt;c3da2958&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;images=1  # Apr 18 — present&lt;/span&gt;
&lt;span class="py"&gt;4361a4f6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;images=0  # May 13 — GONE&lt;/span&gt;
&lt;span class="py"&gt;d4934aa1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;images=0  # May 17 — still gone&lt;/span&gt;
&lt;span class="err"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The images dropped in the May 13 commit. That's the Daily Challenge deploy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recovering from git blobs
&lt;/h2&gt;

&lt;p&gt;The images were gone locally too — deleted at some point after the deploy dropped them. But git doesn't delete blobs when you push new commits. They're still there, just not referenced by any current tree.&lt;/p&gt;

&lt;p&gt;Here's the recovery script:&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;spawn&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;child_process&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;REPO&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mademethedev0/TypeVelocity&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SHA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;4aee7f5bed1ae9446195ff3b6843e571c6c7a36b&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Apr 18 commit&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;OUT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&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="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;blogs/assets&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;IMGS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;typevelocity-100-on-lighthouse-desktop.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;typevelocity-100-on-lighthouse-mobile.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;typevelocity-before-any-optimization-on-lighthouse-desktop.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;typevelocity-before-any-optimization-on-lighthouse-mobile.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;typevelocity-update-new-ui-1.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;typevelocity-update-new-ui-2.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;typevelocity-update-new-ui-3.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ghApi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&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;proc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;spawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gh&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="s1"&gt;api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`repos/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;REPO&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;endpoint&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;out&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="nx"&gt;proc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;proc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;close&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;code&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;code&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;reject&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;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`gh exit &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;code&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;else&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="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="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="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Get full tree at that commit&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tree&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;ghApi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`git/trees/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;SHA&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;?recursive=1`&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;map&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;const&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;tree&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tree&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;blogs/assets/&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;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;basename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sha&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;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;img&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;IMGS&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;blobSha&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;img&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;blobSha&lt;/span&gt;&lt;span class="p"&gt;)&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="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`NOT FOUND: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;img&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;continue&lt;/span&gt;&lt;span class="p"&gt;;&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="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Fetching &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;img&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;blobSha&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)...`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blob&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;ghApi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`git/blobs/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;blobSha&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="c1"&gt;// Blob content is base64-encoded&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&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;\n&lt;/span&gt;&lt;span class="sr"&gt;/g&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="s1"&gt;base64&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&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="nx"&gt;OUT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;buf&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="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`  -&amp;gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;buf&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="s2"&gt; bytes`&lt;/span&gt;&lt;span class="p"&gt;);&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="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Done.&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;main&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="o"&gt;=&amp;gt;&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="nf"&gt;error&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="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;node recover-images.js
&lt;span class="go"&gt;Fetching typevelocity-100-on-lighthouse-desktop.png (a91441b2)...
&lt;/span&gt;&lt;span class="gp"&gt;  -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;105956 bytes
&lt;span class="go"&gt;Fetching typevelocity-100-on-lighthouse-mobile.png (724f2e40)...
&lt;/span&gt;&lt;span class="gp"&gt;  -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;97329 bytes
&lt;span class="c"&gt;...
&lt;/span&gt;&lt;span class="go"&gt;Done.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All seven images recovered in under 2 minutes. Redeployed. Live again.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm doing about it
&lt;/h2&gt;

&lt;p&gt;The real fix is automating the &lt;code&gt;FILES&lt;/code&gt; list — scan &lt;code&gt;results/&lt;/code&gt; and &lt;code&gt;blogs/assets/&lt;/code&gt; at deploy time instead of maintaining it by hand. That's an afternoon of work I haven't done yet.&lt;/p&gt;

&lt;p&gt;The short-term fix: I added a pre-deploy check that logs a warning for any file referenced in a blog post's &lt;code&gt;src=&lt;/code&gt; attributes that isn't in the &lt;code&gt;FILES&lt;/code&gt; list. Not automated, but it makes the gap visible instead of silent.&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;// Pre-deploy check (simplified)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blogFiles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readdirSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;blogs&lt;/span&gt;&lt;span class="dl"&gt;'&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;f&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;missing&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;const&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;blogFiles&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;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`blogs/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;file&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="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;srcs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;content&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;/src="&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;+&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="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;src&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;srcs&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;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;src&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;/src="&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;+&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;"/&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;assets/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fullPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`blogs/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;inList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;FILES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;remote&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;fullPath&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;inList&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;missing&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;fullPath&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Files referenced but not in deploy list:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&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;warn&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;f&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The thing that actually bothers me isn't that it broke. Things break. It's that it broke &lt;strong&gt;quietly&lt;/strong&gt; and stayed broken. A missing image doesn't throw an error anywhere in the pipeline. The build succeeds. The deploy succeeds. Everything looks fine until someone actually reads the blog.&lt;/p&gt;

&lt;p&gt;That's the gap worth closing — not "prevent all breakage" but "make breakage loud."&lt;/p&gt;

&lt;h2&gt;
  
  
  What I should have done (and what it would cost)
&lt;/h2&gt;

&lt;p&gt;Here's the prevention work I didn't do, with rough time estimates:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;th&gt;Prevents&lt;/th&gt;
&lt;th&gt;Effort&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Auto-scan &lt;code&gt;results/&lt;/code&gt; and &lt;code&gt;blogs/assets/&lt;/code&gt; at deploy time&lt;/td&gt;
&lt;td&gt;Files missing from deploy list&lt;/td&gt;
&lt;td&gt;2-3 hours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Validate all &lt;code&gt;src=&lt;/code&gt; and &lt;code&gt;href=&lt;/code&gt; references in HTML&lt;/td&gt;
&lt;td&gt;Broken image/link references&lt;/td&gt;
&lt;td&gt;1 hour&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Add a post-deploy smoke test that checks image load status&lt;/td&gt;
&lt;td&gt;Silent 404s on live site&lt;/td&gt;
&lt;td&gt;1-2 hours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Log warnings for files in &lt;code&gt;results/&lt;/code&gt; not in &lt;code&gt;FILES&lt;/code&gt; array&lt;/td&gt;
&lt;td&gt;Deploy list drift&lt;/td&gt;
&lt;td&gt;30 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Run Lighthouse CI on every deploy&lt;/td&gt;
&lt;td&gt;Performance/accessibility regressions&lt;/td&gt;
&lt;td&gt;1 hour setup&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Total: &lt;strong&gt;6-8 hours of prevention work&lt;/strong&gt; to avoid 17 days of broken images and 2 hours of recovery scrambling.&lt;/p&gt;

&lt;p&gt;The auto-scan is the big one. If the deploy script just walked &lt;code&gt;results/&lt;/code&gt; and &lt;code&gt;blogs/assets/&lt;/code&gt; and pushed everything it found, this entire incident wouldn't have happened. The manual &lt;code&gt;FILES&lt;/code&gt; array exists because I wanted explicit control over what deploys — but that control isn't worth the maintenance burden.&lt;/p&gt;

&lt;p&gt;The validation check is the quick win. 30 lines of code, runs in under a second, catches broken references before they go live.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lesson
&lt;/h2&gt;

&lt;p&gt;Solo projects break in ways that only get caught when someone's actually looking. I wasn't looking closely enough at those pages after the initial publish.&lt;/p&gt;

&lt;p&gt;If you're maintaining a custom deploy pipeline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Automate the file list&lt;/strong&gt; — don't maintain it by hand&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate references&lt;/strong&gt; — check that every &lt;code&gt;src=&lt;/code&gt; and &lt;code&gt;href=&lt;/code&gt; points to something that exists&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make failures loud&lt;/strong&gt; — silent skips are worse than errors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And if you ever accidentally nuke something from production: git history doesn't lie. The blobs are still there. You just have to go get them.&lt;/p&gt;




&lt;p&gt;TypeVelocity is a typing test with lives, ranks, a rhythm game mode, and way too many features for something that started as a weekend project. It's live at &lt;a href="https://typevelocity-nu.vercel.app" rel="noopener noreferrer"&gt;typevelocity-nu.vercel.app&lt;/a&gt;. The images are back. The blogs work again. Go type something.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>devops</category>
      <category>git</category>
      <category>productivity</category>
    </item>
    <item>
      <title>My typing game scored 41 on Lighthouse. Two days later it hit 100. Here's every fix.</title>
      <dc:creator>Made Me The Dev</dc:creator>
      <pubDate>Wed, 11 Mar 2026 02:48:16 +0000</pubDate>
      <link>https://dev.to/mademethedev/my-typing-game-scored-41-on-lighthouse-two-days-later-it-hit-100-heres-every-fix-320e</link>
      <guid>https://dev.to/mademethedev/my-typing-game-scored-41-on-lighthouse-two-days-later-it-hit-100-heres-every-fix-320e</guid>
      <description>&lt;p&gt;I built a typing test called &lt;a href="https://typevelocity-nu.vercel.app" rel="noopener noreferrer"&gt;TypeVelocity&lt;/a&gt;. Pure HTML, CSS, and JavaScript — no React, no Next.js, no framework. Just files and a build script.&lt;/p&gt;

&lt;p&gt;I finally ran Lighthouse on it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mobile: 41 Performance. 83 Accessibility. 92 SEO.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuf3sxt2fvl82j9yzuhse.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuf3sxt2fvl82j9yzuhse.png" alt="Before: Lighthouse mobile score of 41" width="698" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Desktop was a 70 which sounds better until you realize passing is 90.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faoswr5rv1jjn4x43jd32.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faoswr5rv1jjn4x43jd32.png" alt="Before: Lighthouse desktop score of 70" width="695" height="797"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two days of debugging later:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5ptkk4l2t02fd1bjx4qq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5ptkk4l2t02fd1bjx4qq.png" alt="After: Lighthouse mobile score of 100 across all categories" width="691" height="831"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpctamjdox16xic2bt1nm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpctamjdox16xic2bt1nm.png" alt="After: Lighthouse desktop score of 100 across all categories" width="719" height="839"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;100/100/100/100. Both mobile and desktop.&lt;/p&gt;

&lt;p&gt;Here's what was actually wrong and what fixed it.&lt;/p&gt;




&lt;h2&gt;
  
  
  CLS was 1.0 (yes, the maximum)
&lt;/h2&gt;

&lt;p&gt;This single issue was responsible for most of the damage.&lt;/p&gt;

&lt;p&gt;I load CSS asynchronously using the &lt;code&gt;media="print" onload&lt;/code&gt; trick — the browser doesn't block rendering for the stylesheet. Critical styles are inlined in a &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; tag in the HTML head so the page looks correct on first paint.&lt;/p&gt;

&lt;p&gt;Sounds smart. Except I forgot about the stuff that's supposed to be &lt;em&gt;hidden&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;I had two modal overlays, a cookie banner, an adblocker banner, and a tips popup in the DOM. All of them rely on CSS to stay invisible (&lt;code&gt;opacity: 0&lt;/code&gt;, &lt;code&gt;transform: translateY(100%)&lt;/code&gt;, etc). That CSS lives in the async stylesheet. So between HTML parse and stylesheet load, every single one of these elements was fully visible for a split second.&lt;/p&gt;

&lt;p&gt;Lighthouse saw them flash in and disappear. Cumulative Layout Shift: &lt;strong&gt;1.0&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The fix was five lines of inline CSS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.modal-overlay&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;fixed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;inset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;pointer-events&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.cookie-banner&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;fixed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;translateY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.adblock-banner&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;fixed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;translateY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;-100%&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;CLS dropped from 1.0 to 0.001. &lt;strong&gt;Performance went from 41 to 93 in one change.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you're async-loading CSS and wondering why your CLS is terrible, check if you have any hidden elements that aren't hidden in your inline critical CSS. This was the dumbest bug I've ever shipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Accessibility: 83 → 100
&lt;/h2&gt;

&lt;p&gt;A bunch of small things that added up:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;user-scalable=no&lt;/code&gt; on the viewport.&lt;/strong&gt; I had it to prevent zoom on the typing test. Turns out that's an accessibility fail — users with low vision need pinch-to-zoom. Removed it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No &lt;code&gt;&amp;lt;main&amp;gt;&lt;/code&gt; landmark.&lt;/strong&gt; Screen readers use landmarks to navigate. I had a &lt;code&gt;&amp;lt;header&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;footer&amp;gt;&lt;/code&gt; but nothing wrapping the actual page content. One tag.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Heading hierarchy.&lt;/strong&gt; Jumped from &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; to &lt;code&gt;&amp;lt;h3&amp;gt;&lt;/code&gt;, skipping &lt;code&gt;&amp;lt;h2&amp;gt;&lt;/code&gt;. Screen readers expect sequential order.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Missing aria-labels.&lt;/strong&gt; The typing input was an invisible field overlaying the text display — no label, no role, screen readers had no idea what it was.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Contrast ratios.&lt;/strong&gt; Cookie banner link was 2.17:1. Minimum is 4.5:1. Fixed the color.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decorative SVGs.&lt;/strong&gt; The logo, theme toggle icons — all missing &lt;code&gt;aria-hidden="true"&lt;/code&gt;. Screen readers were trying to announce them.&lt;/p&gt;

&lt;p&gt;None of these were hard. I just hadn't tested with accessibility in mind.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Google Fonts Problem
&lt;/h2&gt;

&lt;p&gt;After the CLS fix, Performance was stuck at 93. FCP, LCP, and Speed Index were all around 2.6s on Lighthouse's simulated slow 4G.&lt;/p&gt;

&lt;p&gt;I was loading JetBrains Mono and DM Sans from Google's CDN. 70KB of woff2 files, a CSS request, DNS lookups and TLS handshakes to two Google domains. Everything was async with &lt;code&gt;font-display: optional&lt;/code&gt; — not render-blocking. But on a throttled 1.6 Mbps connection, 70KB of fonts competing for bandwidth with your actual content makes a difference.&lt;/p&gt;

&lt;p&gt;I removed Google Fonts entirely.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;--font-ui&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;'DM Sans'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;system-ui&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;-apple-system&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;sans-serif&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="nt"&gt;--font-mono&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;'JetBrains Mono'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;'Fira Code'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;'Consolas'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;monospace&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fallback fonts were already in the stack. Without the Google Fonts &lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt;, the browser goes straight to system fonts. Zero external requests. The "3rd parties" section in Lighthouse disappeared completely.&lt;/p&gt;

&lt;p&gt;Visual difference? Barely noticeable. System UI fonts are genuinely good now. Consolas vs JetBrains Mono is visible if you compare them side by side, but nobody's doing that on a typing test.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That got us from 93 to 100.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Non-composited animations (the boring one)
&lt;/h2&gt;

&lt;p&gt;Lighthouse flagged 22 elements with non-composited CSS animations. Every result item in the modal had:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;transition&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nt"&gt;background&lt;/span&gt; &lt;span class="err"&gt;0&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="err"&gt;35&lt;/span&gt;&lt;span class="nt"&gt;s&lt;/span&gt; &lt;span class="nt"&gt;ease&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;border-color&lt;/span&gt; &lt;span class="err"&gt;0&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="err"&gt;35&lt;/span&gt;&lt;span class="nt"&gt;s&lt;/span&gt; &lt;span class="nt"&gt;ease&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two problems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;background&lt;/code&gt; shorthand transitions &lt;code&gt;background-position&lt;/code&gt; sub-properties, which can't be GPU-composited. Changed to &lt;code&gt;background-color&lt;/code&gt; everywhere.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;border-color&lt;/code&gt; can't be composited at all. These transitions were on modal elements that are hidden 99% of the time — they only fire if you toggle dark/light mode while the results modal is open. Removed them.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This probably didn't move the score directly. But cleaning it up felt right.&lt;/p&gt;

&lt;h2&gt;
  
  
  Other fixes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Cookie banner as LCP.&lt;/strong&gt; The cookie consent text was being detected as the Largest Contentful Paint because it was the biggest visible text on initial render. Deferred it with a 3.5s &lt;code&gt;setTimeout&lt;/code&gt;. Now the actual page content is the LCP.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Footer &lt;code&gt;content-visibility: auto&lt;/code&gt;.&lt;/strong&gt; Tells the browser to skip rendering off-screen content until the user scrolls to it. Less work during initial paint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;contain: layout style paint&lt;/code&gt; on modal overlays.&lt;/strong&gt; Tells the browser it can skip layout calculations for these elements when they're hidden.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Script defer.&lt;/strong&gt; &lt;code&gt;script.min.js&lt;/code&gt; loads with &lt;code&gt;defer&lt;/code&gt;. Total Blocking Time: 0ms.&lt;/p&gt;

&lt;h2&gt;
  
  
  What was worth how many points
&lt;/h2&gt;

&lt;p&gt;Rough breakdown based on testing each change:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;th&gt;Points gained&lt;/th&gt;
&lt;th&gt;Effort&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Inline hiding CSS for modals/banners&lt;/td&gt;
&lt;td&gt;~50 on mobile&lt;/td&gt;
&lt;td&gt;10 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remove AdSense script&lt;/td&gt;
&lt;td&gt;~23 (Best Practices)&lt;/td&gt;
&lt;td&gt;1 minute&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accessibility fixes (viewport, landmarks, aria, contrast)&lt;/td&gt;
&lt;td&gt;83 → 100&lt;/td&gt;
&lt;td&gt;1 hour&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remove Google Fonts&lt;/td&gt;
&lt;td&gt;~7 Performance&lt;/td&gt;
&lt;td&gt;5 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;background → background-color transitions&lt;/td&gt;
&lt;td&gt;~1-2&lt;/td&gt;
&lt;td&gt;30 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Footer/modal containment hints&lt;/td&gt;
&lt;td&gt;marginal&lt;/td&gt;
&lt;td&gt;15 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The CLS fix alone was worth more than everything else combined. If your score is low, find the big thing first. Don't start by optimizing CSS transitions when your modals are causing a full-page layout shift.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest part
&lt;/h2&gt;

&lt;p&gt;Going from 41 to 93 took maybe three hours. Finding the CLS bug, adding inline CSS, fixing accessibility, deferring the cookie banner.&lt;/p&gt;

&lt;p&gt;Going from 93 to 100 took the rest of two days. Auditing every CSS property in every transition. Removing custom fonts. Adding containment hints. Each optimization was worth a point, maybe two. Textbook diminishing returns.&lt;/p&gt;

&lt;p&gt;Was it worth it? For the number, yes. For actual user experience, the jump from 41 to 93 was where all the real improvement happened. The 93-to-100 push was for my own sanity.&lt;/p&gt;

&lt;p&gt;If you're building something and your Lighthouse score is rough — start with CLS. Check what's visible during that gap between HTML parse and CSS load. That's probably where your points went.&lt;/p&gt;




&lt;p&gt;By the way — &lt;a href="https://typevelocity-nu.vercel.app" rel="noopener noreferrer"&gt;TypeVelocity&lt;/a&gt; is the typing test I built all of this for. It's free, works on your phone, no signup, no account, you just open it and type. Words mode, sentences mode, and a rhythm game mode if you want something different.&lt;/p&gt;

&lt;p&gt;If you feel like trying it, cool. If not, that's fine too. Mostly I just wanted to share what I learned chasing that Lighthouse score because I couldn't find a single post that broke it down fix-by-fix with actual point values when I was stuck at 41.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>beginners</category>
      <category>performance</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
