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.

Ilustración que muestra el renderizado del lado del servidor y la generación estática con Nuxt 3 y 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.

Requisitos previos

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.

nuxt.config.tstypescript
// 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.

vue
<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.

vue
<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.

server/plugins/render-hooks.tstypescript
// 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'
    }
  })
})
Rendimiento del SSR

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.

nuxt.config.tstypescript
// 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.

server/utils/generate-routes.tstypescript
// 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.

nuxt.config.tstypescript
// 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.

server/api/articles/[slug].get.tstypescript
// 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.

server/api/articles/[slug].put.tstypescript
// 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
})
Caché distribuido

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.

vue
<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.

vue
<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.

nuxt.config.tstypescript
// 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.

bash
# 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

#nuxt 3
#vue.js
#ssr
#generación estática
#rendimiento web

Compartir

Artículos relacionados