DEV Community

Moises Hamui
Moises Hamui

Posted on

5 JavaScript SEO Pitfalls That Are Quietly Killing Your Rankings

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.

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.

1. Critical content rendered only on the client

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 useEffect runs, they may be missed entirely or indexed late.

Fix: Use server-side rendering (SSR) or static generation for any content that needs to rank. In Next.js, that means getStaticProps or getServerSideProps over pure client-side fetching for above-the-fold content.

// Bad: title only appears after hydration
export default function Post() {
  const [post, setPost] = useState(null);
  useEffect(() => { fetch('/api/post').then(r => r.json()).then(setPost); }, []);
  return <h1>{post?.title}</h1>;
}

// Good: title is in the initial HTML
export async function getStaticProps() {
  const post = await fetchPost();
  return { props: { post } };
}
Enter fullscreen mode Exit fullscreen mode

2. Soft 404s on dynamic routes

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.

Fix: Return a real 404 status from your server or framework. In Next.js: return { notFound: true } from getStaticProps. In a custom Express setup: res.status(404).

3. Internal links that aren't actually links

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

Fix: Use real anchor tags. Frameworks like Next.js (<Link>) and Remix already render proper <a> elements; just make sure your team uses them instead of programmatic navigation everywhere.

4. Lazy-loaded images without dimensions

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

Fix:

<!-- Hero image: load eagerly with explicit dimensions -->
<img src="/hero.jpg" width="1200" height="600" alt="..." fetchpriority="high" />

<!-- Below-the-fold images: lazy load with dimensions -->
<img src="/feature.jpg" width="600" height="400" alt="..." loading="lazy" />
Enter fullscreen mode Exit fullscreen mode

5. hreflang and canonical tags injected after page load

For multilingual sites — especially ones serving both Spanish and English markets in Latin America — hreflang and canonical 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.

Fix: Render these in your document <head> server-side. In Next.js, use next/head (Pages Router) or generateMetadata (App Router):

export async function generateMetadata({ params }) {
  return {
    alternates: {
      canonical: `https://example.com/${params.slug}`,
      languages: {
        'es-MX': `https://example.com/es/${params.slug}`,
        'en-US': `https://example.com/en/${params.slug}`,
      },
    },
  };
}
Enter fullscreen mode Exit fullscreen mode

How to find these on your own site

A quick triage:

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

I write more about technical SEO, paid media, and analytics over at MHA Consulting — we're a digital marketing consultancy based in Mexico City helping companies fix issues like these.

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

Top comments (0)