SEOLab
MENÚ

Reparación, mantención y desarrollo de sitios web · La Florida

JavaScript SEO: Soluciones a 15 Problemas Comunes 2026

Avanzado · 15 min de lectura · 2026-04

Resuelve los problemas más comunes de JavaScript SEO: renderizado, CSR vs SSR, hydration, crawling issues con React, Vue, Angular y Next.js en 2026.

El JavaScript moderno ha transformado el desarrollo web, pero sigue siendo el dolor de cabeza número uno para SEOs técnicos. A pesar de que Googlebot ejecuta JavaScript desde 2015 y usa una versión evergreen de Chrome desde 2019, los sitios basados en frameworks como React, Vue, Angular y Next.js enfrentan desafíos únicos de indexación, rendering y performance.

En 2026, el panorama es más complejo que nunca: tenemos Client-Side Rendering (CSR), Server-Side Rendering (SSR), Static Site Generation (SSG), Incremental Static Regeneration (ISR), y híbridos de todo lo anterior. Cada approach tiene implicaciones profundas para SEO, y elegir mal puede significar la diferencia entre un sitio que rankea y uno invisible para Google.

Esta guía cubre los 15 problemas más comunes de JavaScript SEO que veo consistentemente en auditorías de 2026, con soluciones prácticas y ejemplos de código real. Si trabajas con frameworks modernos o tu sitio depende de JavaScript para renderizar contenido, esta guía es esencial.

El Estado de JavaScript SEO en 2026

Primero, aclaremos qué puede y no puede hacer Googlebot en 2026:

✓ Googlebot PUEDE:

  • Ejecutar JavaScript moderno (ES2023+)
  • Renderizar React, Vue, Angular, Svelte, etc.
  • Ejecutar async/await, Promises, fetch API
  • Procesar la mayoría de polyfills y transpilaciones
  • Scrollear páginas (limitado) para lazy loading
  • Ejecutar IntersectionObserver

✗ Googlebot NO PUEDE (o tiene dificultades):

  • Esperar indefinidamente (timeout ~5 segundos)
  • Ejecutar infinite scrolls perfectamente
  • Interactuar con modals que requieren clicks
  • Ejecutar Service Workers de forma confiable
  • Garantizar rendering de contenido que depende de eventos de usuario
  • Procesar WebSockets en tiempo real

Realidad 2026: Aunque Googlebot ejecuta JavaScript, el rendering tiene costos: delay de indexación (render queue), mayor uso de crawl budget, y potencial de errores. Si puedes servir contenido sin JavaScript, hazlo.

Problema #1: Contenido Invisible en el HTML Inicial (CSR Puro)

El Problema

Aplicaciones React/Vue puras con Client-Side Rendering sirven HTML casi vacío:

<!DOCTYPE html>
<html>
<head>
  <title>Mi App</title>
</head>
<body>
  <div id="root"></div>
  <script src="bundle.js"></script>
</body>
</html>

Todo el contenido se genera con JavaScript después de cargar. Googlebot debe esperar rendering, causando:

  • Delay de indexación de días/semanas
  • Riesgo de timeout si rendering tarda >5s
  • Imposibilidad de preview en social media

La Solución: SSR o SSG

Opción A: Server-Side Rendering (Next.js)

// pages/producto/[id].js
export async function getServerSideProps(context) {
  const { id } = context.params;
  const producto = await fetch(`https://api.ejemplo.com/productos/${id}`).then(r => r.json());

  return {
    props: { producto }
  };
}

export default function ProductoPage({ producto }) {
  return (
    <div>
      <h1>{producto.nombre}</h1>
      <p>{producto.descripcion}</p>
      <span>${producto.precio}</span>
    </div>
  );
}

El HTML servido ya contiene el contenido completo.

Opción B: Static Site Generation

// Para contenido que cambia poco
export async function getStaticProps() {
  const posts = await fetch('https://api.ejemplo.com/posts').then(r => r.json());

  return {
    props: { posts },
    revalidate: 3600 // ISR: regenerar cada hora
  };
}

Cuándo Usar Qué

Método Caso de Uso SEO Score
CSR Apps detrás de login, dashboards
SSR Contenido dinámico, personalizado
SSG Blogs, docs, landing pages
ISR Ecommerce, contenido que cambia moderadamente

Problema #2: Hydration Errors Causando Contenido Mismatch

El Problema

El contenido renderizado en servidor no coincide con el client:

// Servidor renderiza:
<div>Última actualización: 2026-04-13 10:30</div>

// Cliente hidrata con:
<div>Última actualización: 2026-04-13 14:45</div>

React/Vue tiran warning, pero peor: Google puede ver una versión y los usuarios otra.

La Solución: useEffect para Contenido Client-Only

import { useState, useEffect } from 'react';

function Timestamp() {
  const [timestamp, setTimestamp] = useState(null);

  useEffect(() => {
    // Solo ejecuta en cliente
    setTimestamp(new Date().toLocaleString());
  }, []);

  // Durante SSR, renderiza placeholder
  if (!timestamp) {
    return <div>Cargando...</div>;
  }

  return <div>Última actualización: {timestamp}</div>;
}

O mejor aún, usa suppressHydrationWarning:

<time suppressHydrationWarning>
  {new Date().toLocaleString()}
</time>

Problema #3: Soft Navigation y URL Changes Sin Actualizar HTML

El Problema

SPAs usan History API para cambiar URLs sin recargar:

// URL cambia de /productos a /productos/zapatillas
history.pushState({}, '', '/productos/zapatillas');

Pero el <title>, meta tags y canonical no se actualizan, causando:

  • Títulos incorrectos en SERPs
  • Meta descriptions que no coinciden
  • Canonical tags apuntando a la página anterior

La Solución: Actualizar Metadata en Cada Navegación

Con React Helmet (React tradicional):

import { Helmet } from 'react-helmet';

function ProductoPage({ producto }) {
  return (
    <>
      <Helmet>
        <title>{producto.nombre} - Mi Tienda</title>
        <meta name="description" content={producto.descripcionCorta} />
        <link rel="canonical" href={`https://ejemplo.com/productos/${producto.id}`} />
        <meta property="og:title" content={producto.nombre} />
        <meta property="og:image" content={producto.imagen} />
      </Helmet>

      <div>
        <h1>{producto.nombre}</h1>
        {/* contenido */}
      </div>
    </>
  );
}

Con Next.js Head:

import Head from 'next/head';

export default function ProductoPage({ producto }) {
  return (
    <>
      <Head>
        <title>{producto.nombre} - Mi Tienda</title>
        <meta name="description" content={producto.descripcionCorta} />
        <link rel="canonical" href={`https://ejemplo.com/productos/${producto.id}`} />
      </Head>

      <h1>{producto.nombre}</h1>
    </>
  );
}

Problema #4: Links No Rastreables (onClick en lugar de href)

El Problema

Botones o divs con onClick en lugar de <a> tags:

// ❌ Googlebot no puede seguir esto
<div onClick={() => navigate('/productos')}>
  Ver Productos
</div>

<button onClick={() => window.location = '/contacto'}>
  Contactar
</button>

Googlebot no ejecuta clicks. No descubrirá esas páginas.

La Solución: Siempre Usa Tags

// ✅ Googlebot puede rastrear
import Link from 'next/link'; // o react-router Link

<Link href="/productos">
  <a>Ver Productos</a>
</Link>

// ✅ O <a> nativo
<a href="/contacto">Contactar</a>

Si necesitas JavaScript para funcionalidad extra:

<a
  href="/productos"
  onClick={(e) => {
    // Tu lógica custom
    trackEvent('click', 'productos');
    // Deja que el link funcione normalmente
  }}
>
  Ver Productos
</a>

Problema #5: Infinite Scroll Sin Paginación

El Problema

Contenido que solo carga con scroll infinito:

window.addEventListener('scroll', () => {
  if (window.scrollY > threshold) {
    loadMoreProducts();
  }
});

Googlebot tiene capacidad limitada de scroll. Mucho contenido nunca será indexado.

La Solución: Hybrid Approach con Paginación

// URL structure:
// /productos (página 1)
// /productos?page=2
// /productos?page=3

function ProductosPage({ productos, page, totalPages }) {
  const [items, setItems] = useState(productos);
  const [currentPage, setCurrentPage] = useState(page);

  // Infinite scroll para UX
  const loadMore = async () => {
    const nextPage = currentPage + 1;
    const newItems = await fetch(`/api/productos?page=${nextPage}`).then(r => r.json());
    setItems([...items, ...newItems]);
    setCurrentPage(nextPage);

    // Actualiza URL sin recargar
    history.pushState({}, '', `/productos?page=${nextPage}`);
  };

  return (
    <div>
      {items.map(item => <ProductCard key={item.id} {...item} />)}

      {/* Infinite scroll para usuarios */}
      <IntersectionObserver onIntersect={loadMore} />

      {/* Paginación tradicional para bots */}
      <nav aria-label="Paginación de productos">
        {currentPage > 1 && (
          <a href={`/productos?page=${currentPage - 1}`} rel="prev">
            Anterior
          </a>
        )}

        {currentPage < totalPages && (
          <a href={`/productos?page=${currentPage + 1}`} rel="next">
            Siguiente
          </a>
        )}
      </nav>
    </div>
  );
}

Implementa rel="prev" y rel="next" para que Google entienda la serie.

Problema #6: Contenido Detrás de Tabs/Accordions No Indexado

El Problema

Contenido en tabs o accordions que usa display: none:

<div className={activeTab !== 'specs' ? 'hidden' : ''}>
  {/* Especificaciones técnicas importantes */}
</div>

Si el CSS usa display: none, Google históricamente lo ignoraba. En 2026 es más tolerante, pero hay riesgo.

La Solución: Usa aria-hidden en lugar de display: none

/* En lugar de display: none */
.tab-content[aria-hidden="true"] {
  position: absolute;
  left: -10000px;
  top: auto;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

O mejor: renderiza todo, oculta visualmente:

function ProductTabs({ specs, reviews }) {
  const [activeTab, setActiveTab] = useState('specs');

  return (
    <div>
      {/* Tabs navigation */}
      <div role="tablist">
        <button onClick={() => setActiveTab('specs')} aria-selected={activeTab === 'specs'}>
          Especificaciones
        </button>
        <button onClick={() => setActiveTab('reviews')} aria-selected={activeTab === 'reviews'}>
          Reviews
        </button>
      </div>

      {/* Todo el contenido está en el DOM */}
      <div role="tabpanel" className={activeTab !== 'specs' ? 'visually-hidden' : ''}>
        <h2>Especificaciones</h2>
        {specs}
      </div>

      <div role="tabpanel" className={activeTab !== 'reviews' ? 'visually-hidden' : ''}>
        <h2>Reviews</h2>
        {reviews}
      </div>
    </div>
  );
}

Problema #7: Fetch Calls Fallando en Server-Side Rendering

El Problema

Código que funciona en cliente pero falla en servidor:

// ❌ window no existe en servidor
const apiUrl = window.location.origin + '/api/productos';

// ❌ localStorage no existe en servidor
const userId = localStorage.getItem('userId');

La Solución: Detectar Environment

// Utility helper
export const isServer = typeof window === 'undefined';
export const isClient = !isServer;

// En tu componente/página
function ProductoPage() {
  const [user, setUser] = useState(null);

  useEffect(() => {
    // Solo ejecuta en cliente
    if (isClient) {
      const userId = localStorage.getItem('userId');
      fetchUser(userId).then(setUser);
    }
  }, []);

  // SSR renderiza sin user data
  // CSR hidrata y carga user data
}

// Para fetch durante SSR
export async function getServerSideProps(context) {
  // Usa absolute URL
  const protocol = context.req.headers['x-forwarded-proto'] || 'http';
  const host = context.req.headers.host;
  const apiUrl = `${protocol}://${host}/api/productos`;

  const productos = await fetch(apiUrl).then(r => r.json());

  return { props: { productos } };
}

Problema #8: Canonical Tags Dinámicos Incorrectos

El Problema

URLs con parámetros que comparten contenido:

/productos?sort=precio
/productos?sort=popularidad
/productos?filter=nike

Cada una genera canonical diferente, causando contenido duplicado.

La Solución: Canonical Consistente

import { useRouter } from 'next/router';
import Head from 'next/head';

function ProductosPage() {
  const router = useRouter();

  // Canonical siempre apunta a URL base sin parámetros
  const canonicalUrl = `https://ejemplo.com${router.pathname}`;

  return (
    <>
      <Head>
        <link rel="canonical" href={canonicalUrl} />
      </Head>

      {/* contenido */}
    </>
  );
}

O si los parámetros SÍ crean páginas únicas:

const { sort, filter } = router.query;

// Construye canonical incluyendo parámetros relevantes
const params = new URLSearchParams();
if (filter) params.set('filter', filter);

const canonicalUrl = `https://ejemplo.com/productos${params.toString() ? '?' + params.toString() : ''}`;

Problema #9: Redirects Client-Side No Reconocidos

El Problema

Redirects con JavaScript puro:

// ❌ Googlebot puede no seguir esto
useEffect(() => {
  if (!user) {
    window.location = '/login';
  }
}, [user]);

La Solución: Redirects Server-Side cuando sea posible

Next.js getServerSideProps:

export async function getServerSideProps(context) {
  const { req } = context;
  const user = await getUserFromCookie(req);

  if (!user) {
    return {
      redirect: {
        destination: '/login',
        permanent: false // 302
      }
    };
  }

  return { props: { user } };
}

Next.js Middleware (recommended en 2026):

// middleware.js
import { NextResponse } from 'next/server';

export function middleware(request) {
  const user = request.cookies.get('user');

  if (!user && request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url));
  }
}

Problema #10: Structured Data Generado con JavaScript No Validado

El Problema

Schema JSON-LD inyectado con JavaScript tiene errores:

// Fecha mal formateada, rating fuera de rango, etc.
const schema = {
  "@type": "Product",
  "aggregateRating": {
    "ratingValue": "6", // ❌ Fuera de rango 1-5
  },
  "offers": {
    "priceValidUntil": "2026/04/30" // ❌ Formato incorrecto
  }
};

La Solución: Validación + Testing Automatizado

// schemas/product.js
export function generateProductSchema(producto) {
  // Validación de datos
  const rating = Math.max(0, Math.min(5, producto.rating)); // Clamp 0-5

  const schema = {
    "@context": "https://schema.org",
    "@type": "Product",
    "name": producto.nombre,
    "image": producto.imagenes,
    "description": producto.descripcion,
    "sku": producto.sku,
    "offers": {
      "@type": "Offer",
      "price": producto.precio.toFixed(2),
      "priceCurrency": "CLP",
      "priceValidUntil": producto.precioValidoHasta.toISOString().split('T')[0], // YYYY-MM-DD
      "availability": producto.enStock ? "https://schema.org/InStock" : "https://schema.org/OutOfStock"
    }
  };

  // Solo añade rating si hay reviews
  if (producto.numeroReviews > 0) {
    schema.aggregateRating = {
      "@type": "AggregateRating",
      "ratingValue": rating.toString(),
      "reviewCount": producto.numeroReviews.toString()
    };
  }

  return schema;
}

// En componente
function ProductoPage({ producto }) {
  const schema = generateProductSchema(producto);

  return (
    <>
      <Head>
        <script
          type="application/ld+json"
          dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
        />
      </Head>
      {/* contenido */}
    </>
  );
}

Valida con Google Rich Results Test en staging antes de deploy.

Problema #11: Estado de Loading Sin Contenido Para Googlebot

El Problema

Mostrar solo spinner mientras carga:

function ProductoPage() {
  const [producto, setProducto] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetchProducto().then(data => {
      setProducto(data);
      setLoading(false);
    });
  }, []);

  if (loading) {
    return <Spinner />; // ❌ Googlebot puede ver solo esto
  }

  return <div>{producto.nombre}</div>;
}

Si el fetch tarda >5s, Googlebot ve solo spinner.

La Solución: SSR/SSG con Datos Pre-Loaded

// Next.js
export async function getServerSideProps({ params }) {
  const producto = await fetchProducto(params.id);

  return {
    props: { producto }
  };
}

function ProductoPage({ producto }) {
  // Producto ya viene del servidor, no hay loading state
  return (
    <div>
      <h1>{producto.nombre}</h1>
      <p>{producto.descripcion}</p>
    </div>
  );
}

Problema #12: Errores 404 Manejados Solo con JavaScript

El Problema

Página de error que retorna 200 status code:

// ❌ Retorna HTTP 200
function ProductoPage({ producto }) {
  if (!producto) {
    return <div>Producto no encontrado</div>;
  }

  return <div>{producto.nombre}</div>;
}

Google indexa como página válida con contenido "Producto no encontrado".

La Solución: Status Code Correcto desde Servidor

// Next.js
export async function getServerSideProps({ params, res }) {
  const producto = await fetchProducto(params.id);

  if (!producto) {
    res.statusCode = 404;
    return {
      props: { error: '404' }
    };
  }

  return { props: { producto } };
}

function ProductoPage({ producto, error }) {
  if (error === '404') {
    return <Error404 />; // Componente custom de 404
  }

  return <div>{producto.nombre}</div>;
}

Para CSR puro (no recomendado pero si no hay opción):

useEffect(() => {
  if (!producto) {
    // Al menos indica a crawlers
    document.querySelector('meta[name="robots"]')?.setAttribute('content', 'noindex');
  }
}, [producto]);

Problema #13: JavaScript Bundle Demasiado Grande

El Problema

Bundle de 2MB+ bloquea rendering:

// 500KB de dependencies innecesarias
import _ from 'lodash'; // Toda la librería cuando solo usas 2 funciones
import moment from 'moment'; // 300KB cuando solo necesitas formatear fechas

Googlebot tiene timeout. Si rendering tarda >5s, puede indexar parcialmente.

La Solución: Code Splitting + Tree Shaking

Importaciones específicas:

// ❌ Importa toda lodash (500KB)
import _ from 'lodash';
const uniq = _.uniq;

// ✅ Importa solo función necesaria (5KB)
import uniq from 'lodash/uniq';

Dynamic imports para rutas:

// Next.js automático por página
// Cada página es bundle separado

// Para componentes grandes
const Chart = dynamic(() => import('../components/Chart'), {
  loading: () => <p>Cargando gráfico...</p>,
  ssr: false // No renderizar en servidor si es muy pesado
});

Análisis de bundle:

# Next.js
npm run build -- --analyze

# React (con webpack-bundle-analyzer)
npm run build
npx webpack-bundle-analyzer build/bundle-stats.json

Objetivo: FCP <1.8s, LCP <2.5s incluso con JavaScript.

Problema #14: Fetch con Relative URLs Fallando en SSR

El Problema

// ❌ Falla en server (no hay window.location)
const data = await fetch('/api/productos');

La Solución: Absolute URLs en Server

// utils/fetch.js
export async function apiFetch(endpoint) {
  const baseUrl = typeof window === 'undefined'
    ? process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'
    : '';

  const url = `${baseUrl}${endpoint}`;
  const response = await fetch(url);

  if (!response.ok) {
    throw new Error(`API error: ${response.status}`);
  }

  return response.json();
}

// Uso
const productos = await apiFetch('/api/productos');

Problema #15: Meta Tags Sociales (Open Graph) No Dinámicos

El Problema

Open Graph estático en index.html:

<!-- ❌ Mismo OG para todas las páginas -->
<meta property="og:title" content="Mi Sitio">
<meta property="og:image" content="/logo.png">

Shares en redes sociales muestran info genérica.

La Solución: OG Dinámico Por Página

// Next.js con next-seo
import { NextSeo } from 'next-seo';

function ProductoPage({ producto }) {
  const seoConfig = {
    title: producto.nombre,
    description: producto.descripcionCorta,
    canonical: `https://ejemplo.com/productos/${producto.slug}`,
    openGraph: {
      type: 'product',
      url: `https://ejemplo.com/productos/${producto.slug}`,
      title: producto.nombre,
      description: producto.descripcionCorta,
      images: [
        {
          url: producto.imagenPrincipal,
          width: 1200,
          height: 630,
          alt: producto.nombre
        }
      ],
      site_name: 'Mi Tienda'
    },
    twitter: {
      handle: '@mitienda',
      cardType: 'summary_large_image'
    }
  };

  return (
    <>
      <NextSeo {...seoConfig} />
      <div>{/* contenido */}</div>
    </>
  );
}

Framework-Specific Best Practices

Next.js (Recomendado para SEO en 2026)

// Excelente por defecto para SEO
✅ SSR out-of-the-box
✅ Automatic code splitting
✅ Image optimization
✅ Built-in <Head> component
✅ Middleware para redirects
✅ ISR (Incremental Static Regeneration)

// Best practices
- Usa getServerSideProps para contenido dinámico
- Usa getStaticProps + ISR para contenido semi-estático
- Implementa next-sitemap para sitemaps automáticos
- Usa next/image para optimización de imágenes

React SPA (Create React App, Vite)

// Requiere trabajo extra para SEO
⚠️ CSR por defecto
⚠️ Requiere setup de pre-rendering o SSR
⚠️ Meta tags dinámicos requieren React Helmet

// Soluciones
- Usa prerender.io o Rendertron para pre-rendering
- Migra a Next.js si SEO es prioridad
- Implementa React Helmet Async
- Considera React Server Components

Vue.js (Nuxt)

// Nuxt es equivalente a Next.js para Vue
✅ SSR nativo
✅ Automatic routing
✅ vue-meta para SEO tags

// Best practices
- Usa asyncData para fetch pre-rendering
- Implementa nuxt-sitemap
- Usa nuxt/image para optimización

Angular Universal

// Angular con SSR
✅ Angular Universal para SSR
⚠️ Setup más complejo que Next/Nuxt

// Best practices
- Implementa TransferState para evitar doble fetch
- Usa Meta y Title services
- Pre-rendering con angular-prerender

Testing Checklist Para JavaScript SEO

Tools Esenciales

  1. Google URL Inspection Tool

    • Valida rendering real de Googlebot
    • Ver HTML renderizado
    • Screenshot de cómo ve Google
  2. Chrome DevTools

    • Disable JavaScript para ver content sin JS
    • Network tab para ver fetch timing
    • Coverage para bundle analysis
  3. Screaming Frog (Rendering mode)

    • Crawl con JavaScript rendering habilitado
    • Compara contenido renderizado vs no-renderizado
  4. Lighthouse

    • Performance metrics (LCP, FCP, TBT)
    • SEO audit específico

Testing Protocol

# 1. Build production
npm run build

# 2. Ejecuta Lighthouse
lighthouse https://tu-sitio.com --view

# 3. Verifica rendering en URL Inspection (Search Console)

# 4. Compara HTML fuente vs renderizado
curl https://tu-sitio.com > source.html
# Usa URL Inspection "View Crawled Page" > HTML para ver versión renderizada

# 5. Valida structured data
# Google Rich Results Test con cada tipo de página

Conclusión

JavaScript SEO en 2026 es complejo pero manejable si sigues los principios fundamentales: servir contenido en el HTML inicial cuando sea posible (SSR/SSG), asegurar que links sean rastreables, actualizar metadata en navegaciones, y mantener bundles optimizados.

Los 15 problemas cubiertos en esta guía representan el 90% de issues que veo en auditorías reales. La mayoría son evitables usando frameworks modernos como Next.js o Nuxt con configuración adecuada, y siguiendo las mejores prácticas de cada framework.

El futuro apunta a más pre-rendering y menos dependencia de client-side JavaScript para contenido crítico. Google prefiere contenido en HTML estático, y aunque puede ejecutar JavaScript, siempre habrá un costo en tiempo de indexación y crawl budget.

Próximo paso: Audita tu sitio con Google URL Inspection Tool. Compara el HTML fuente con el renderizado. Si hay diferencias significativas, tienes oportunidades de optimización. Prioriza SSR/SSG para páginas de landing importantes y asegura que todo el contenido crítico esté en el HTML inicial.

Problemas concretos que arreglo

WhatsAppCorreo