Renderizado del lado del servidor para SPAs: Solución definitiva para la indexación en motores de búsqueda
Los rastreadores como Googlebot y YandexBot suelen encontrar HTML vacío al explorar SPAs basadas en renderizado del lado del cliente (CSR). Sin renderizado del lado del servidor (SSR), estos rastreadores no esperan a que se ejecute JavaScript y abandonan las páginas sin indexarlas. El SSR resuelve este problema generando HTML completamente renderizado en el servidor antes de enviarlo al navegador. Esto acelera el FCP, el LCP y el TTFB, mejorando los Core Web Vitals y la visibilidad orgánica.
Comparativa de estrategias de renderizado
| Parámetro | CSR | SSR | Pre-renderizado | SSG |
|----------|-----|-----|------------------|-----|
| Ubicación del renderizado | Navegador | Servidor (por cada petición) | Preconstruido para URLs específicas | Preconstruido para todas las páginas |
| FCP/LCP | Lento | Rápido | Rápido | Muy rápido |
| SEO | Deficiente | Excelente | Bueno | Excelente |
| Actualización de datos | En tiempo real | En tiempo real | Obsoleto | Obsoleto |
| Carga en el servidor | Mínima | Alta | Baja | Mínima |
El SSR funciona mejor en páginas dinámicas: catálogos de productos, feeds de noticias o paneles de control. El pre-renderizado es ideal para URLs con actualizaciones poco frecuentes (p. ej., páginas de marketing). El SSG es la opción perfecta para contenido estático como documentación o páginas de destino.
React + Next.js: Implementación paso a paso
Crea un nuevo proyecto:
npx create-next-app@latest mi-app-ssr
cd mi-app-ssr
npm run dev
Para páginas dinámicas, usa getServerSideProps:
// pages/producto/[id].jsx
export async function getServerSideProps(context) {
const { id } = context.params;
const res = await fetch(`https://api.ejemplo.com/productos/${id}`);
const producto = await res.json();
if (!res.ok) {
return { notFound: true };
}
return {
props: { producto },
};
}
export default function PaginaProducto({ producto }) {
return (
<main>
<h1>{producto.nombre}</h1>
<p>{producto.descripcion}</p>
</main>
);
}
Agrega etiquetas meta optimizadas para SEO:
import Head from 'next/head';
export default function PaginaProducto({ producto }) {
return (
<>
<Head>
<title>{producto.nombre} | Tienda</title>
<meta name="description" content={producto.descripcionCorta} />
<meta property="og:title" content={producto.nombre} />
<meta property="og:image" content={producto.urlImagen} />
</Head>
<main>
<h1>{producto.nombre}</h1>
</main>
</>
);
}
Despliegue: Vercel, Netlify o Node.js con PM2.
Hidratación sin errores
La hidratación es el proceso mediante el cual React del lado del cliente «revive» el HTML renderizado en el servidor. Para evitar discrepancias:
- Mantén sincronizados los datos entre servidor y cliente.
- Verifica el entorno de ejecución antes de usar
windowodocument. - Supervisa las advertencias en la consola del navegador.
const esNavegador = typeof window !== 'undefined';
Vue + Nuxt.js: Renderizado del lado del servidor
Inicia un proyecto:
npx nuxi@latest init mi-app-nuxt
cd mi-app-nuxt
npm install
npm run dev
Obtén datos del lado del servidor:
<!-- pages/producto/[id].vue -->
<script setup>
const ruta = useRoute();
const { data: producto, error } = await useFetch(
`/api/productos/${ruta.params.id}`
);
if (error.value) {
throw createError({ statusCode: 404, message: 'No encontrado' });
}
useHead({
title: producto.value?.nombre,
meta: [
{ name: 'description', content: producto.value?.descripcionCorta },
],
});
</script>
<template>
<main>
<h1>{{ producto?.nombre }}</h1>
<p>{{ producto?.descripcion }}</p>
</main>
</template>
Configura nuxt.config.ts:
export default defineNuxtConfig({
ssr: true,
app: {
head: {
htmlAttrs: { lang: 'es' },
link: [{ rel: 'canonical', href: 'https://ejemplo.com' }],
},
},
nitro: {
preset: 'node-server',
},
});
Angular Universal: Integración SSR
Para Angular 17+:
ng add @angular/ssr
Define el endpoint del servidor en server.ts:
import { APP_BASE_HREF } from '@angular/common';
import { CommonEngine } from '@angular/ssr';
import express from 'express';
// ... otros imports
const servidor = express();
// ...
servidor.get('*', (req, res, next) => {
commonEngine
.render({
bootstrap: AppServerModule,
documentFilePath: join(browserDistFolder, 'index.html'),
url: req.originalUrl,
publicPath: browserDistFolder,
providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }],
})
.then((html) => res.send(html))
.catch(next);
});
Construye y sirve:
npm run build:ssr
npm run serve:ssr
Detección segura de plataforma:
import { isPlatformBrowser } from '@angular/common';
import { inject, PLATFORM_ID } from '@angular/core';
const platformId = inject(PLATFORM_ID);
if (isPlatformBrowser(platformId)) {
console.log(window.location.href);
}
Integración con CMS desacoplado
Para Next.js + Contentful GraphQL:
export async function getServerSideProps({ params }) {
const consulta = `
query ObtenerArticulo($slug: String!) {
articleCollection(where: { slug: $slug }) {
items {
titulo
cuerpo
seo {
titulo
descripcion
}
}
}
}
`;
const res = await fetch(
`https://graphql.contentful.com/content/v1/spaces/${process.env.CONTENTFUL_SPACE_ID}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.CONTENTFUL_TOKEN}`,
},
body: JSON.stringify({ consulta, variables: { slug: params.slug } }),
}
);
const { data } = await res.json();
// ...
}
Conclusiones clave
- El SSR entrega HTML completamente renderizado a los rastreadores, resolviendo el principal problema de indexación de las aplicaciones CSR.
- Next.js, Nuxt.js y Angular Universal son frameworks probados en producción para SSR.
- Las discrepancias durante la hidratación provocan re-renderizados completos: valida siempre el entorno de ejecución.
- Los Core Web Vitals (FCP, LCP, TTFB) mejoran entre un 50 % y un 80 % tras migrar a SSR.
- Usa caché Redis para mantener el TTFB por debajo de 200 ms incluso durante picos de tráfico.
— Editorial Team
Aún no hay comentarios.