Nuxt 3: SSR en statische generatie, de complete gids

Beheers SSR en statische generatie met Nuxt 3. Van useFetch tot route rules: leer hoe Vue.js-applicaties optimaal presteren.

Illustratie van server-side rendering en statische generatie met Nuxt 3 en Vue.js

Nuxt 3 verandert de manier waarop Vue.js-applicaties gebouwd worden door meerdere renderingmodi aan te bieden die geschikt zijn voor uiteenlopende use cases. Van Server-Side Rendering (SSR) tot statische generatie en hybride rendering biedt het framework opmerkelijke flexibiliteit om performance en SEO te optimaliseren.

Vereisten

Deze tutorial gaat uit van basiskennis van Vue 3 en de Composition API. Bekendheid met server-side rendering is handig maar niet verplicht, omdat de fundamenten in de gids worden uitgelegd.

De renderingmodi van Nuxt 3 begrijpen

Voordat men in de code duikt, is het essentieel om de verschillen tussen de beschikbare renderingmodi te begrijpen. Elke modus speelt in op specifieke behoeften qua performance, SEO en gebruikerservaring.

SSR (Server-Side Rendering) genereert de HTML op de server bij elke request. Statische generatie (SSG) pre-genereert alle pagina's tijdens de build. De hybride modus maakt het mogelijk deze benaderingen per pagina te combineren.

nuxt.config.tstypescript
// Configuratie van de verschillende renderingmodi
export default defineNuxtConfig({
  // SSR standaard ingeschakeld (aanbevolen voor SEO)
  ssr: true,

  // Statische generatie: pre-rendert alle pagina's
  // Gebruik 'npm run generate' om te bouwen
  // target: 'static', // Nuxt 2 syntax

  // Hybride modus: per route configureerbaar
  routeRules: {
    // Homepagina: pre-gerenderd en gecached
    '/': { prerender: true },
    // Blog: statische generatie
    '/blog/**': { prerender: true },
    // Dashboard: alleen client-side rendering
    '/dashboard/**': { ssr: false },
    // API: geen pre-rendering
    '/api/**': { prerender: false }
  }
})

Deze configuratie toont de kracht van de hybride modus: elke sectie van de applicatie gebruikt de renderingmodus die het beste past bij haar behoeften.

Data ophalen met useFetch en useAsyncData

Nuxt 3 biedt twee belangrijke composables om data isomorf op te halen. Deze composables werken zowel server- als client-side, met automatisch hydratatiebeheer.

useFetch is een wrapper rond useAsyncData die HTTP-aanroepen vereenvoudigt. useAsyncData biedt meer controle voor geavanceerde scenario's.

vue
<script setup lang="ts">
// pages/blog/[slug].vue
// Detailpagina van een artikel met useFetch

// Routeparameter ophalen
const route = useRoute()

// useFetch: automatisch data ophalen
// Data wordt server-side opgehaald en op de client gehydrateerd
const { data: article, pending, error } = await useFetch(
  `/api/articles/${route.params.slug}`,
  {
    // Unieke key voor caching en deduplicatie
    key: `article-${route.params.slug}`,
    // Data zo nodig transformeren
    transform: (response) => response.data,
    // Cache-opties
    getCachedData: (key) => {
      // Controleren of data in cache staat
      const nuxtApp = useNuxtApp()
      return nuxtApp.payload.data[key]
    }
  }
)

// Foutafhandeling met navigatie
if (error.value) {
  throw createError({
    statusCode: 404,
    message: 'Artikel niet gevonden'
  })
}
</script>

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

Voor situaties die meer controle vragen, kan useAsyncData elke asynchrone functie uitvoeren.

vue
<script setup lang="ts">
// pages/products/index.vue
// Productlijst met useAsyncData en filters

const route = useRoute()

// useAsyncData: volledige controle over de fetch-logica
const { data: products, refresh } = await useAsyncData(
  'products-list',
  async () => {
    // Indien nodig uit meerdere bronnen ophalen
    const [productsResponse, categoriesResponse] = await Promise.all([
      $fetch('/api/products', {
        query: {
          category: route.query.category,
          sort: route.query.sort || 'date'
        }
      }),
      $fetch('/api/categories')
    ])

    // Data combineren en transformeren
    return {
      products: productsResponse.data,
      categories: categoriesResponse.data,
      total: productsResponse.meta.total
    }
  },
  {
    // Verversen wanneer query params veranderen
    watch: [() => route.query]
  }
)

// Functie voor handmatige refresh
const updateFilters = async (newCategory: string) => {
  await navigateTo({
    query: { ...route.query, category: newCategory }
  })
}
</script>

Deze composables voorkomen dubbele fetches: data die op de server is opgehaald wordt geserialiseerd in de HTML-payload en hergebruikt tijdens de client-hydratatie.

SSR aanpassen met server hooks

De SSR van Nuxt 3 kan via server hooks aangepast worden. Deze hooks maken het mogelijk in te grijpen op verschillende momenten van de renderingcyclus om standaardgedrag aan te passen.

server/plugins/render-hooks.tstypescript
// Server-plugin om SSR-rendering aan te passen

export default defineNitroPlugin((nitroApp) => {
  // Hook die voor het renderen van elke pagina draait
  nitroApp.hooks.hook('render:html', (html, { event }) => {
    // Scripts of metadata injecteren
    html.head.push(`
      <script>
        // Analytics of globale configuratie
        window.__APP_CONFIG__ = {
          environment: '${process.env.NODE_ENV}',
          apiUrl: '${process.env.API_URL}'
        }
      </script>
    `)
  })

  // Hook voor beheer van de render-cache
  nitroApp.hooks.hook('render:response', (response, { event }) => {
    // Aangepaste cache-headers toevoegen
    const path = event.path

    if (path.startsWith('/blog/')) {
      // Lange cache voor blogartikelen
      response.headers['Cache-Control'] = 'public, max-age=3600, s-maxage=86400'
    } else if (path.startsWith('/api/')) {
      // Geen cache voor API's
      response.headers['Cache-Control'] = 'no-store'
    }
  })
})
SSR-performance

De hook render:response is ideaal om HTTP-cachingstrategieën te implementeren. SSR combineren met een CDN dat Cache-Control-headers respecteert, maakt het mogelijk pre-gerenderde pagina's te serveren met behoud van invalidatiemogelijkheden.

Statische generatie met nuxt generate

Statische generatie bouwt alle pagina's vooraf tijdens de build. Deze aanpak is ideaal voor sites met stabiele content zoals blogs, documentatie of marketingsites.

Voor dynamische routes moet Nuxt alle te genereren URL's kennen. De hook prerender:routes maakt het mogelijk deze routes programmatisch te definiëren.

nuxt.config.tstypescript
// Volledige configuratie voor statische generatie

export default defineNuxtConfig({
  // Statische generatie inschakelen
  nitro: {
    prerender: {
      // Automatisch crawlen van links inschakelen
      crawlLinks: true,
      // Routes die altijd opgenomen moeten worden
      routes: ['/', '/about', '/contact'],
      // Bepaalde routes negeren
      ignore: ['/admin', '/api']
    }
  },

  hooks: {
    // Hook om dynamische routes te genereren
    async 'prerender:routes'(ctx) {
      // Artikelen ophalen uit API of DB
      const articles = await fetch('https://api.example.com/articles')
        .then(res => res.json())

      // Artikelroutes toevoegen
      for (const article of articles) {
        ctx.routes.add(`/blog/${article.slug}`)
      }

      // Categorieën ophalen
      const categories = await fetch('https://api.example.com/categories')
        .then(res => res.json())

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

Voor projecten met veel pagina's kan de automatische crawler tekortschieten. Hier volgt een robuustere aanpak met een apart configuratiebestand.

server/utils/generate-routes.tstypescript
// Hulpfunctie om de lijst met dynamische routes te genereren

import { prisma } from './prisma'

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

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

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

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

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

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

  return routes
}

Klaar om je Vue.js / Nuxt.js gesprekken te halen?

Oefen met onze interactieve simulatoren, flashcards en technische tests.

Hybride rendering met routeRules

Hybride rendering is de paradepaardje-feature van Nuxt 3. Het stelt in staat verschillende renderingregels per route te definiëren en zo het beste van SSR en SSG te combineren.

nuxt.config.tstypescript
// Geavanceerde configuratie voor hybride rendering

export default defineNuxtConfig({
  routeRules: {
    // Marketingpagina's: pre-gerenderd en lange tijd gecached
    '/': { prerender: true },
    '/pricing': { prerender: true },
    '/features/**': { prerender: true },

    // Blog: ISR (Incremental Static Regeneration)
    // Revalidatie elk uur
    '/blog/**': {
      isr: 3600,
      prerender: true
    },

    // Documentatie: CDN-cache met revalidatie
    '/docs/**': {
      swr: 86400, // Stale-while-revalidate
      prerender: true
    },

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

    // Winkelwagen en checkout: alleen client-side
    '/cart': { ssr: false },
    '/checkout/**': { ssr: false },

    // Dashboard: SPA-modus
    '/dashboard/**': {
      ssr: false,
      // Pre-rendering uitschakelen
      prerender: false
    },

    // API-routes: standaard geen cache
    '/api/**': {
      cors: true,
      headers: {
        'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE'
      }
    }
  }
})

Deze configuratie illustreert een typische architectuur voor moderne applicaties: openbare pagina's worden voor SEO geoptimaliseerd met SSG, terwijl interactieve secties client-side rendering gebruiken.

Performanceoptimalisatie met datacaching

Naast paginacaching maakt Nuxt 3 het mogelijk opgehaalde data te cachen. Deze strategie verlaagt de belasting op API's en verbetert responstijden.

server/api/articles/[slug].get.tstypescript
// API-endpoint met datacaching

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

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

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

    const article = await getArticleBySlug(slug)

    if (!article) {
      throw createError({
        statusCode: 404,
        message: 'Artikel niet gevonden'
      })
    }

    return article
  },
  {
    // Cachekey op basis van de slug
    getKey: (event) => `article-${getRouterParam(event, 'slug')}`,
    // Cacheduur: 1 uur
    maxAge: 3600,
    // Stale-while-revalidate: oude cache serveren tijdens update
    staleMaxAge: 7200,
    // Tag-gebaseerde invalidatie
    tags: ['articles']
  }
)

Om de cache te invalideren bij contentwijzigingen biedt Nuxt een tagsysteem.

server/api/articles/[slug].put.tstypescript
// Artikel bijwerken met cache-invalidatie

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

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

  // Het artikel bijwerken
  const article = await updateArticle(slug, body)

  // Cache voor dit artikel invalideren
  await useStorage('cache').removeItem(`nitro:handlers:article-${slug}`)

  // Of tag-gebaseerde invalidatie (alle artikelen)
  // await useStorage('cache').clear('articles')

  return article
})
Gedistribueerde cache

In productie met meerdere instanties is in-memory cache onvoldoende. Het wordt aangeraden Redis of een ander gedistribueerd systeem te configureren via de Nitro-configuratie om consistentie tussen instanties te garanderen.

SEO en metadata beheren

SSR maakt SEO-optimalisatie mogelijk door metadata server-side te genereren. Nuxt 3 biedt verschillende benaderingen om meta tags dynamisch te beheren.

vue
<script setup lang="ts">
// pages/blog/[slug].vue
// Blogpagina met geoptimaliseerde SEO

const route = useRoute()

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

// Dynamische SEO-configuratie op basis van het artikel
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
})

// Gestructureerde data voor 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>

Voor statische pagina's kan de metadata direct in het component gedefinieerd worden.

vue
<script setup lang="ts">
// pages/about.vue
// Statische pagina met SEO

definePageMeta({
  title: 'Over ons'
})

useSeoMeta({
  title: 'Over SharpSkill | Voorbereiding op technische sollicitatiegesprekken',
  description: 'Maak kennis met SharpSkill, het platform voor de voorbereiding op technische sollicitatiegesprekken. De missie: ontwikkelaars helpen slagen in hun technische gesprekken.',
  ogTitle: 'Over SharpSkill',
  ogDescription: 'Het platform voor voorbereiding op technische sollicitatiegesprekken',
  ogImage: '/images/og-about.webp'
})
</script>

Deployment en productieoverwegingen

De deploymentkeuze hangt af van de gebruikte renderingmodus. Hier de belangrijkste opties en hun configuraties.

nuxt.config.tstypescript
// Configuratie voor verschillende deployomgevingen

export default defineNuxtConfig({
  nitro: {
    // Preset afhankelijk van het doelplatform
    // preset: 'vercel', // Vercel
    // preset: 'netlify', // Netlify
    // preset: 'cloudflare-pages', // Cloudflare
    // preset: 'node-server', // Klassieke Node.js

    // Configuratie voor Node.js in productie
    preset: 'node-server',

    // Compressie van responses
    compressPublicAssets: true,

    // Configuratie van de cache-storage
    storage: {
      cache: {
        driver: 'redis',
        url: process.env.REDIS_URL
      }
    }
  },

  // Runtime omgevingsvariabelen
  runtimeConfig: {
    // Geheimen (niet blootgesteld aan de client)
    apiSecret: process.env.API_SECRET,
    // Publieke configuratie
    public: {
      apiBase: process.env.NUXT_PUBLIC_API_BASE || '/api'
    }
  }
})

Voor statische deployment maakt het commando npm run generate een map .output/public aan die klaar is om bij elke statische bestandshost geplaatst te worden.

bash
# Statische generatie
npm run generate

# De inhoud van .output/public kan ingezet worden op:
# - Vercel (automatische detectie)
# - Netlify (automatische configuratie)
# - GitHub Pages
# - S3 + CloudFront
# - Elke CDN of statische bestandsserver

Begin met oefenen!

Test je kennis met onze gespreksimulatoren en technische tests.

Conclusie

Nuxt 3 biedt uitzonderlijke flexibiliteit voor het renderen van Vue.js-applicaties. De keuze tussen SSR, SSG en hybride rendering hangt af van de specifieke behoeften van elk project.

Belangrijkste punten:

SSR: ideaal voor dynamische content die goede SEO vereist (e-commerce, nieuwssites)

SSG: perfect voor stabiele content (blogs, documentatie, marketingsites)

Hybride: de beste aanpak voor complexe applicaties met uiteenlopende behoeften

useFetch/useAsyncData: automatische hydratatie en cachebeheer

routeRules: fijnmazige configuratie voor het gedrag van elke route

Caching: meerdere strategieën om productieperformance te optimaliseren

Hybride rendering combineren met een doordachte cachestrategie maakt het mogelijk performante applicaties te bouwen die voor SEO geoptimaliseerd zijn en tegelijk de interactiviteit van Single Page Applications behouden.

Tags

#nuxt 3
#vue.js
#ssr
#statische generatie
#web performance

Delen

Gerelateerde artikelen