<?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: Moises Hamui</title>
    <description>The latest articles on DEV Community by Moises Hamui (@mhaconsulting).</description>
    <link>https://dev.to/mhaconsulting</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%2F3976314%2F7c384c8e-af51-482e-85a4-6ec1c175c8f6.png</url>
      <title>DEV Community: Moises Hamui</title>
      <link>https://dev.to/mhaconsulting</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mhaconsulting"/>
    <language>en</language>
    <item>
      <title>5 JavaScript SEO Pitfalls That Are Quietly Killing Your Rankings</title>
      <dc:creator>Moises Hamui</dc:creator>
      <pubDate>Tue, 09 Jun 2026 16:51:26 +0000</pubDate>
      <link>https://dev.to/mhaconsulting/5-javascript-seo-pitfalls-that-are-quietly-killing-your-rankings-fjn</link>
      <guid>https://dev.to/mhaconsulting/5-javascript-seo-pitfalls-that-are-quietly-killing-your-rankings-fjn</guid>
      <description>&lt;p&gt;If you've shipped a React, Vue, or Next.js site that ranks worse than the old static version it replaced, you're not imagining it. Single-page apps and heavy client-side rendering can introduce silent SEO regressions that don't show up in Lighthouse — they only show up months later in your organic traffic graph.&lt;/p&gt;

&lt;p&gt;After auditing dozens of JavaScript-heavy sites at our digital marketing consultancy, the same issues come up again and again. Here are five that account for most of the damage.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Critical content rendered only on the client
&lt;/h2&gt;

&lt;p&gt;Googlebot does render JavaScript, but it does so on a delay and with a budget. If your H1, primary copy, or internal links only appear after &lt;code&gt;useEffect&lt;/code&gt; runs, they may be missed entirely or indexed late.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Use server-side rendering (SSR) or static generation for any content that needs to rank. In Next.js, that means &lt;code&gt;getStaticProps&lt;/code&gt; or &lt;code&gt;getServerSideProps&lt;/code&gt; over pure client-side fetching for above-the-fold content.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Bad: title only appears after hydration&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Post&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setPost&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;fetch&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/post&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;setPost&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Good: title is in the initial HTML&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getStaticProps&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;post&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;fetchPost&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Soft 404s on dynamic routes
&lt;/h2&gt;

&lt;p&gt;When a dynamic route is hit with an invalid ID, many SPAs render an "Oops, not found" component while returning HTTP 200. Google logs these as soft 404s and eventually drops the URLs from the index — sometimes taking the whole pattern with them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Return a real 404 status from your server or framework. In Next.js: &lt;code&gt;return { notFound: true }&lt;/code&gt; from &lt;code&gt;getStaticProps&lt;/code&gt;. In a custom Express setup: &lt;code&gt;res.status(404)&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Internal links that aren't actually links
&lt;/h2&gt;

&lt;p&gt;I keep seeing &lt;code&gt;&amp;lt;div onClick={() =&amp;gt; router.push('/foo')}&amp;gt;&lt;/code&gt; instead of &lt;code&gt;&amp;lt;a href="/foo"&amp;gt;&lt;/code&gt;. Crawlers won't follow the div. Your internal linking graph — one of the biggest on-page ranking signals — silently disappears.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Use real anchor tags. Frameworks like Next.js (&lt;code&gt;&amp;lt;Link&amp;gt;&lt;/code&gt;) and Remix already render proper &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; elements; just make sure your team uses them instead of programmatic navigation everywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Lazy-loaded images without dimensions
&lt;/h2&gt;

&lt;p&gt;Two problems here. First, missing &lt;code&gt;width&lt;/code&gt; and &lt;code&gt;height&lt;/code&gt; cause Cumulative Layout Shift, which is a Core Web Vitals ranking factor. Second, &lt;code&gt;loading="lazy"&lt;/code&gt; on the above-the-fold hero image delays LCP and tanks your Performance score.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Hero image: load eagerly with explicit dimensions --&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="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;alt=&lt;/span&gt;&lt;span class="s"&gt;"..."&lt;/span&gt; &lt;span class="na"&gt;fetchpriority=&lt;/span&gt;&lt;span class="s"&gt;"high"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- Below-the-fold images: lazy load with dimensions --&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;"/feature.jpg"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"600"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"400"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"..."&lt;/span&gt; &lt;span class="na"&gt;loading=&lt;/span&gt;&lt;span class="s"&gt;"lazy"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. hreflang and canonical tags injected after page load
&lt;/h2&gt;

&lt;p&gt;For multilingual sites — especially ones serving both Spanish and English markets in Latin America — &lt;code&gt;hreflang&lt;/code&gt; and &lt;code&gt;canonical&lt;/code&gt; tags must be in the initial HTML. If they're added by a client-side library after hydration, Google often misses them, leading to wrong-language pages ranking in the wrong market.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Render these in your document &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; server-side. In Next.js, use &lt;code&gt;next/head&lt;/code&gt; (Pages Router) or &lt;code&gt;generateMetadata&lt;/code&gt; (App Router):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateMetadata&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;alternates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;canonical&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`https://example.com/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="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="na"&gt;languages&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;es-MX&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`https://example.com/es/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en-US&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`https://example.com/en/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="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="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How to find these on your own site
&lt;/h2&gt;

&lt;p&gt;A quick triage:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the page in Chrome, disable JavaScript (DevTools → Command Menu → "Disable JavaScript"), reload. If your H1 and primary content disappear, you have problem #1.&lt;/li&gt;
&lt;li&gt;Run a crawl with Screaming Frog in JavaScript rendering mode and compare to the non-JS crawl. Big delta = you have a problem.&lt;/li&gt;
&lt;li&gt;Check Google Search Console → Pages → "Crawled — currently not indexed." If dynamic routes show up there, look at #2.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;I write more about technical SEO, paid media, and analytics over at &lt;a href="https://mhaconsulting.mx" rel="noopener noreferrer"&gt;MHA Consulting&lt;/a&gt; — we're a digital marketing consultancy based in Mexico City helping companies fix issues like these.&lt;/p&gt;

&lt;p&gt;If you've run into a JS SEO problem I didn't cover here, drop it in the comments — happy to dig in.&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
