Nuxt 3: SSR y generación estática, la guía completa
Domine el SSR y la generación estática con Nuxt 3. Desde useFetch hasta route rules, aprenda a optimizar el rendimiento de sus aplicaciones Vue.js.

Nuxt 3 transforma la manera en que se construyen las aplicaciones Vue.js al ofrecer múltiples modos de renderizado adaptados a diferentes casos de uso. Desde el Server-Side Rendering (SSR) hasta la generación estática y el renderizado híbrido, el framework brinda una flexibilidad notable para optimizar el rendimiento y el SEO.
Este tutorial asume conocimientos básicos de Vue 3 y la Composition API. La familiaridad con los conceptos de renderizado del lado del servidor resulta útil pero no es obligatoria, ya que los fundamentos se explican a lo largo de la guía.
Comprender los modos de renderizado de Nuxt 3
Antes de adentrarse en el código, resulta esencial entender las diferencias entre los modos de renderizado disponibles. Cada modo responde a necesidades específicas en términos de rendimiento, SEO y experiencia de usuario.
El SSR (Server-Side Rendering) genera el HTML en el servidor para cada solicitud. La generación estática (SSG) pre-genera todas las páginas en el momento del build. El modo híbrido permite combinar ambos enfoques página por página.
// Configuración de los diferentes modos de renderizado
export default defineNuxtConfig({
// SSR activado por defecto (recomendado para SEO)
ssr: true,
// Generación estática: pre-renderiza todas las páginas
// Usar 'npm run generate' para construir
// target: 'static', // Sintaxis Nuxt 2
// Modo híbrido: configurable por ruta
routeRules: {
// Página de inicio: pre-renderizada y cacheada
'/': { prerender: true },
// Blog: generación estática
'/blog/**': { prerender: true },
// Dashboard: renderizado solo en cliente
'/dashboard/**': { ssr: false },
// API: sin pre-renderizado
'/api/**': { prerender: false }
}
})Esta configuración demuestra el poder del modo híbrido: cada sección de la aplicación utiliza el modo de renderizado más adecuado a sus necesidades.
Obtención de datos con useFetch y useAsyncData
Nuxt 3 ofrece dos composables principales para obtener datos de forma isomorfa. Estos composables funcionan tanto en el servidor como en el cliente, con una gestión automática de la hidratación.
useFetch es un wrapper alrededor de useAsyncData que simplifica las llamadas HTTP. useAsyncData ofrece más control para casos de uso avanzados.
<script setup lang="ts">
// pages/blog/[slug].vue
// Página de detalle del artículo con useFetch
// Obtener parámetro de la ruta
const route = useRoute()
// useFetch: obtención automática de datos
// Los datos se obtienen en el servidor y se hidratan en el cliente
const { data: article, pending, error } = await useFetch(
`/api/articles/${route.params.slug}`,
{
// Clave única para caché y deduplicación
key: `article-${route.params.slug}`,
// Transformar datos si es necesario
transform: (response) => response.data,
// Opciones de caché
getCachedData: (key) => {
// Verificar si los datos están en caché
const nuxtApp = useNuxtApp()
return nuxtApp.payload.data[key]
}
}
)
// Manejo de errores con navegación
if (error.value) {
throw createError({
statusCode: 404,
message: 'Artículo no encontrado'
})
}
</script>
<template>
<div>
<div v-if="pending" class="loading">
Cargando artículo...
</div>
<article v-else-if="article">
<h1>{{ article.title }}</h1>
<div v-html="article.content" />
</article>
</div>
</template>Para los casos que requieren más control, useAsyncData permite ejecutar cualquier función asíncrona.
<script setup lang="ts">
// pages/products/index.vue
// Lista de productos con useAsyncData y filtros
const route = useRoute()
// useAsyncData: control total sobre la lógica de fetching
const { data: products, refresh } = await useAsyncData(
'products-list',
async () => {
// Obtener desde múltiples fuentes si es necesario
const [productsResponse, categoriesResponse] = await Promise.all([
$fetch('/api/products', {
query: {
category: route.query.category,
sort: route.query.sort || 'date'
}
}),
$fetch('/api/categories')
])
// Combinar y transformar los datos
return {
products: productsResponse.data,
categories: categoriesResponse.data,
total: productsResponse.meta.total
}
},
{
// Refrescar cuando cambien los query params
watch: [() => route.query]
}
)
// Función de actualización manual
const updateFilters = async (newCategory: string) => {
await navigateTo({
query: { ...route.query, category: newCategory }
})
}
</script>Estos composables evitan el doble fetching: los datos obtenidos en el servidor se serializan en el payload HTML y se reutilizan durante la hidratación del cliente.
Personalizar el SSR con server hooks
El SSR de Nuxt 3 puede personalizarse mediante server hooks. Estos hooks permiten intervenir en distintas etapas del ciclo de renderizado para modificar el comportamiento por defecto.
// Plugin de servidor para personalizar el renderizado SSR
export default defineNitroPlugin((nitroApp) => {
// Hook ejecutado antes de renderizar cada página
nitroApp.hooks.hook('render:html', (html, { event }) => {
// Inyectar scripts o metadatos
html.head.push(`
<script>
// Analítica o configuración global
window.__APP_CONFIG__ = {
environment: '${process.env.NODE_ENV}',
apiUrl: '${process.env.API_URL}'
}
</script>
`)
})
// Hook para gestión del caché de renderizado
nitroApp.hooks.hook('render:response', (response, { event }) => {
// Añadir cabeceras de caché personalizadas
const path = event.path
if (path.startsWith('/blog/')) {
// Caché largo para artículos del blog
response.headers['Cache-Control'] = 'public, max-age=3600, s-maxage=86400'
} else if (path.startsWith('/api/')) {
// Sin caché para APIs
response.headers['Cache-Control'] = 'no-store'
}
})
})El hook render:response resulta ideal para implementar estrategias de caché HTTP. Combinar SSR con un CDN que respete las cabeceras Cache-Control permite servir páginas pre-renderizadas manteniendo la capacidad de invalidarlas.
Generación estática con nuxt generate
La generación estática pre-construye todas las páginas en el momento del build. Este enfoque resulta ideal para sitios con contenido estable como blogs, documentación o sitios de marketing.
Para las rutas dinámicas, Nuxt necesita conocer todas las URLs a generar. El hook prerender:routes permite definir estas rutas de forma programática.
// Configuración completa para generación estática
export default defineNuxtConfig({
// Activar generación estática
nitro: {
prerender: {
// Activar el rastreo automático de enlaces
crawlLinks: true,
// Rutas que siempre deben incluirse
routes: ['/', '/about', '/contact'],
// Ignorar ciertas rutas
ignore: ['/admin', '/api']
}
},
hooks: {
// Hook para generar rutas dinámicas
async 'prerender:routes'(ctx) {
// Obtener artículos desde API o BD
const articles = await fetch('https://api.example.com/articles')
.then(res => res.json())
// Añadir rutas de artículos
for (const article of articles) {
ctx.routes.add(`/blog/${article.slug}`)
}
// Obtener categorías
const categories = await fetch('https://api.example.com/categories')
.then(res => res.json())
for (const category of categories) {
ctx.routes.add(`/category/${category.slug}`)
}
}
}
})Para proyectos con muchas páginas, el crawler automático puede resultar insuficiente. He aquí un enfoque más robusto con un archivo de configuración separado.
// Utilidad para generar la lista de rutas dinámicas
import { prisma } from './prisma'
export async function getAllStaticRoutes(): Promise<string[]> {
const routes: string[] = []
// Artículos del blog
const articles = await prisma.article.findMany({
where: { published: true },
select: { slug: true, category: { select: { slug: true } } }
})
for (const article of articles) {
routes.push(`/blog/${article.category.slug}/${article.slug}`)
}
// Páginas de productos
const products = await prisma.product.findMany({
where: { active: true },
select: { slug: true }
})
for (const product of products) {
routes.push(`/products/${product.slug}`)
}
// Páginas de etiquetas
const tags = await prisma.tag.findMany({
select: { slug: true }
})
for (const tag of tags) {
routes.push(`/tags/${tag.slug}`)
}
return routes
}¿Listo para aprobar tus entrevistas de Vue.js / Nuxt.js?
Practica con nuestros simuladores interactivos, flashcards y tests técnicos.
Renderizado híbrido con routeRules
El renderizado híbrido es la característica estrella de Nuxt 3. Permite definir reglas de renderizado distintas por ruta, combinando lo mejor del SSR y del SSG.
// Configuración avanzada de renderizado híbrido
export default defineNuxtConfig({
routeRules: {
// Páginas de marketing: pre-renderizadas y cacheadas a largo plazo
'/': { prerender: true },
'/pricing': { prerender: true },
'/features/**': { prerender: true },
// Blog: ISR (Incremental Static Regeneration)
// Revalidación cada hora
'/blog/**': {
isr: 3600,
prerender: true
},
// Documentación: caché CDN con revalidación
'/docs/**': {
swr: 86400, // Stale-while-revalidate
prerender: true
},
// E-commerce: SSR con caché corto
'/products/**': {
ssr: true,
cache: {
maxAge: 60,
staleMaxAge: 300
}
},
// Carrito y checkout: solo en cliente
'/cart': { ssr: false },
'/checkout/**': { ssr: false },
// Dashboard: modo SPA
'/dashboard/**': {
ssr: false,
// Desactivar pre-renderizado
prerender: false
},
// Rutas API: sin caché por defecto
'/api/**': {
cors: true,
headers: {
'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE'
}
}
}
})Esta configuración ilustra una arquitectura típica de aplicación moderna: las páginas públicas se optimizan para SEO con SSG, mientras que las secciones interactivas utilizan renderizado en el cliente.
Optimización del rendimiento con caché de datos
Más allá del caché de páginas, Nuxt 3 permite cachear los datos obtenidos. Esta estrategia reduce la carga sobre las APIs y mejora los tiempos de respuesta.
// Endpoint API con caché de datos
import { getArticleBySlug } from '~/server/utils/articles'
export default defineCachedEventHandler(
async (event) => {
const slug = getRouterParam(event, 'slug')
if (!slug) {
throw createError({
statusCode: 400,
message: 'Slug ausente'
})
}
const article = await getArticleBySlug(slug)
if (!article) {
throw createError({
statusCode: 404,
message: 'Artículo no encontrado'
})
}
return article
},
{
// Clave de caché basada en el slug
getKey: (event) => `article-${getRouterParam(event, 'slug')}`,
// Duración del caché: 1 hora
maxAge: 3600,
// Stale-while-revalidate: servir caché obsoleto durante la actualización
staleMaxAge: 7200,
// Invalidación basada en tags
tags: ['articles']
}
)Para invalidar el caché cuando cambia el contenido, Nuxt proporciona un sistema de tags.
// Actualización de artículo con invalidación de caché
import { updateArticle } from '~/server/utils/articles'
export default defineEventHandler(async (event) => {
const slug = getRouterParam(event, 'slug')
const body = await readBody(event)
// Actualizar el artículo
const article = await updateArticle(slug, body)
// Invalidar el caché para este artículo
await useStorage('cache').removeItem(`nitro:handlers:article-${slug}`)
// O invalidación basada en tags (todos los artículos)
// await useStorage('cache').clear('articles')
return article
})En producción con múltiples instancias, el caché en memoria resulta insuficiente. Se recomienda configurar Redis u otro sistema distribuido mediante la configuración de Nitro para garantizar la coherencia entre instancias.
Gestión del SEO y metadatos
El SSR permite optimizar el SEO al generar los metadatos en el servidor. Nuxt 3 ofrece varios enfoques para gestionar las meta tags de forma dinámica.
<script setup lang="ts">
// pages/blog/[slug].vue
// Página de blog con SEO optimizado
const route = useRoute()
const { data: article } = await useFetch(`/api/articles/${route.params.slug}`)
// Configuración SEO dinámica basada en el artículo
useSeoMeta({
title: article.value?.title,
description: article.value?.excerpt,
ogTitle: article.value?.title,
ogDescription: article.value?.excerpt,
ogImage: article.value?.coverImage,
ogType: 'article',
twitterCard: 'summary_large_image',
twitterTitle: article.value?.title,
twitterDescription: article.value?.excerpt,
twitterImage: article.value?.coverImage
})
// Datos estructurados para Google
useHead({
script: [
{
type: 'application/ld+json',
innerHTML: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'Article',
headline: article.value?.title,
description: article.value?.excerpt,
image: article.value?.coverImage,
datePublished: article.value?.publishedAt,
dateModified: article.value?.updatedAt,
author: {
'@type': 'Organization',
name: 'SharpSkill'
}
})
}
]
})
</script>Para las páginas estáticas, los metadatos pueden definirse directamente en el componente.
<script setup lang="ts">
// pages/about.vue
// Página estática con SEO
definePageMeta({
title: 'Acerca de'
})
useSeoMeta({
title: 'Acerca de SharpSkill | Preparación para entrevistas técnicas',
description: 'Descubra SharpSkill, la plataforma de preparación para entrevistas técnicas. Su misión: ayudar a los desarrolladores a triunfar en sus entrevistas técnicas.',
ogTitle: 'Acerca de SharpSkill',
ogDescription: 'La plataforma de preparación para entrevistas técnicas',
ogImage: '/images/og-about.webp'
})
</script>Despliegue y consideraciones de producción
La elección del despliegue depende del modo de renderizado utilizado. He aquí las principales opciones y sus configuraciones.
// Configuración para distintos entornos de despliegue
export default defineNuxtConfig({
nitro: {
// Preset según la plataforma objetivo
// preset: 'vercel', // Vercel
// preset: 'netlify', // Netlify
// preset: 'cloudflare-pages', // Cloudflare
// preset: 'node-server', // Node.js clásico
// Configuración para Node.js en producción
preset: 'node-server',
// Compresión de respuestas
compressPublicAssets: true,
// Configuración del almacenamiento de caché
storage: {
cache: {
driver: 'redis',
url: process.env.REDIS_URL
}
}
},
// Variables de entorno en runtime
runtimeConfig: {
// Secretos (no expuestos al cliente)
apiSecret: process.env.API_SECRET,
// Configuración pública
public: {
apiBase: process.env.NUXT_PUBLIC_API_BASE || '/api'
}
}
})Para el despliegue estático, el comando npm run generate crea una carpeta .output/public lista para desplegar en cualquier host de archivos estáticos.
# Generación estática
npm run generate
# El contenido de .output/public puede desplegarse en:
# - Vercel (detección automática)
# - Netlify (configuración automática)
# - GitHub Pages
# - S3 + CloudFront
# - Cualquier CDN o servidor de archivos estáticos¡Empieza a practicar!
Pon a prueba tu conocimiento con nuestros simuladores de entrevista y tests técnicos.
Conclusión
Nuxt 3 ofrece una flexibilidad excepcional para renderizar aplicaciones Vue.js. La elección entre SSR, SSG y renderizado híbrido depende de las necesidades específicas de cada proyecto.
Puntos clave:
✅ SSR: ideal para contenido dinámico que requiere buen SEO (e-commerce, sitios de noticias)
✅ SSG: perfecto para contenido estable (blogs, documentación, sitios de marketing)
✅ Híbrido: el mejor enfoque para aplicaciones complejas con necesidades variadas
✅ useFetch/useAsyncData: hidratación automática y gestión de caché
✅ routeRules: configuración granular para el comportamiento de cada ruta
✅ Caché: múltiples estrategias para optimizar el rendimiento en producción
Combinar el renderizado híbrido con una estrategia de caché bien pensada permite construir aplicaciones de alto rendimiento optimizadas para SEO, manteniendo la interactividad de las Single Page Applications.
Etiquetas
Compartir
Artículos relacionados

Preguntas esenciales de Vue.js: 25 preguntas para conseguir el puesto
Prepara tu entrevista de Vue.js con estas 25 preguntas esenciales. Desde la reactividad hasta los composables, domina los conceptos clave para tu próxima entrevista.

Vue 3 Pinia vs Vuex: Gestión de Estado Moderna y Preguntas de Entrevista 2026
Comparación detallada entre Pinia y Vuex: diseño de API, soporte TypeScript, Composition API, rendimiento, estrategias de migración y preguntas frecuentes en entrevistas Vue para 2026.

Composition API de Vue 3: Guía completa para dominar la reactividad
Aprende a utilizar la Composition API de Vue 3 con esta guía práctica. Domina ref, reactive, computed, watch y composables para construir aplicaciones Vue de alto rendimiento.