SEO w aplikacji Vue 3: unhead, sitemap, prerendering i SSR

Optymalizacja pod wyszukiwarki, czyli SEO, to kluczowy element każdej strony i każdej aplikacji internetowej. Witryny, które wyszukiwarkom trudno zindeksować albo które są źle zoptymalizowane, lądują na końcu, za kolejnymi stronami wyników.

SEO w aplikacji Vue 3: unhead, sitemap, prerendering i SSR
Szybka odpowiedź

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:

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

Jeś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 to już przeszłość

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.

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')

W komponencie useHead i useSeoMeta zapisują do <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>
Dwie różne ścieżki importu

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.

javascript
const produit = ref(null)

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

Ta sama karta dostępna pod kilkoma adresami, z parametrami śledzenia i bez nich, rozmywa sygnał. Zadeklaruj adres wzorcowy:

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

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>`
)

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:

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

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

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

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'
      }
    })
  }]
})

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.

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

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

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

Jak wybrać i nie pomylić się

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

  1. Serwuj kompletny HTML, przez prerendering albo renderowanie na serwerze. Bez tego reszta nie ma sensu.
  2. Tytuł i opis osobne dla każdej strony, przez unhead.
  3. Link kanoniczny na stronach dostępnych pod kilkoma adresami.
  4. Wygenerowany sitemap zadeklarowany w robots.txt.
  5. Dane strukturalne na stronach opisujących rozpoznawalny byt.
  6. 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

Instalowanie vue-meta Jego wersja dla Vue 3 nigdy nie została ustabilizowana. Biblioteką używaną dzisiaj, także przez Nuxt, jest unhead.
Dwie ścieżki importu unhead Composable'e pochodzą z @unhead/vue, a createHead z @unhead/vue/client. Import wszystkiego z jednej ścieżki daje „useHead is not a function”.
Dopieszczanie znaczników meta na pustej aplikacji Robot, który nie wykonuje JavaScriptu, nie widzi nic. Sprawdź najpierw, co zwraca curl.
Budowanie linku kanonicznego z window.location.href Parametry kampanii trafiają wtedy do adresu kanonicznego. Użyj ścieżki trasy.
Mierzenie wydajności na serwerze deweloperskim Te liczby nie mają nic wspólnego z produkcją. Mierz na wyniku npm run build.

JavaScriptSEOVue.js

Damien Flandrin Web developer od 2010 roku, twórca Gekkode i Email Impact. Każdy artykuł jest sprawdzany na prawdziwym projekcie przed publikacją. Kontakt
Newsletter

Nowe testy, poradniki i projekty — e-mailem.

Powtarzalne testy, wersjonowany kod, datowane wyniki. Nigdy spamu.