Nuxt 3: SSR i generowanie statyczne, kompletny przewodnik

Opanowanie SSR i generowania statycznego w Nuxt 3. Od useFetch po route rules: jak optymalizować wydajność aplikacji Vue.js.

Ilustracja przedstawiająca renderowanie po stronie serwera i generowanie statyczne w Nuxt 3 i Vue.js

Nuxt 3 zmienia sposób tworzenia aplikacji Vue.js, oferując wiele trybów renderowania dostosowanych do różnych przypadków użycia. Od Server-Side Rendering (SSR) przez generowanie statyczne aż po renderowanie hybrydowe framework zapewnia wyjątkową elastyczność w optymalizacji wydajności i SEO.

Wymagania wstępne

Ten tutorial zakłada podstawową znajomość Vue 3 i Composition API. Znajomość koncepcji renderowania po stronie serwera jest pomocna, lecz nie wymagana, ponieważ podstawy są wyjaśniane w trakcie przewodnika.

Zrozumienie trybów renderowania w Nuxt 3

Zanim zacznie się pisać kod, warto zrozumieć różnice między dostępnymi trybami renderowania. Każdy tryb odpowiada na konkretne potrzeby związane z wydajnością, SEO i doświadczeniem użytkownika.

SSR (Server-Side Rendering) generuje HTML na serwerze przy każdym żądaniu. Generowanie statyczne (SSG) wstępnie generuje wszystkie strony w czasie buildu. Tryb hybrydowy pozwala łączyć te podejścia strona po stronie.

nuxt.config.tstypescript
// Konfiguracja różnych trybów renderowania
export default defineNuxtConfig({
  // SSR włączone domyślnie (zalecane dla SEO)
  ssr: true,

  // Generowanie statyczne: pre-renderuje wszystkie strony
  // Użyj 'npm run generate' do zbudowania
  // target: 'static', // Składnia Nuxt 2

  // Tryb hybrydowy: konfigurowalny per trasa
  routeRules: {
    // Strona główna: pre-renderowana i cache'owana
    '/': { prerender: true },
    // Blog: generowanie statyczne
    '/blog/**': { prerender: true },
    // Dashboard: renderowanie tylko po stronie klienta
    '/dashboard/**': { ssr: false },
    // API: bez pre-renderowania
    '/api/**': { prerender: false }
  }
})

Ta konfiguracja pokazuje moc trybu hybrydowego: każda sekcja aplikacji korzysta z trybu renderowania najlepiej dopasowanego do jej potrzeb.

Pobieranie danych z useFetch i useAsyncData

Nuxt 3 udostępnia dwa główne composable do izomorficznego pobierania danych. Działają one zarówno po stronie serwera, jak i klienta, z automatycznym zarządzaniem hydracją.

useFetch to wrapper wokół useAsyncData, który upraszcza wywołania HTTP. useAsyncData daje większą kontrolę w zaawansowanych scenariuszach.

vue
<script setup lang="ts">
// pages/blog/[slug].vue
// Strona szczegółów artykułu z useFetch

// Pobranie parametru trasy
const route = useRoute()

// useFetch: automatyczne pobieranie danych
// Dane są pobierane na serwerze, a następnie hydratowane na kliencie
const { data: article, pending, error } = await useFetch(
  `/api/articles/${route.params.slug}`,
  {
    // Unikalny klucz dla cache i deduplikacji
    key: `article-${route.params.slug}`,
    // Transformacja danych w razie potrzeby
    transform: (response) => response.data,
    // Opcje cache
    getCachedData: (key) => {
      // Sprawdzenie, czy dane są w cache
      const nuxtApp = useNuxtApp()
      return nuxtApp.payload.data[key]
    }
  }
)

// Obsługa błędów z nawigacją
if (error.value) {
  throw createError({
    statusCode: 404,
    message: 'Artykuł nie znaleziony'
  })
}
</script>

<template>
  <div>
    <div v-if="pending" class="loading">
      Ładowanie artykułu...
    </div>
    <article v-else-if="article">
      <h1>{{ article.title }}</h1>
      <div v-html="article.content" />
    </article>
  </div>
</template>

W przypadkach wymagających większej kontroli useAsyncData pozwala wykonać dowolną funkcję asynchroniczną.

vue
<script setup lang="ts">
// pages/products/index.vue
// Lista produktów z useAsyncData i filtrami

const route = useRoute()

// useAsyncData: pełna kontrola nad logiką pobierania
const { data: products, refresh } = await useAsyncData(
  'products-list',
  async () => {
    // Pobranie z wielu źródeł w razie potrzeby
    const [productsResponse, categoriesResponse] = await Promise.all([
      $fetch('/api/products', {
        query: {
          category: route.query.category,
          sort: route.query.sort || 'date'
        }
      }),
      $fetch('/api/categories')
    ])

    // Łączenie i transformacja danych
    return {
      products: productsResponse.data,
      categories: categoriesResponse.data,
      total: productsResponse.meta.total
    }
  },
  {
    // Odświeżanie przy zmianie query params
    watch: [() => route.query]
  }
)

// Funkcja ręcznego odświeżania
const updateFilters = async (newCategory: string) => {
  await navigateTo({
    query: { ...route.query, category: newCategory }
  })
}
</script>

Te composable zapobiegają podwójnemu pobieraniu: dane uzyskane na serwerze są serializowane w payloadzie HTML i ponownie wykorzystywane podczas hydracji po stronie klienta.

Personalizacja SSR za pomocą server hooks

SSR w Nuxt 3 można dostosować za pomocą server hooks. Hooki te pozwalają ingerować w różne etapy cyklu renderowania, aby modyfikować domyślne zachowanie.

server/plugins/render-hooks.tstypescript
// Plugin serwera do dostosowywania renderowania SSR

export default defineNitroPlugin((nitroApp) => {
  // Hook wykonywany przed renderowaniem każdej strony
  nitroApp.hooks.hook('render:html', (html, { event }) => {
    // Wstrzyknięcie skryptów lub metadanych
    html.head.push(`
      <script>
        // Analityka lub konfiguracja globalna
        window.__APP_CONFIG__ = {
          environment: '${process.env.NODE_ENV}',
          apiUrl: '${process.env.API_URL}'
        }
      </script>
    `)
  })

  // Hook do zarządzania cache renderowania
  nitroApp.hooks.hook('render:response', (response, { event }) => {
    // Dodanie niestandardowych nagłówków cache
    const path = event.path

    if (path.startsWith('/blog/')) {
      // Długi cache dla artykułów blogowych
      response.headers['Cache-Control'] = 'public, max-age=3600, s-maxage=86400'
    } else if (path.startsWith('/api/')) {
      // Brak cache dla API
      response.headers['Cache-Control'] = 'no-store'
    }
  })
})
Wydajność SSR

Hook render:response doskonale nadaje się do wdrażania strategii cache HTTP. Połączenie SSR z CDN, który respektuje nagłówki Cache-Control, pozwala serwować pre-renderowane strony zachowując możliwość ich unieważnienia.

Generowanie statyczne z nuxt generate

Generowanie statyczne buduje wszystkie strony z wyprzedzeniem w czasie buildu. Takie podejście jest idealne dla witryn ze stabilną treścią, takich jak blogi, dokumentacja czy strony marketingowe.

W przypadku tras dynamicznych Nuxt musi znać wszystkie URL-e do wygenerowania. Hook prerender:routes pozwala definiować te trasy programowo.

nuxt.config.tstypescript
// Pełna konfiguracja generowania statycznego

export default defineNuxtConfig({
  // Włączenie generowania statycznego
  nitro: {
    prerender: {
      // Włączenie automatycznego crawlowania linków
      crawlLinks: true,
      // Trasy zawsze do uwzględnienia
      routes: ['/', '/about', '/contact'],
      // Ignorowanie wybranych tras
      ignore: ['/admin', '/api']
    }
  },

  hooks: {
    // Hook do generowania tras dynamicznych
    async 'prerender:routes'(ctx) {
      // Pobranie artykułów z API lub bazy
      const articles = await fetch('https://api.example.com/articles')
        .then(res => res.json())

      // Dodanie tras artykułów
      for (const article of articles) {
        ctx.routes.add(`/blog/${article.slug}`)
      }

      // Pobranie kategorii
      const categories = await fetch('https://api.example.com/categories')
        .then(res => res.json())

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

W projektach z dużą liczbą stron automatyczny crawler może okazać się niewystarczający. Oto bardziej solidne podejście z osobnym plikiem konfiguracyjnym.

server/utils/generate-routes.tstypescript
// Narzędzie do generowania listy tras dynamicznych

import { prisma } from './prisma'

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

  // Artykuły blogowe
  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}`)
  }

  // Strony produktów
  const products = await prisma.product.findMany({
    where: { active: true },
    select: { slug: true }
  })

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

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

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

  return routes
}

Gotowy na rozmowy o Vue.js / Nuxt.js?

Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.

Renderowanie hybrydowe z routeRules

Renderowanie hybrydowe jest sztandarową funkcją Nuxt 3. Pozwala definiować różne reguły renderowania dla każdej trasy, łącząc to, co najlepsze z SSR i SSG.

nuxt.config.tstypescript
// Zaawansowana konfiguracja renderowania hybrydowego

export default defineNuxtConfig({
  routeRules: {
    // Strony marketingowe: pre-renderowane i długo cache'owane
    '/': { prerender: true },
    '/pricing': { prerender: true },
    '/features/**': { prerender: true },

    // Blog: ISR (Incremental Static Regeneration)
    // Rewalidacja co godzinę
    '/blog/**': {
      isr: 3600,
      prerender: true
    },

    // Dokumentacja: cache CDN z rewalidacją
    '/docs/**': {
      swr: 86400, // Stale-while-revalidate
      prerender: true
    },

    // E-commerce: SSR z krótkim cache
    '/products/**': {
      ssr: true,
      cache: {
        maxAge: 60,
        staleMaxAge: 300
      }
    },

    // Koszyk i checkout: tylko po stronie klienta
    '/cart': { ssr: false },
    '/checkout/**': { ssr: false },

    // Dashboard: tryb SPA
    '/dashboard/**': {
      ssr: false,
      // Wyłączenie pre-renderowania
      prerender: false
    },

    // Trasy API: brak cache domyślnie
    '/api/**': {
      cors: true,
      headers: {
        'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE'
      }
    }
  }
})

Ta konfiguracja ilustruje typową architekturę współczesnej aplikacji: strony publiczne są optymalizowane pod SEO za pomocą SSG, podczas gdy interaktywne sekcje używają renderowania po stronie klienta.

Optymalizacja wydajności z cache danych

Poza cache stron Nuxt 3 pozwala cache'ować pobrane dane. Strategia ta zmniejsza obciążenie API i poprawia czasy odpowiedzi.

server/api/articles/[slug].get.tstypescript
// Endpoint API z cache danych

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

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

    if (!slug) {
      throw createError({
        statusCode: 400,
        message: 'Brak slugu'
      })
    }

    const article = await getArticleBySlug(slug)

    if (!article) {
      throw createError({
        statusCode: 404,
        message: 'Artykuł nie znaleziony'
      })
    }

    return article
  },
  {
    // Klucz cache oparty na slugu
    getKey: (event) => `article-${getRouterParam(event, 'slug')}`,
    // Czas życia cache: 1 godzina
    maxAge: 3600,
    // Stale-while-revalidate: serwowanie nieaktualnego cache podczas aktualizacji
    staleMaxAge: 7200,
    // Unieważnianie oparte na tagach
    tags: ['articles']
  }
)

Aby unieważnić cache przy zmianie treści, Nuxt udostępnia system tagów.

server/api/articles/[slug].put.tstypescript
// Aktualizacja artykułu z unieważnieniem cache

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

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

  // Aktualizacja artykułu
  const article = await updateArticle(slug, body)

  // Unieważnienie cache dla tego artykułu
  await useStorage('cache').removeItem(`nitro:handlers:article-${slug}`)

  // Lub unieważnianie po tagach (wszystkie artykuły)
  // await useStorage('cache').clear('articles')

  return article
})
Cache rozproszony

W produkcji z wieloma instancjami cache w pamięci jest niewystarczający. Zaleca się skonfigurowanie Redisa lub innego systemu rozproszonego za pośrednictwem konfiguracji Nitro, aby zapewnić spójność między instancjami.

Zarządzanie SEO i metadanymi

SSR umożliwia optymalizację SEO poprzez generowanie metadanych po stronie serwera. Nuxt 3 oferuje kilka podejść do dynamicznego zarządzania meta tagami.

vue
<script setup lang="ts">
// pages/blog/[slug].vue
// Strona blogowa ze zoptymalizowanym SEO

const route = useRoute()

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

// Dynamiczna konfiguracja SEO oparta na artykule
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
})

// Dane strukturalne dla 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>

Dla stron statycznych metadane można zdefiniować bezpośrednio w komponencie.

vue
<script setup lang="ts">
// pages/about.vue
// Strona statyczna z SEO

definePageMeta({
  title: 'O nas'
})

useSeoMeta({
  title: 'O SharpSkill | Przygotowanie do rozmów technicznych',
  description: 'Poznaj SharpSkill, platformę do przygotowania do rozmów technicznych. Misja: pomóc programistom odnieść sukces na rozmowach technicznych.',
  ogTitle: 'O SharpSkill',
  ogDescription: 'Platforma do przygotowania do rozmów technicznych',
  ogImage: '/images/og-about.webp'
})
</script>

Wdrożenie i kwestie produkcyjne

Wybór metody wdrożenia zależy od używanego trybu renderowania. Oto główne opcje i ich konfiguracje.

nuxt.config.tstypescript
// Konfiguracja dla różnych środowisk wdrożenia

export default defineNuxtConfig({
  nitro: {
    // Preset zależny od platformy docelowej
    // preset: 'vercel', // Vercel
    // preset: 'netlify', // Netlify
    // preset: 'cloudflare-pages', // Cloudflare
    // preset: 'node-server', // Klasyczny Node.js

    // Konfiguracja dla Node.js w produkcji
    preset: 'node-server',

    // Kompresja odpowiedzi
    compressPublicAssets: true,

    // Konfiguracja magazynu cache
    storage: {
      cache: {
        driver: 'redis',
        url: process.env.REDIS_URL
      }
    }
  },

  // Zmienne środowiskowe runtime
  runtimeConfig: {
    // Sekrety (niedostępne dla klienta)
    apiSecret: process.env.API_SECRET,
    // Konfiguracja publiczna
    public: {
      apiBase: process.env.NUXT_PUBLIC_API_BASE || '/api'
    }
  }
})

Dla wdrożenia statycznego polecenie npm run generate tworzy folder .output/public gotowy do wdrożenia na dowolnym hoście plików statycznych.

bash
# Generowanie statyczne
npm run generate

# Zawartość .output/public można wdrożyć na:
# - Vercel (automatyczne wykrywanie)
# - Netlify (automatyczna konfiguracja)
# - GitHub Pages
# - S3 + CloudFront
# - Dowolnym CDN lub serwerze plików statycznych

Zacznij ćwiczyć!

Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.

Podsumowanie

Nuxt 3 oferuje wyjątkową elastyczność w renderowaniu aplikacji Vue.js. Wybór między SSR, SSG i renderowaniem hybrydowym zależy od konkretnych potrzeb każdego projektu.

Kluczowe wnioski:

SSR: idealne dla treści dynamicznych wymagających dobrej SEO (e-commerce, serwisy informacyjne)

SSG: doskonałe dla stabilnych treści (blogi, dokumentacja, strony marketingowe)

Hybrydowe: najlepsze podejście dla złożonych aplikacji o różnych potrzebach

useFetch/useAsyncData: automatyczna hydracja i zarządzanie cache

routeRules: precyzyjna konfiguracja zachowania każdej trasy

Caching: liczne strategie optymalizacji wydajności w produkcji

Połączenie renderowania hybrydowego z dobrze przemyślaną strategią cache pozwala budować wydajne aplikacje zoptymalizowane pod SEO, zachowujące jednocześnie interaktywność Single Page Applications.

Tagi

#nuxt 3
#vue.js
#ssr
#generowanie statyczne
#wydajność web

Udostępnij

Powiązane artykuły