
Zacznij od sprawdzenia, co dostaje robot, przez curl: jeśli HTML jest pusty, żaden znacznik meta dodany po stronie klienta nic nie da. Prawdziwą odpowiedzią jest prerendering albo renderowanie na serwerze. Dopiero potem zajmij się tytułami, opisami i danymi strukturalnymi za pomocą unhead, który zastąpił vue-meta.
Aplikacja Vue buduje swoją stronę w przeglądarce. HTML wysyłany przez serwer jest prawie pusty, a dalej wszystko zależy od wykonania JavaScriptu. Dla Google to jeszcze do przejścia, dla pozostałych robotów dużo mniej, zaczynając od tych, które zasilają asystentów konwersacyjnych. Oto jak zabrać się za ten problem po kolei.
Prawdziwym problemem nie są znaczniki meta
Pierwsze pytanie nie brzmi „jak zmienić tytuł”, tylko „co dostaje robot”. Sprawdź to bez przeglądarki:
curl -s https://mon-site.com/ma-page | head -40Jeśli odpowiedzią jest pusty <div id="app"></div>, żaden znacznik meta dodany po stronie klienta niczego nie zmieni dla robota, który nie wykonuje JavaScriptu. Renderowanie musi się wtedy odbyć przed wysłaniem. To temat ostatniej sekcji i zdecydowanie najważniejszy.
Przykłady zakładają projekt utworzony przez create-vue i Vite. Kolejne sekcje zajmują się najpierw metadanymi, bo przydają się w każdym scenariuszu, także przy renderowaniu na serwerze.
Metadane z unhead
Każda strona musi mieć własny tytuł i opis. unhead instaluje się jak każdy inny plugin Vue. W aplikacji jednostronicowej muszą się zmieniać przy każdej nawigacji.
vue-meta był odpowiedzią z czasów Vue 2 i większość artykułów w sieci nadal się do niego odwołuje. Wersja dla Vue 3 nigdy nie doczekała się stabilnego wydania. Biblioteką używaną dzisiaj, również przez Nuxt, jest 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')W komponencie useHead i useSeoMeta zapisują do <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>Composable’e pochodzą z @unhead/vue, ale createHead z @unhead/vue/client, albo z @unhead/vue/server przy renderowaniu na serwerze. Import wszystkiego z tej samej ścieżki kończy się błędem createHead is not a function.
useSeoMeta oszczędza ręcznego wypisywania tablic znaczników: każdy klucz odpowiada znanemu znacznikowi, a literówki wychodzą już przy podpowiedziach edytora.
Wartości mogą być reaktywne: przekaż ref albo wartość obliczaną, a znacznik podąży za wczytaniem danych.
const produit = ref(null)
useHead({
title: computed(() => produit.value?.nom ?? 'Chargement…')
})Link kanoniczny
Ta sama karta dostępna pod kilkoma adresami, z parametrami śledzenia i bez nich, rozmywa sygnał. Zadeklaruj adres wzorcowy:
useHead({
link: [{ rel: 'canonical', href: `https://mon-site.com${route.path}` }]
})Buduj go ze ścieżki trasy, nigdy z window.location.href, który zabrałby ze sobą parametry kampanii.
Sitemap
Plik sitemap.xml wymienia adresy, które uznajesz za istotne. W aplikacji jednostronicowej nie da się go odkryć przez przechodzenie po linkach: trzeba go wygenerować.
Generuj go z tego samego źródła co trasy, na etapie budowania, a nie ręcznie:
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>`
)Trasy z parametrem są pomijane: ich rzeczywiste adresy muszą pochodzić z twoich danych, a nie z definicji tras. Następnie zadeklaruj plik w robots.txt:
User-agent: *
Allow: /
Sitemap: https://mon-site.com/sitemap.xmlDane strukturalne
Blok JSON-LD wprost opisuje, co zawiera strona. To on zasila wyniki rozszerzone i czyta się go dużo łatwiej niż tekst wymagający interpretacji.
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'
}
})
}]
})Sprawdź efekt testem wyników z elementami rozszerzonymi od Google, na adresie online, a nie na kodzie źródłowym.
Wydajność
Core Web Vitals ważą w rankingu. Trzy dźwignie dają większość zysku w aplikacji Vue.
Podziel bundle według tras. Wystarczy dynamiczny import w definicji tras: Vite wypuszcza osobny plik, ładowany na żądanie.
const routes = [
{ path: '/', component: () => import('../views/AccueilView.vue') },
{ path: '/produit/:id', component: () => import('../views/ProduitView.vue') }
]Podawaj wymiary obrazów. Jawne width i height eliminują przesunięcia układu, które psują CLS. Format AVIF lub WebP mocno zbija wagę.
Mierz, zanim zaczniesz optymalizować. Uruchom Lighthouse na zbudowanej wersji, nie na serwerze deweloperskim: te liczby nie mają ze sobą nic wspólnego.
npm run build
npm run preview
npx lighthouse http://localhost:4173 --viewRenderowanie na serwerze (SSR), czyli prawdziwa odpowiedź
Wszystko powyżej poprawia aplikację, która już jest indeksowalna. Jeśli twój początkowy HTML jest pusty, to właśnie tutaj rozstrzyga się partia.
Trzy opcje, w kolejności rosnącego nakładu pracy.
Prerendering generuje kompletny plik HTML dla każdej trasy na etapie budowania. To rozwiązanie najprostsze i wystarcza wszędzie tam, gdzie treść nie zależy od odwiedzającego: strony ofertowe, dokumentacja, blog. Plugin taki jak vite-plugin-prerender dokłada się do istniejącej konfiguracji, bez ruszania kodu aplikacji.
Generowanie statycznej strony idzie tym samym tropem dalej, obsługując też dane i routing. Nuxt w trybie nuxt generate pokrywa tę potrzebę.
Renderowanie na serwerze, czyli SSR od server-side rendering, buduje stronę przy każdym żądaniu. Jest konieczne, gdy treść zależy od odwiedzającego albo zmienia się bez przerwy. Wymaga serwera Node i uwagi przy kodzie, który zakłada istnienie window lub document. Nuxt pozostaje najkrótszą drogą, SSR postawiony ręcznie to projekt sam w sobie.
Jeśli treść jest ta sama dla wszystkich, prerenderuj: dostajesz najlepszy wynik najmniejszym nakładem. Renderowanie na serwerze uzasadnia się dopiero wtedy, gdy strona naprawdę zależy od tego, kto o nią prosi.
Widoczność w odpowiedziach asystentów
Roboty asystentów konwersacyjnych z reguły nie wykonują JavaScriptu. Aplikacja renderowana w całości po stronie klienta jest dla nich niewidzialna, niezależnie od jej znaczników meta.
To, co dla nich się liczy, pokrywa się z klasycznymi dobrymi praktykami: HTML serwowany od razu, struktura nagłówków idąca za treścią, dane strukturalne, jasna odpowiedź na początku strony zamiast po trzech akapitach wstępu.
Kolejność priorytetów
- Serwuj kompletny HTML, przez prerendering albo renderowanie na serwerze. Bez tego reszta nie ma sensu.
- Tytuł i opis osobne dla każdej strony, przez unhead.
- Link kanoniczny na stronach dostępnych pod kilkoma adresami.
- Wygenerowany sitemap zadeklarowany w
robots.txt. - Dane strukturalne na stronach opisujących rozpoznawalny byt.
- Wydajność, mierzona na zbudowanej wersji.
Jeśli dopiero poznajesz Vue, poradnik Vue 3 dla początkujących daje podstawy potrzebne, zanim ruszysz z routingiem i renderowaniem na serwerze.
Częste błędy
@unhead/vue, a createHead z @unhead/vue/client. Import wszystkiego z jednej ścieżki daje „useHead is not a function”.curl.npm run build.

