<?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: Jibola Timothy Kolawole</title>
    <description>The latest articles on DEV Community by Jibola Timothy Kolawole (@ags_anchor).</description>
    <link>https://dev.to/ags_anchor</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%2F3947426%2Fd9853fb1-3cfb-4416-9e85-5e4f7e090ea6.png</url>
      <title>DEV Community: Jibola Timothy Kolawole</title>
      <link>https://dev.to/ags_anchor</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ags_anchor"/>
    <language>en</language>
    <item>
      <title>I Thought I Understood SPAs. A Blank Screen Proved Me Wrong.</title>
      <dc:creator>Jibola Timothy Kolawole</dc:creator>
      <pubDate>Sat, 23 May 2026 10:29:00 +0000</pubDate>
      <link>https://dev.to/ags_anchor/i-thought-i-understood-spas-a-blank-screen-proved-me-wrong-1ca0</link>
      <guid>https://dev.to/ags_anchor/i-thought-i-understood-spas-a-blank-screen-proved-me-wrong-1ca0</guid>
      <description>&lt;p&gt;I had AI help me structure the router. It looked clean. The logic flowed. I shipped it.&lt;/p&gt;

&lt;p&gt;Then I opened the browser and saw a blank white page.&lt;/p&gt;

&lt;p&gt;No error. No warning. Just silence. 🦗&lt;/p&gt;

&lt;h2&gt;
  
  
  🧱 The Blank Screen Problem
&lt;/h2&gt;

&lt;p&gt;Here's what the router was supposed to do — and what it was actually doing.&lt;/p&gt;

&lt;p&gt;The architecture was simple enough on paper: &lt;code&gt;index.html&lt;/code&gt; holds the landing page. A separate &lt;code&gt;#app-container&lt;/code&gt; div catches everything else — auth screens, dashboards. The router listens to the URL and decides what to render where.&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;async&lt;/span&gt; &lt;span class="nf"&gt;_render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ...identify path...&lt;/span&gt;
  &lt;span class="c1"&gt;// ...check auth...&lt;/span&gt;
  &lt;span class="c1"&gt;// ...show loading...&lt;/span&gt;
  &lt;span class="c1"&gt;// ...render view...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Logical. Clean. Broken in four different ways simultaneously. 🙃&lt;/p&gt;

&lt;h2&gt;
  
  
  🐛 Bug #1: The &lt;code&gt;export&lt;/code&gt; That Silently Killed a Module
&lt;/h2&gt;

&lt;p&gt;Inside &lt;code&gt;dashCustomer.js&lt;/code&gt;, the entire dashboard view was wrapped in a &lt;code&gt;DOMContentLoaded&lt;/code&gt; callback:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DOMContentLoaded&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="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;container&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CustomerDashView&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;   &lt;span class="c1"&gt;// 💀 fatal syntax error&lt;/span&gt;
    &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// 400+ lines of dashboard code&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;export&lt;/code&gt; must be at the top level of a module. You cannot export from inside a function or callback. This is a hard syntax rule in ES modules.&lt;/p&gt;

&lt;p&gt;The browser's response to this? It silently refuses to parse the file. 🤐 No module loads. The router tries to import &lt;code&gt;CustomerDashView&lt;/code&gt;, gets &lt;code&gt;undefined&lt;/code&gt;, tries to call &lt;code&gt;undefined.render()&lt;/code&gt;, and throws an error so far downstream from the real cause that you spend twenty minutes looking at the wrong file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix was just two lines — remove the wrapper ✂️:&lt;/strong&gt;&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;// ✅ No DOMContentLoaded. No wrapper. Just this:&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CustomerDashView&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// dashboard code&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🐛 Bug #2: Calling &lt;code&gt;appendChild&lt;/code&gt; on a Plain Object
&lt;/h2&gt;

&lt;p&gt;With the syntax error fixed, something rendered. Then it immediately crashed with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;❌ TypeError: Failed to execute 'appendChild' on 'Element': 
   parameter 1 is not of type 'Node'.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The router was doing 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;view&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;routeLoader&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;appContainer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// ❌ wrong assumption&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The view files don't return DOM nodes. They return plain objects with a &lt;code&gt;.render()&lt;/code&gt; method:&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;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CustomerDashView&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&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;// attach logic, load data, etc.&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;appendChild&lt;/code&gt; expects a DOM element. 🧩 Passing it a plain JavaScript object throws immediately. The fix required understanding &lt;em&gt;what kind of thing&lt;/em&gt; the view actually was:&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;view&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;routeLoader&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;view&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;render&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;function&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;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;appContainer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;       &lt;span class="c1"&gt;// ✅ for object-style views&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;view&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;Node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;appContainer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// ✅ for views that return DOM nodes&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🐛 Bug #3: The Container That Was Always Hidden
&lt;/h2&gt;

&lt;p&gt;After both fixes, the router was loading the view. The data was fetching. The JavaScript was running. The page was still blank. 😶&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;#app-container&lt;/code&gt; in &lt;code&gt;index.html&lt;/code&gt; had this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"app-container"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"display:none"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The router was rendering an entire working dashboard into an invisible box. 📦 Nobody had written the line to show it.&lt;/p&gt;

&lt;p&gt;No error. No warning. The app was working perfectly, invisibly. 👻&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;// 🤦 This single line was simply missing:&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;appContainer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;appContainer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;display&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🐛 Bug #4: A Variable Used Before It Was Declared
&lt;/h2&gt;

&lt;p&gt;While adding the visibility fix, I introduced a new one:&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;// 🔝 Early in the function:&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;appContainer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;appContainer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;display&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="c1"&gt;// 💥 ReferenceError&lt;/span&gt;

&lt;span class="c1"&gt;// ...15 lines of other logic...&lt;/span&gt;

&lt;span class="c1"&gt;// 👇 Only declared down here:&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;appContainer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app-container&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;const&lt;/code&gt; doesn't hoist. ⚠️ I'd moved the visibility toggle above the declaration during refactoring and didn't notice. The variable was being referenced before it existed.&lt;/p&gt;

&lt;p&gt;The fix was simply to move the &lt;code&gt;const appContainer&lt;/code&gt; declaration to the top of the function — before the first point it was used. 🔼&lt;/p&gt;

&lt;h2&gt;
  
  
  💬 The Moment That Actually Mattered
&lt;/h2&gt;

&lt;p&gt;By the time I'd found and fixed these bugs, it was late. I was frustrated. I messaged someone and said something like: &lt;em&gt;"I've been using AI to write this code and honestly I understand most of it but maybe not all of it."&lt;/em&gt; 😬&lt;/p&gt;

&lt;p&gt;The response stopped me:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"AI writes code you don't understand. It works. Then it breaks. You don't know why. You ask AI to fix it. It breaks something else. You're stuck in a loop with no exit — which is exactly what happened with your routing bug tonight."&lt;/em&gt; 🔁&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That was accurate. That's exactly what had happened.&lt;/p&gt;

&lt;p&gt;But then came the part I actually needed to hear 👇:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"A basic SPA is actually just 3 ideas:&lt;/em&gt;&lt;br&gt;
&lt;em&gt;📄 One HTML file with a div that acts as a container.&lt;/em&gt;&lt;br&gt;
&lt;em&gt;🔄 JavaScript that listens to the URL and swaps content into that container.&lt;/em&gt;&lt;br&gt;
&lt;em&gt;🔗 A way to change the URL without reloading the page using &lt;code&gt;history.pushState&lt;/code&gt;.&lt;/em&gt;&lt;br&gt;
&lt;em&gt;That's it. Everything else is just complexity on top of those 3 things."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I'd been working with a router that had auth guards, lazy loading, body class management, header visibility toggling, loading spinners, view mounting strategies. And I hadn't been able to debug it confidently because I'd lost sight of what a router fundamentally &lt;em&gt;is&lt;/em&gt;. 🗺️&lt;/p&gt;

&lt;p&gt;Here it is in its purest form 👇:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"app"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
  &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&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;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;h1&amp;gt;Home&amp;lt;/h1&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/about&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;h1&amp;gt;About&amp;lt;/h1&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;h1&amp;gt;Not Found&amp;lt;/h1&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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a&lt;/span&gt;&lt;span class="dl"&gt;'&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;a&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;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pushState&lt;/span&gt;&lt;span class="p"&gt;({},&lt;/span&gt; &lt;span class="dl"&gt;''&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;href&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;popstate&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

  &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 🚀 boot&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole contract. 📜 Every SPA framework, every custom router, every &lt;code&gt;react-router&lt;/code&gt; or &lt;code&gt;vue-router&lt;/code&gt; you've ever used is this — with abstractions layered on top.&lt;/p&gt;

&lt;p&gt;Once I saw that, the bugs from earlier became obvious in a different way 💡:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔴 The &lt;code&gt;export&lt;/code&gt; error broke the module so no view could load&lt;/li&gt;
&lt;li&gt;🔴 The &lt;code&gt;appendChild&lt;/code&gt; error meant the router didn't understand what a "view" was&lt;/li&gt;
&lt;li&gt;🔴 The &lt;code&gt;display:none&lt;/code&gt; bug meant the container never became part of the visible page&lt;/li&gt;
&lt;li&gt;🔴 The hoisting bug meant the container reference didn't exist when the code needed it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each bug was a violation of one of those three core ideas. And I couldn't see that while I was staring at 200 lines of code I hadn't fully internalized. 😔&lt;/p&gt;

&lt;h2&gt;
  
  
  🎯 The Honest Takeaway
&lt;/h2&gt;

&lt;p&gt;I'm not writing this to tell you to stop using AI tools. I use them constantly. They're genuinely useful for scaffolding, for boilerplate, for getting a working first version fast. 🤖&lt;/p&gt;

&lt;p&gt;But there's a specific failure mode that's easy to fall into: you ask AI to write something, it works, you move on, and you never actually learn what it does. Then when it breaks — and it &lt;em&gt;will&lt;/em&gt; break — you're standing in front of a blank screen with no map. 🗺️❌&lt;/p&gt;

&lt;p&gt;The process that actually works, the one I ended up doing last night whether I intended to or not:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;🤖 Let AI write the first version&lt;/li&gt;
&lt;li&gt;👀 Read every line. Ask "what does this do?"&lt;/li&gt;
&lt;li&gt;🙋 Ask AI to explain anything you don't recognise&lt;/li&gt;
&lt;li&gt;🗑️ Delete a piece and try to rewrite it yourself&lt;/li&gt;
&lt;li&gt;💥 Break something deliberately and fix it yourself&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Step 5 is the one that sticks. 📌 I found four bugs last night under pressure, in code I was uncertain about, and understood every fix by the time I was done. That understanding is mine now. It didn't come from reading documentation or watching a tutorial.&lt;/p&gt;

&lt;p&gt;It came from the blank screen. 🖥️&lt;/p&gt;

&lt;h2&gt;
  
  
  ⚙️ What I'm Doing Differently
&lt;/h2&gt;

&lt;p&gt;Every time I use a significant piece of code I didn't write from scratch, I now ask myself one question before moving on:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💭 &lt;em&gt;"Can I explain what breaks if I delete this line?"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If the answer is no, that's a gap. Not a crisis — gaps are fine and normal. 🙂 But I write it down. I come back to it. I poke at it until I can answer the question.&lt;/p&gt;

&lt;p&gt;The router works now. ✅ The dashboards load. ✅ But more importantly, I know exactly &lt;em&gt;why&lt;/em&gt; — and I know what I'd look at first if the screen went blank again.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;🚚 I'm building OffScape Logistics, a delivery platform for businesses in Lagos and Ibadan with real-time GPS tracking, escrow payments, and role-based dashboards. If you're building something similar or just want to talk about SPA architecture, I'm on Twitter Creative @astra9cc&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  JavaScript, #WebDevelopment, #SPA, #Debugging, #LearningtoCode, #BuildinginPublic
&lt;/h1&gt;

</description>
      <category>javascript</category>
      <category>react</category>
      <category>buildinpublic</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I've audited dozens of estate agency websites. The same 5 problems show up every single time.</title>
      <dc:creator>Jibola Timothy Kolawole</dc:creator>
      <pubDate>Sat, 23 May 2026 10:13:52 +0000</pubDate>
      <link>https://dev.to/ags_anchor/ive-audited-dozens-of-estate-agency-websites-the-same-5-problems-show-up-every-single-time-7go</link>
      <guid>https://dev.to/ags_anchor/ive-audited-dozens-of-estate-agency-websites-the-same-5-problems-show-up-every-single-time-7go</guid>
      <description>&lt;p&gt;&lt;strong&gt;Estate agency websites sit in an interesting category from a technical standpoint — high commercial intent traffic, predominantly mobile, long decision cycles, and almost universally under-optimised.&lt;br&gt;
After running manual performance and conversion audits on a significant number of them, the same five issues appear with remarkable consistency. This isn't a marketing post — it's a technical pattern recognition writeup.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;LCP in the 6–10 second range on mobile
Almost always the same root causes:&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Hero images uncompressed, no WebP, no srcset&lt;/li&gt;
&lt;li&gt;No fetchpriority="high" on the LCP image element&lt;/li&gt;
&lt;li&gt;Render-blocking third-party scripts (GTM, chat widgets, 
CRM trackers) firing before first paint&lt;/li&gt;
&lt;li&gt;Fonts loading without font-display: swap&lt;/li&gt;
&lt;li&gt;No preconnect hints for external origins&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;The fix that moves the needle most consistently: compress the hero image, set fetchpriority="high", defer non-critical scripts.&lt;br&gt;
One client went from LCP 8.1s → 2.3s on mobile just from image optimisation and script deferral alone.&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;No meaningful content above the fold on mobile
This is partly a UX problem and partly a technical one.
The pattern: full-viewport hero image loads, content reflows underneath it, by which point the user has already made their decision about the page.
The CLS implication is real too — images without explicit width and height attributes cause layout shift as they load, directly impacting Core Web Vitals scores.
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;`html&lt;span class="c"&gt;&amp;lt;!-- What we find --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"hero.jpg"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- What it should be --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; 
  &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"hero.webp"&lt;/span&gt; 
  &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"1200"&lt;/span&gt; 
  &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"600"&lt;/span&gt; 
  &lt;span class="na"&gt;fetchpriority=&lt;/span&gt;&lt;span class="s"&gt;"high"&lt;/span&gt;
  &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Property search"&lt;/span&gt;
&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;`
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;Form friction — too many fields, no progressive disclosure
The technical pattern here is almost always a legacy CRM integration that dumps every available field into the form by default.
What actually converts:&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;Step 1: Name + Email (or phone) — capture the lead&lt;br&gt;
Step 2: Qualification questions — only shown after step 1&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Multi-step forms with progressive disclosure consistently outperform single long forms. Libraries like React Hook Form or even a basic vanilla JS step controller handle this cleanly.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;No micro-conversion infrastructure
Most estate agency sites have exactly one conversion point: the contact form.
There's no:&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;Property alert signup (email capture with listing criteria)&lt;br&gt;
Instant valuation widget (high-intent lead capture)&lt;br&gt;
Area guide download (top-of-funnel email collection)&lt;br&gt;
Exit intent capture&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;From a technical standpoint, none of these are complex to build. Property alerts can be as simple as a Mailchimp-integrated form with conditional tagging. But they're almost universally absent.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Mobile performance debt
Beyond raw load times, the structural mobile issues we see consistently:&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Touch targets under 44px (Google's recommended minimum)&lt;/li&gt;
&lt;li&gt;Fixed desktop navigation not adapted for thumb zones&lt;/li&gt;
&lt;li&gt;Images sized for desktop served to mobile 
(no responsive images / srcset)&lt;/li&gt;
&lt;li&gt;No lazy loading on below-fold images&lt;/li&gt;
&lt;li&gt;CSS not split — full desktop stylesheet served to mobile&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;A real device audit almost always reveals UX debt that emulators miss. Lighthouse mobile emulation is useful but not a substitute.&lt;/p&gt;

&lt;p&gt;**&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The aggregate impact&lt;br&gt;
Across the audits we've run, fixing the above — without rebuilding the site — consistently moves monthly enquiry volume by 30–70%.&lt;br&gt;
**&lt;br&gt;
The technical lift is rarely dramatic. It's almost always:&lt;br&gt;
&lt;/p&gt;


&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- Image pipeline cleanup
- Script load order optimisation
- Form restructuring
- Basic micro-conversion tooling
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you work on real estate sites or service business sites generally — curious what patterns you're seeing. Anything consistently missing from this list?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Built with: Lighthouse, PageSpeed Insights, GA4, manual device testing&lt;/strong&gt;&lt;br&gt;
Tags: #WebPerf #CoreWebVitals #RealEstate #ConversionOptimisation #agelessbusinessteam&lt;/p&gt;

</description>
      <category>corevitals</category>
      <category>javascript</category>
      <category>seo</category>
      <category>webperf</category>
    </item>
  </channel>
</rss>
