
Prüf zuerst mit curl, was ein Crawler tatsächlich bekommt: Ist das HTML leer, hilft kein clientseitig ergänzter Meta-Tag. Prerendering oder Server-Rendering ist die eigentliche Antwort. Erst danach kümmerst du dich mit unhead, dem Nachfolger von vue-meta, um Titel, Beschreibungen und strukturierte Daten.
Eine Vue-Anwendung baut ihre Seite im Browser auf. Das HTML, das der Server ausliefert, ist fast leer, und danach hängt alles davon ab, dass das JavaScript läuft. Für Google ist das machbar, für andere Crawler deutlich weniger – allen voran für die der KI-Assistenten. So gehst du das Problem der Reihe nach an.
Das eigentliche Problem sind nicht die Meta-Tags
Die erste Frage lautet nicht „wie ändere ich meinen Titel“, sondern „was bekommt der Crawler überhaupt“. Prüfe das ohne Browser:
curl -s https://mon-site.com/ma-page | head -40Ist die Antwort ein leeres <div id="app"></div>, ändert kein clientseitig ergänzter Meta-Tag etwas für einen Crawler, der kein JavaScript ausführt. Das Rendering muss dann vor dem Ausliefern passieren. Darum geht es im letzten Abschnitt, und der ist mit Abstand der wichtigste.
Die Beispiele setzen ein Projekt voraus, das mit create-vue und Vite erstellt wurde. Die folgenden Abschnitte behandeln zuerst die Metadaten, weil sie in jedem Fall nützlich sind, auch beim Server-Rendering.
Metadaten mit unhead
Jede Seite braucht ihren eigenen Titel und ihre eigene Beschreibung. unhead installierst du wie jedes andere Vue-Plugin. In einer Single-Page-Anwendung müssen beide sich bei jeder Navigation ändern.
vue-meta war die Antwort zu Zeiten von Vue 2, und der Großteil der Artikel im Netz verweist noch darauf. Die Version für Vue 3 wurde nie stabil. Die Bibliothek, die heute verwendet wird, auch von Nuxt, ist 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 einer Komponente schreiben useHead und useSeoMeta in den <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>Die Composables kommen aus @unhead/vue, createHead dagegen aus @unhead/vue/client oder beim Server-Rendering aus @unhead/vue/server. Alles über denselben Pfad zu importieren, ergibt den Fehler createHead is not a function.
useSeoMeta erspart dir die von Hand geschriebenen Tag-Arrays: Jeder Schlüssel entspricht einem bekannten Tag, und Tippfehler fallen schon in der Autovervollständigung auf.
Die Werte dürfen reaktiv sein: Übergib eine ref oder eine Computed Property, und der Tag folgt dem Laden der Daten.
const produit = ref(null)
useHead({
title: computed(() => produit.value?.nom ?? 'Chargement…')
})Der Canonical-Link
Derselbe Eintrag, über mehrere URLs erreichbar, mit oder ohne Tracking-Parameter, verwässert das Signal. Gib die Referenzadresse an:
useHead({
link: [{ rel: 'canonical', href: `https://mon-site.com${route.path}` }]
})Bau sie aus dem Pfad der Route auf, niemals aus window.location.href, das die Kampagnenparameter mitschleppen würde.
Die Sitemap
Eine Datei sitemap.xml listet die URLs auf, die dir wichtig sind. In einer Single-Page-Anwendung lässt sie sich nicht durch Crawlen entdecken: Sie muss erzeugt werden.
Erzeuge sie beim Build aus derselben Quelle wie deine Routen, statt sie von Hand zu pflegen:
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>`
)Routen mit Parametern bleiben außen vor: Ihre echten URLs müssen aus deinen Daten kommen, nicht aus der Routendefinition. Danach meldest du die Datei in der robots.txt an:
User-agent: *
Allow: /
Sitemap: https://mon-site.com/sitemap.xmlStrukturierte Daten
Ein JSON-LD-Block beschreibt ausdrücklich, was auf der Seite steht. Er speist die Rich Results und ist weit einfacher zu lesen als ein Text, den ein Crawler erst interpretieren muss.
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'
}
})
}]
})Prüf das Ergebnis mit dem Rich-Results-Test von Google, an der Live-URL und nicht am Quellcode.
Die Performance
Die Core Web Vitals wiegen im Ranking. Drei Hebel bringen bei einer Vue-Anwendung den Großteil des Gewinns.
Das Bundle nach Route aufteilen. Ein dynamischer Import in der Routendefinition reicht: Vite erzeugt eine eigene Datei, die bei Bedarf geladen wird.
const routes = [
{ path: '/', component: () => import('../views/AccueilView.vue') },
{ path: '/produit/:id', component: () => import('../views/ProduitView.vue') }
]Bilder dimensionieren. Explizite Angaben für width und height verhindern den Layout-Sprung, der den CLS verschlechtert. AVIF oder WebP senken das Gewicht deutlich.
Messen, bevor du optimierst. Lass Lighthouse auf der gebauten Site laufen, nicht auf dem Entwicklungsserver: Die Zahlen haben nichts miteinander zu tun.
npm run build
npm run preview
npx lighthouse http://localhost:4173 --viewServer-Rendering (SSR), die eigentliche Antwort
Alles bisher Genannte verbessert eine Anwendung, die bereits indexierbar ist. Ist dein initiales HTML leer, entscheidet sich hier die Partie.
Drei Optionen, nach steigendem Aufwand.
Das Prerendering erzeugt beim Build eine vollständige HTML-Datei je Route. Das ist die einfachste Lösung, und sie genügt, sobald der Inhalt nicht vom Besucher abhängt: Präsentationsseiten, Dokumentation, Blog. Ein Plugin wie vite-plugin-prerender kommt zur bestehenden Konfiguration dazu, ohne dass du den Anwendungscode anfassen musst.
Die statische Site-Generierung treibt dieselbe Idee weiter, samt Datenhandling und Routing. Nuxt im Modus nuxt generate deckt diesen Bedarf ab.
Das Server-Rendering, kurz SSR für server-side rendering, baut die Seite bei jeder Anfrage neu. Nötig ist das, wenn der Inhalt vom Besucher abhängt oder sich ständig ändert. Es verlangt einen Node-Server und Vorsicht bei Code, der die Existenz von window oder document voraussetzt. Nuxt bleibt der kürzeste Weg, ein von Hand aufgebautes Server-Rendering ist ein Projekt für sich.
Ist der Inhalt für alle derselbe, dann prerendere: bestes Ergebnis für den geringsten Aufwand. Server-Rendering lohnt sich nur, wenn die Seite wirklich davon abhängt, wer sie anfragt.
Sichtbarkeit in den Antworten der Assistenten
Die Crawler der KI-Assistenten führen in der Regel kein JavaScript aus. Eine vollständig clientseitig gerenderte Anwendung ist für sie unsichtbar, egal welche Meta-Tags sie trägt.
Was für sie zählt, deckt sich mit den klassischen guten Praktiken: HTML, das so ausgeliefert wird, wie es ist, eine Überschriftenstruktur, die dem Inhalt folgt, strukturierte Daten und eine klare Antwort oben auf der Seite statt nach drei einleitenden Absätzen.
Die Reihenfolge der Prioritäten
- Vollständiges HTML ausliefern, per Prerendering oder Server-Rendering. Ohne das bringt der Rest nichts.
- Ein eigener Titel und eine eigene Beschreibung pro Seite, mit unhead.
- Ein Canonical-Link auf Seiten, die über mehrere URLs erreichbar sind.
- Eine erzeugte Sitemap, angemeldet in der
robots.txt. - Strukturierte Daten auf Seiten, die eine identifizierbare Entität beschreiben.
- Die Performance, gemessen an der gebauten Site.
Wenn Vue neu für dich ist, legt das Vue-3-Tutorial für Einsteiger die nötigen Grundlagen, bevor du dich an Routing und Server-Rendering machst.
Häufige Fehler
@unhead/vue, createHead aus @unhead/vue/client. Alles über denselben Pfad zu importieren, ergibt „useHead is not a function“.curl.npm run build.

