Vue.js SEO: een Vue 3-applicatie echt indexeerbaar maken

Zoekmachineoptimalisatie, of SEO, is een cruciaal onderdeel van elke website en elke webapplicatie. Sites en applicaties die zoekmachines maar moeizaam indexeren, of die slecht geoptimaliseerd zijn, verdwijnen uiteindelijk achter pagina's en pagina's met resultaten.

Vue.js SEO: een Vue 3-applicatie echt indexeerbaar maken
Kort antwoord

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:

bash
curl -s https://mon-site.com/ma-page | head -40

Is 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 is verleden tijd

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.

bash
npm install @unhead/vue
src/main.js
import { 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>:

src/views/ProduitView.vue
<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>
Twee verschillende importpaden

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.

javascript
const produit = ref(null)

useHead({
  title: computed(() => produit.value?.nom ?? 'Chargement…')
})

Dezelfde pagina die via meerdere URL’s bereikbaar is, met of zonder trackingparameters, verwatert het signaal. Geef het referentieadres op:

javascript
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:

scripts/sitemap.js
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:

public/robots.txt
User-agent: *
Allow: /

Sitemap: https://mon-site.com/sitemap.xml

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

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

src/router/index.js
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.

bash
npm run build
npm run preview
npx lighthouse http://localhost:4173 --view

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

Kiezen zonder ernaast te zitten

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

  1. Volledige HTML uitleveren, via prerendering of server-side rendering. Zonder dat heeft de rest geen zin.
  2. Een eigen titel en beschrijving per pagina, met unhead.
  3. Een canonical link op pagina’s die via meerdere URL’s bereikbaar zijn.
  4. Een gegenereerde sitemap, aangemeld in robots.txt.
  5. Gestructureerde data op pagina’s die een herkenbare entiteit beschrijven.
  6. 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

vue-meta installeren De versie voor Vue 3 is nooit stabiel geworden. De bibliotheek die vandaag wordt gebruikt is unhead, ook door Nuxt.
Twee importpaden voor unhead De composables komen uit @unhead/vue, createHead uit @unhead/vue/client. Alles vanaf hetzelfde pad importeren geeft “useHead is not a function”.
Metatags verzorgen op een lege applicatie Een bot die geen JavaScript uitvoert ziet niets. Controleer eerst de uitvoer van curl.
De canonical opbouwen uit window.location.href De campagneparameters belanden dan in de canonical link. Gebruik het pad van de route.
Performance meten op de ontwikkelserver Die cijfers hebben niets met productie te maken. Meet op het resultaat van npm run build.

JavaScriptSEOVue.js

Damien Flandrin Webdeveloper sinds 2010, maker van Gekkode en Email Impact. Elk artikel wordt vóór publicatie getest op een echt project. Contact
Nieuwsbrief

Nieuwe tests, tutorials en projecten, per e-mail.

Reproduceerbare tests, geversioneerde code, gedateerde resultaten. Nooit spam.