
Kijk eerst met curl wat een bot binnenkrijgt: is de HTML leeg, dan helpt geen enkele metatag die je aan de clientkant toevoegt. Prerendering of server-side rendering is het echte antwoord. Pas daarna regel je titels, beschrijvingen en gestructureerde data met unhead, de opvolger van vue-meta.
Een Vue-applicatie bouwt haar pagina in de browser. De HTML die de server uitlevert is bijna leeg, en daarna hangt alles af van de uitvoering van JavaScript. Voor Google is dat haalbaar, voor de andere bots veel minder, om te beginnen die van de chatassistenten. Zo pak je het probleem in de juiste volgorde aan.
Het echte probleem zijn niet de metatags
De eerste vraag is niet “hoe verander ik mijn titel”, maar “wat krijgt de bot binnen”. Controleer dat zonder browser:
curl -s https://mon-site.com/ma-page | head -40Is het antwoord een lege <div id="app"></div>, dan verandert geen enkele metatag die je aan de clientkant toevoegt daar iets aan voor een bot die geen JavaScript uitvoert. De rendering moet dan vóór het versturen gebeuren. Dat is het onderwerp van de laatste sectie, en veruit het belangrijkste.
De voorbeelden gaan uit van een project dat is opgezet met create-vue en Vite. De secties hierna behandelen eerst de metadata, want die zijn in alle gevallen nuttig, ook met server-side rendering.
Metadata met unhead
Elke pagina hoort haar eigen titel en beschrijving te hebben. unhead installeer je zoals elke andere Vue-plugin. In een single-page application moeten titel en beschrijving bij elke navigatie meeveranderen.
vue-meta was het antwoord uit de tijd van Vue 2, en de meeste artikelen online verwijzen er nog steeds naar. De versie voor Vue 3 is nooit stabiel geworden. De bibliotheek die vandaag wordt gebruikt, ook door Nuxt, is unhead.
npm install @unhead/vueimport { createApp } from 'vue'
import { createHead } from '@unhead/vue/client'
import App from './App.vue'
const app = createApp(App)
app.use(createHead())
app.mount('#app')In een component schrijven useHead en useSeoMeta in de <head>:
<script setup>
import { useHead, useSeoMeta } from '@unhead/vue'
useHead({
title: 'T-Shirt Gekkode',
titleTemplate: '%s — Boutique Gekkode',
link: [{ rel: 'canonical', href: 'https://mon-site.com/t-shirt' }]
})
useSeoMeta({
description: 'Un T-shirt en coton, laine et polyester.',
ogTitle: 'T-Shirt Gekkode',
ogDescription: 'Un T-shirt en coton, laine et polyester.',
ogImage: 'https://mon-site.com/images/t-shirt.png',
ogUrl: 'https://mon-site.com/t-shirt',
twitterCard: 'summary_large_image'
})
</script>De composables komen uit @unhead/vue, maar createHead komt uit @unhead/vue/client, of uit @unhead/vue/server bij server-side rendering. Alles vanaf hetzelfde pad importeren levert de fout createHead is not a function op.
Met useSeoMeta hoef je de arrays met tags niet met de hand te schrijven: elke sleutel komt overeen met een bekende tag, en typefouten zie je meteen in de autocompletion.
De waarden mogen reactief zijn: geef een ref of een computed property door, en de tag volgt het laden van de data.
const produit = ref(null)
useHead({
title: computed(() => produit.value?.nom ?? 'Chargement…')
})De canonical link
Dezelfde pagina die via meerdere URL’s bereikbaar is, met of zonder trackingparameters, verwatert het signaal. Geef het referentieadres op:
useHead({
link: [{ rel: 'canonical', href: `https://mon-site.com${route.path}` }]
})Bouw dat adres op uit het pad van de route, nooit uit window.location.href, want dan sleep je de campagneparameters mee.
De sitemap
Een bestand sitemap.xml somt de URL’s op die jij belangrijk vindt. In een single-page application is die lijst niet te ontdekken door te crawlen: je moet hem genereren.
Genereer hem bij de build uit dezelfde bron als je routes, in plaats van met de hand:
import { writeFileSync } from 'node:fs'
import { routes } from '../src/router/routes.js'
const base = 'https://mon-site.com'
const urls = routes
.filter((r) => !r.meta?.noindex && !r.path.includes(':'))
.map((r) => ` <url><loc>${base}${r.path}</loc></url>`)
.join('\n')
writeFileSync(
'public/sitemap.xml',
`<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${urls}
</urlset>`
)Routes met een parameter vallen af: hun echte URL’s moeten uit je data komen, niet uit de definitie van de routes. Meld het bestand daarna aan in robots.txt:
User-agent: *
Allow: /
Sitemap: https://mon-site.com/sitemap.xmlGestructureerde data
Een JSON-LD-blok beschrijft expliciet wat er op de pagina staat. Dat voedt de rich results, en het wordt veel makkelijker gelezen dan tekst die geïnterpreteerd moet worden.
useHead({
script: [{
type: 'application/ld+json',
innerHTML: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'Product',
name: 'T-Shirt Gekkode',
offers: {
'@type': 'Offer',
price: '25.00',
priceCurrency: 'EUR',
availability: 'https://schema.org/InStock'
}
})
}]
})Controleer het resultaat met de rich-resultstest van Google, op de live URL en niet op de broncode.
Performance
De Core Web Vitals wegen mee in de ranking. Drie knoppen leveren op een Vue-applicatie het grootste deel van de winst op.
De bundle per route opsplitsen. Een dynamische import in de definitie van de routes volstaat: Vite maakt er een apart bestand van, dat op aanvraag wordt geladen.
const routes = [
{ path: '/', component: () => import('../views/AccueilView.vue') },
{ path: '/produit/:id', component: () => import('../views/ProduitView.vue') }
]Afmetingen op je afbeeldingen zetten. Een expliciete width en height voorkomen de layoutverschuiving die je CLS verpest. Het formaat AVIF of WebP drukt het gewicht flink.
Meten voor je optimaliseert. Draai Lighthouse op de gebouwde site, niet op de ontwikkelserver: die cijfers hebben niets met elkaar te maken.
npm run build
npm run preview
npx lighthouse http://localhost:4173 --viewServer-side rendering (SSR), het echte antwoord
Alles hierboven verbetert een applicatie die al indexeerbaar is. Is je initiële HTML leeg, dan wordt de wedstrijd hier beslist.
Drie opties, in oplopende volgorde van inspanning.
Prerendering genereert bij de build één volledig HTML-bestand per route. Dat is de eenvoudigste oplossing, en ze volstaat zodra de inhoud niet van de bezoeker afhangt: presentatiepagina’s, documentatie, blog. Een plugin als vite-plugin-prerender voeg je toe aan de bestaande configuratie, zonder de code van de applicatie aan te raken.
Static site generation trekt hetzelfde idee verder door, inclusief het beheer van data en routing. Nuxt in de modus nuxt generate dekt die behoefte.
Server-side rendering, of SSR voor server-side rendering, bouwt de pagina bij elke request op. Dat is nodig wanneer de inhoud van de bezoeker afhangt of voortdurend verandert. Het vraagt een Node-server en oplettendheid bij code die uitgaat van het bestaan van window of document. Nuxt blijft de kortste weg, server-side rendering met de hand opzetten is een project op zich.
Is de inhoud voor iedereen hetzelfde, prerender dan: je krijgt het beste resultaat voor de minste moeite. Server-side rendering is alleen te rechtvaardigen als de pagina echt afhangt van wie hem opvraagt.
Vindbaarheid in de antwoorden van assistenten
De bots van chatassistenten voeren over het algemeen geen JavaScript uit. Een applicatie die volledig aan de clientkant wordt gerenderd is voor hen onzichtbaar, wat er ook in haar metatags staat.
Wat voor hen telt, valt samen met de klassieke goede gewoontes: HTML die zo wordt uitgeleverd, een koppenstructuur die het betoog volgt, gestructureerde data, en een helder antwoord bovenaan de pagina in plaats van na drie inleidende alinea’s.
De volgorde van de prioriteiten
- Volledige HTML uitleveren, via prerendering of server-side rendering. Zonder dat heeft de rest geen zin.
- Een eigen titel en beschrijving per pagina, met unhead.
- Een canonical link op pagina’s die via meerdere URL’s bereikbaar zijn.
- Een gegenereerde sitemap, aangemeld in
robots.txt. - Gestructureerde data op pagina’s die een herkenbare entiteit beschrijven.
- Performance, gemeten op de gebouwde site.
Ben je nieuw in Vue, dan legt de tutorial Vue 3 voor beginners de basis die je nodig hebt voordat je aan routing en server-side rendering begint.
Veelgemaakte fouten
@unhead/vue, createHead uit @unhead/vue/client. Alles vanaf hetzelfde pad importeren geeft “useHead is not a function”.curl.npm run build.

