Nuxt 3: SSR e generazione statica, la guida completa

Padroneggiare SSR e generazione statica con Nuxt 3. Da useFetch alle route rules, scoprire come ottimizzare le prestazioni delle applicazioni Vue.js.

Illustrazione che mostra il rendering lato server e la generazione statica con Nuxt 3 e Vue.js

Nuxt 3 trasforma il modo in cui vengono costruite le applicazioni Vue.js, offrendo molteplici modalità di rendering adatte a casi d'uso diversi. Dal Server-Side Rendering (SSR) alla generazione statica fino al rendering ibrido, il framework offre una flessibilità notevole per ottimizzare prestazioni e SEO.

Prerequisiti

Questo tutorial presuppone una conoscenza di base di Vue 3 e della Composition API. La familiarità con i concetti di rendering lato server è utile ma non obbligatoria, poiché i fondamentali vengono spiegati lungo tutta la guida.

Comprendere le modalità di rendering di Nuxt 3

Prima di immergersi nel codice, è essenziale comprendere le differenze tra le modalità di rendering disponibili. Ogni modalità risponde a esigenze specifiche in termini di prestazioni, SEO ed esperienza utente.

Il SSR (Server-Side Rendering) genera l'HTML sul server a ogni richiesta. La generazione statica (SSG) pre-genera tutte le pagine al momento del build. La modalità ibrida consente di combinare questi approcci pagina per pagina.

nuxt.config.tstypescript
// Configurazione delle diverse modalità di rendering
export default defineNuxtConfig({
  // SSR attivo per default (consigliato per la SEO)
  ssr: true,

  // Generazione statica: pre-renderizza tutte le pagine
  // Usare 'npm run generate' per il build
  // target: 'static', // Sintassi Nuxt 2

  // Modalità ibrida: configurabile per rotta
  routeRules: {
    // Home page: pre-renderizzata e in cache
    '/': { prerender: true },
    // Blog: generazione statica
    '/blog/**': { prerender: true },
    // Dashboard: rendering solo lato client
    '/dashboard/**': { ssr: false },
    // API: nessun pre-rendering
    '/api/**': { prerender: false }
  }
})

Questa configurazione mostra la potenza della modalità ibrida: ogni sezione dell'applicazione utilizza la modalità di rendering più adatta alle proprie esigenze.

Recupero dei dati con useFetch e useAsyncData

Nuxt 3 fornisce due composable principali per recuperare i dati in modo isomorfo. Questi composable funzionano sia lato server sia lato client, con una gestione automatica dell'idratazione.

useFetch è un wrapper attorno a useAsyncData che semplifica le chiamate HTTP. useAsyncData offre più controllo per casi d'uso avanzati.

vue
<script setup lang="ts">
// pages/blog/[slug].vue
// Pagina di dettaglio dell'articolo con useFetch

// Ottenere il parametro della rotta
const route = useRoute()

// useFetch: recupero automatico dei dati
// I dati vengono recuperati lato server e idratati sul client
const { data: article, pending, error } = await useFetch(
  `/api/articles/${route.params.slug}`,
  {
    // Chiave univoca per cache e deduplicazione
    key: `article-${route.params.slug}`,
    // Trasformare i dati se necessario
    transform: (response) => response.data,
    // Opzioni di cache
    getCachedData: (key) => {
      // Verificare se i dati sono in cache
      const nuxtApp = useNuxtApp()
      return nuxtApp.payload.data[key]
    }
  }
)

// Gestione degli errori con navigazione
if (error.value) {
  throw createError({
    statusCode: 404,
    message: 'Articolo non trovato'
  })
}
</script>

<template>
  <div>
    <div v-if="pending" class="loading">
      Caricamento articolo...
    </div>
    <article v-else-if="article">
      <h1>{{ article.title }}</h1>
      <div v-html="article.content" />
    </article>
  </div>
</template>

Per i casi che richiedono più controllo, useAsyncData consente di eseguire qualsiasi funzione asincrona.

vue
<script setup lang="ts">
// pages/products/index.vue
// Lista prodotti con useAsyncData e filtri

const route = useRoute()

// useAsyncData: controllo completo sulla logica di fetching
const { data: products, refresh } = await useAsyncData(
  'products-list',
  async () => {
    // Recuperare da più sorgenti se necessario
    const [productsResponse, categoriesResponse] = await Promise.all([
      $fetch('/api/products', {
        query: {
          category: route.query.category,
          sort: route.query.sort || 'date'
        }
      }),
      $fetch('/api/categories')
    ])

    // Combinare e trasformare i dati
    return {
      products: productsResponse.data,
      categories: categoriesResponse.data,
      total: productsResponse.meta.total
    }
  },
  {
    // Aggiornare quando cambiano i query params
    watch: [() => route.query]
  }
)

// Funzione di refresh manuale
const updateFilters = async (newCategory: string) => {
  await navigateTo({
    query: { ...route.query, category: newCategory }
  })
}
</script>

Questi composable evitano il doppio fetching: i dati recuperati sul server vengono serializzati nel payload HTML e riutilizzati durante l'idratazione lato client.

Personalizzare l'SSR con i server hooks

L'SSR di Nuxt 3 può essere personalizzato tramite i server hooks. Questi hooks consentono di intervenire in diverse fasi del ciclo di rendering per modificare il comportamento di default.

server/plugins/render-hooks.tstypescript
// Plugin di server per personalizzare il rendering SSR

export default defineNitroPlugin((nitroApp) => {
  // Hook eseguito prima del rendering di ogni pagina
  nitroApp.hooks.hook('render:html', (html, { event }) => {
    // Iniettare script o metadati
    html.head.push(`
      <script>
        // Analytics o configurazione globale
        window.__APP_CONFIG__ = {
          environment: '${process.env.NODE_ENV}',
          apiUrl: '${process.env.API_URL}'
        }
      </script>
    `)
  })

  // Hook per la gestione della cache di rendering
  nitroApp.hooks.hook('render:response', (response, { event }) => {
    // Aggiungere header di cache personalizzati
    const path = event.path

    if (path.startsWith('/blog/')) {
      // Cache lunga per gli articoli del blog
      response.headers['Cache-Control'] = 'public, max-age=3600, s-maxage=86400'
    } else if (path.startsWith('/api/')) {
      // Nessuna cache per le API
      response.headers['Cache-Control'] = 'no-store'
    }
  })
})
Prestazioni dell'SSR

L'hook render:response è ideale per implementare strategie di cache HTTP. Combinare l'SSR con un CDN che rispetti gli header Cache-Control consente di servire pagine pre-renderizzate mantenendo la possibilità di invalidarle.

Generazione statica con nuxt generate

La generazione statica costruisce in anticipo tutte le pagine al momento del build. Questo approccio è ideale per siti con contenuto stabile come blog, documentazione o siti di marketing.

Per le rotte dinamiche, Nuxt deve conoscere tutti gli URL da generare. L'hook prerender:routes consente di definire queste rotte in modo programmatico.

nuxt.config.tstypescript
// Configurazione completa per la generazione statica

export default defineNuxtConfig({
  // Attivare la generazione statica
  nitro: {
    prerender: {
      // Attivare il crawling automatico dei link
      crawlLinks: true,
      // Rotte da includere sempre
      routes: ['/', '/about', '/contact'],
      // Ignorare alcune rotte
      ignore: ['/admin', '/api']
    }
  },

  hooks: {
    // Hook per generare rotte dinamiche
    async 'prerender:routes'(ctx) {
      // Recuperare gli articoli da API o DB
      const articles = await fetch('https://api.example.com/articles')
        .then(res => res.json())

      // Aggiungere le rotte degli articoli
      for (const article of articles) {
        ctx.routes.add(`/blog/${article.slug}`)
      }

      // Recuperare le categorie
      const categories = await fetch('https://api.example.com/categories')
        .then(res => res.json())

      for (const category of categories) {
        ctx.routes.add(`/category/${category.slug}`)
      }
    }
  }
})

Per progetti con molte pagine, il crawler automatico può risultare insufficiente. Ecco un approccio più robusto con un file di configurazione separato.

server/utils/generate-routes.tstypescript
// Utility per generare la lista delle rotte dinamiche

import { prisma } from './prisma'

export async function getAllStaticRoutes(): Promise<string[]> {
  const routes: string[] = []

  // Articoli 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}`)
  }

  // Pagine prodotto
  const products = await prisma.product.findMany({
    where: { active: true },
    select: { slug: true }
  })

  for (const product of products) {
    routes.push(`/products/${product.slug}`)
  }

  // Pagine tag
  const tags = await prisma.tag.findMany({
    select: { slug: true }
  })

  for (const tag of tags) {
    routes.push(`/tags/${tag.slug}`)
  }

  return routes
}

Pronto a superare i tuoi colloqui su Vue.js / Nuxt.js?

Pratica con i nostri simulatori interattivi, flashcards e test tecnici.

Rendering ibrido con routeRules

Il rendering ibrido è la funzione di punta di Nuxt 3. Consente di definire regole di rendering diverse per ciascuna rotta, combinando il meglio di SSR e SSG.

nuxt.config.tstypescript
// Configurazione avanzata del rendering ibrido

export default defineNuxtConfig({
  routeRules: {
    // Pagine marketing: pre-renderizzate e in cache a lungo termine
    '/': { prerender: true },
    '/pricing': { prerender: true },
    '/features/**': { prerender: true },

    // Blog: ISR (Incremental Static Regeneration)
    // Revalidazione ogni ora
    '/blog/**': {
      isr: 3600,
      prerender: true
    },

    // Documentazione: cache CDN con revalidazione
    '/docs/**': {
      swr: 86400, // Stale-while-revalidate
      prerender: true
    },

    // E-commerce: SSR con cache breve
    '/products/**': {
      ssr: true,
      cache: {
        maxAge: 60,
        staleMaxAge: 300
      }
    },

    // Carrello e checkout: solo lato client
    '/cart': { ssr: false },
    '/checkout/**': { ssr: false },

    // Dashboard: modalità SPA
    '/dashboard/**': {
      ssr: false,
      // Disattivare il pre-rendering
      prerender: false
    },

    // Rotte API: nessuna cache di default
    '/api/**': {
      cors: true,
      headers: {
        'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE'
      }
    }
  }
})

Questa configurazione illustra un'architettura tipica delle applicazioni moderne: le pagine pubbliche sono ottimizzate per la SEO con SSG, mentre le sezioni interattive utilizzano il rendering lato client.

Ottimizzazione delle prestazioni con la cache dei dati

Oltre alla cache delle pagine, Nuxt 3 consente di mettere in cache i dati recuperati. Questa strategia riduce il carico sulle API e migliora i tempi di risposta.

server/api/articles/[slug].get.tstypescript
// Endpoint API con cache dei dati

import { getArticleBySlug } from '~/server/utils/articles'

export default defineCachedEventHandler(
  async (event) => {
    const slug = getRouterParam(event, 'slug')

    if (!slug) {
      throw createError({
        statusCode: 400,
        message: 'Slug mancante'
      })
    }

    const article = await getArticleBySlug(slug)

    if (!article) {
      throw createError({
        statusCode: 404,
        message: 'Articolo non trovato'
      })
    }

    return article
  },
  {
    // Chiave di cache basata sullo slug
    getKey: (event) => `article-${getRouterParam(event, 'slug')}`,
    // Durata della cache: 1 ora
    maxAge: 3600,
    // Stale-while-revalidate: servire la cache obsoleta durante l'aggiornamento
    staleMaxAge: 7200,
    // Invalidazione basata su tag
    tags: ['articles']
  }
)

Per invalidare la cache quando i contenuti cambiano, Nuxt fornisce un sistema di tag.

server/api/articles/[slug].put.tstypescript
// Aggiornamento articolo con invalidazione della cache

import { updateArticle } from '~/server/utils/articles'

export default defineEventHandler(async (event) => {
  const slug = getRouterParam(event, 'slug')
  const body = await readBody(event)

  // Aggiornare l'articolo
  const article = await updateArticle(slug, body)

  // Invalidare la cache per questo articolo
  await useStorage('cache').removeItem(`nitro:handlers:article-${slug}`)

  // Oppure invalidazione basata sui tag (tutti gli articoli)
  // await useStorage('cache').clear('articles')

  return article
})
Cache distribuita

In produzione con più istanze, la cache in memoria è insufficiente. Si consiglia di configurare Redis o un altro sistema distribuito tramite la configurazione di Nitro per garantire la coerenza tra le istanze.

Gestione di SEO e metadati

L'SSR consente di ottimizzare la SEO generando i metadati lato server. Nuxt 3 propone diversi approcci per gestire i meta tag in modo dinamico.

vue
<script setup lang="ts">
// pages/blog/[slug].vue
// Pagina blog con SEO ottimizzata

const route = useRoute()

const { data: article } = await useFetch(`/api/articles/${route.params.slug}`)

// Configurazione SEO dinamica basata sull'articolo
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
})

// Dati strutturati per 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>

Per le pagine statiche, i metadati possono essere definiti direttamente nel componente.

vue
<script setup lang="ts">
// pages/about.vue
// Pagina statica con SEO

definePageMeta({
  title: 'Chi siamo'
})

useSeoMeta({
  title: 'Chi siamo SharpSkill | Preparazione ai colloqui tecnici',
  description: 'Scoprire SharpSkill, la piattaforma di preparazione ai colloqui tecnici. La missione: aiutare gli sviluppatori ad avere successo nei colloqui tecnici.',
  ogTitle: 'Chi siamo SharpSkill',
  ogDescription: 'La piattaforma di preparazione ai colloqui tecnici',
  ogImage: '/images/og-about.webp'
})
</script>

Deploy e considerazioni di produzione

La scelta del deploy dipende dalla modalità di rendering utilizzata. Ecco le opzioni principali e le loro configurazioni.

nuxt.config.tstypescript
// Configurazione per diversi ambienti di deploy

export default defineNuxtConfig({
  nitro: {
    // Preset in base alla piattaforma di destinazione
    // preset: 'vercel', // Vercel
    // preset: 'netlify', // Netlify
    // preset: 'cloudflare-pages', // Cloudflare
    // preset: 'node-server', // Node.js classico

    // Configurazione per Node.js in produzione
    preset: 'node-server',

    // Compressione delle risposte
    compressPublicAssets: true,

    // Configurazione dello storage di cache
    storage: {
      cache: {
        driver: 'redis',
        url: process.env.REDIS_URL
      }
    }
  },

  // Variabili d'ambiente runtime
  runtimeConfig: {
    // Segreti (non esposti al client)
    apiSecret: process.env.API_SECRET,
    // Configurazione pubblica
    public: {
      apiBase: process.env.NUXT_PUBLIC_API_BASE || '/api'
    }
  }
})

Per il deploy statico, il comando npm run generate crea una cartella .output/public pronta per essere distribuita su qualsiasi host di file statici.

bash
# Generazione statica
npm run generate

# Il contenuto di .output/public può essere distribuito su:
# - Vercel (rilevamento automatico)
# - Netlify (configurazione automatica)
# - GitHub Pages
# - S3 + CloudFront
# - Qualsiasi CDN o server di file statici

Inizia a praticare!

Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.

Conclusione

Nuxt 3 offre una flessibilità eccezionale per il rendering delle applicazioni Vue.js. La scelta tra SSR, SSG e rendering ibrido dipende dalle esigenze specifiche di ciascun progetto.

Punti chiave:

SSR: ideale per contenuti dinamici che richiedono buona SEO (e-commerce, siti di notizie)

SSG: perfetto per contenuti stabili (blog, documentazione, siti di marketing)

Ibrido: l'approccio migliore per applicazioni complesse con esigenze variegate

useFetch/useAsyncData: idratazione automatica e gestione della cache

routeRules: configurazione granulare per il comportamento di ogni rotta

Caching: molteplici strategie per ottimizzare le prestazioni in produzione

Combinare il rendering ibrido con una strategia di cache ben pensata consente di costruire applicazioni performanti e ottimizzate per la SEO, mantenendo l'interattività delle Single Page Applications.

Tag

#nuxt 3
#vue.js
#ssr
#generazione statica
#performance web

Condividi

Articoli correlati