SEO com Vue.js: tornar uma aplicação Vue 3 indexável

A otimização para motores de pesquisa, ou SEO, é uma peça central de qualquer site ou aplicação web. As aplicações e os sites que os motores de pesquisa indexam mal, ou que estão mal otimizados, acabam escondidos atrás de páginas e páginas de resultados.

SEO com Vue.js: tornar uma aplicação Vue 3 indexável
Resposta rápida

Começa por verificar o que um robô recebe com o curl: se o HTML vier vazio, nenhuma meta tag acrescentada do lado do cliente vai servir. A pré-renderização ou a renderização no servidor é a verdadeira resposta. Só depois trata dos títulos, das descrições e dos dados estruturados com o unhead, que substitui o vue-meta.

Uma aplicação Vue constrói a página no navegador. O HTML servido pelo servidor está quase vazio, e tudo depois depende da execução do JavaScript. Isso ainda passa com a Google, muito menos com os outros robôs, a começar pelos dos assistentes conversacionais. Eis como atacar o problema pela ordem certa.

O verdadeiro problema não são as meta tags

A primeira pergunta não é «como mudo o meu título» mas «o que é que o robô recebe». Verifica-o sem navegador:

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

Se a resposta for um <div id="app"></div> vazio, nenhuma meta tag acrescentada do lado do cliente muda seja o que for para um robô que não executa JavaScript. A renderização tem então de acontecer antes do envio. É o tema da última secção, e de longe o mais importante.

Os exemplos partem de um projeto criado com create-vue e Vite. As secções seguintes tratam primeiro dos metadados, porque são úteis em todos os casos, mesmo com renderização no servidor.

Os metadados com unhead

Cada página deve ter o seu título e a sua descrição. O unhead instala-se como qualquer plugin Vue. Numa aplicação de página única, título e descrição têm de mudar a cada navegação.

vue-meta é passado

O vue-meta foi a resposta no tempo do Vue 2 e a maioria dos artigos online ainda o refere. A sua versão para Vue 3 nunca chegou a estabilizar. A biblioteca usada hoje, inclusive pelo Nuxt, é o 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')

Num componente, useHead e useSeoMeta escrevem no <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>
Dois caminhos de import diferentes

Os composables vêm de @unhead/vue, mas o createHead vem de @unhead/vue/client, ou de @unhead/vue/server em renderização no servidor. Importar tudo do mesmo caminho dá o erro createHead is not a function.

O useSeoMeta evita escrever à mão os arrays de tags: cada chave corresponde a uma tag conhecida, e as gralhas saltam à vista no autocompletar.

Os valores podem ser reativos: passa uma ref ou uma propriedade computada, e a tag acompanha o carregamento dos dados.

javascript
const produit = ref(null)

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

A mesma ficha acessível por vários URL, com ou sem parâmetros de seguimento, dilui o sinal. Declara o endereço de referência:

javascript
useHead({
  link: [{ rel: 'canonical', href: `https://mon-site.com${route.path}` }]
})

Constrói-o a partir do caminho da rota, nunca de window.location.href, que arrastaria consigo os parâmetros de campanha.

O sitemap

Um ficheiro sitemap.xml lista os URL que consideras importantes. Numa aplicação de página única não pode ser descoberto por exploração: tem de ser gerado.

Gera-o a partir da mesma fonte que as tuas rotas, no momento do build, em vez de o escrever à mão:

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

As rotas com parâmetro ficam de fora: os seus URL reais têm de vir dos teus dados, não da definição das rotas. Declara depois o ficheiro no robots.txt:

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

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

Os dados estruturados

Um bloco JSON-LD descreve explicitamente o que a página contém. É o que alimenta os resultados enriquecidos, e lê-se muito mais facilmente do que um texto a interpretar.

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

Verifica o resultado com o teste de resultados enriquecidos da Google, sobre o URL em linha e não sobre o código-fonte.

O desempenho

Os Core Web Vitals pesam no ranking. Três alavancas dão o essencial do ganho numa aplicação Vue.

Dividir o bundle por rota. Basta um import dinâmico na definição das rotas: o Vite produz um ficheiro separado, carregado a pedido.

src/router/index.js
const routes = [
  { path: '/', component: () => import('../views/AccueilView.vue') },
  { path: '/produit/:id', component: () => import('../views/ProduitView.vue') }
]

Dimensionar as imagens. Um width e um height explícitos evitam o deslocamento do layout que degrada o CLS. O formato AVIF ou WebP reduz muito o peso.

Medir antes de otimizar. Corre o Lighthouse sobre o site construído, não sobre o servidor de desenvolvimento: os números não têm nada a ver.

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

A renderização no servidor (SSR), a verdadeira resposta

Tudo o que vem antes melhora uma aplicação já indexável. Se o teu HTML inicial está vazio, é aqui que o jogo se decide.

Três opções, por ordem de esforço crescente.

A pré-renderização gera um ficheiro HTML completo por rota no momento do build. É a solução mais simples, e chega assim que o conteúdo não depende do visitante: páginas de apresentação, documentação, blogue. Um plugin como o vite-plugin-prerender acrescenta-se à configuração existente, sem tocar no código da aplicação.

A geração de site estático leva a mesma ideia mais longe, com a gestão dos dados e do routing. O Nuxt em modo nuxt generate cobre esta necessidade.

A renderização no servidor, ou SSR de server-side rendering, constrói a página a cada pedido. É necessária quando o conteúdo depende do visitante ou muda em permanência. Impõe um servidor Node e obriga a ter atenção ao código que pressupõe a existência de window ou document. O Nuxt continua a ser o caminho mais curto, SSR montado à mão é um projeto por si só.

Escolher sem falhar

Se o conteúdo é o mesmo para toda a gente, pré-renderiza: obténs o melhor resultado com o menor esforço. A renderização no servidor só se justifica se a página depender mesmo de quem a pede.

Aparecer nas respostas dos assistentes

Os robôs dos assistentes conversacionais geralmente não executam JavaScript. Uma aplicação inteiramente renderizada do lado do cliente é invisível para eles, tenha as meta tags que tiver.

O que conta para eles junta-se às boas práticas clássicas: HTML servido tal e qual, uma estrutura de títulos que segue o discurso, dados estruturados, uma resposta clara no topo da página em vez de três parágrafos de introdução.

A ordem das prioridades

  1. Servir HTML completo, por pré-renderização ou renderização no servidor. Sem isso, o resto não serve de nada.
  2. Um título e uma descrição próprios de cada página, com o unhead.
  3. Um link canónico nas páginas acessíveis por vários URL.
  4. Um sitemap gerado e declarado no robots.txt.
  5. Os dados estruturados nas páginas que descrevem uma entidade identificável.
  6. O desempenho, medido sobre o site construído.

Se estás a descobrir o Vue, o tutorial de Vue 3 para principiantes assenta as bases necessárias antes de atacar o routing e a renderização no servidor.

Erros frequentes

Instalar o vue-meta A sua versão para Vue 3 nunca chegou a estabilizar. A biblioteca usada hoje é o unhead, inclusive pelo Nuxt.
Dois caminhos de import no unhead Os composables vêm de @unhead/vue, o createHead de @unhead/vue/client. Importar tudo do mesmo caminho dá «useHead is not a function».
Cuidar das meta tags numa aplicação vazia Um robô que não executa JavaScript não vê nada. Verifica primeiro a saída do curl.
Construir o canónico a partir de window.location.href Os parâmetros de campanha acabam no link canónico. Usa o caminho da rota.
Medir o desempenho no servidor de desenvolvimento Os números não têm nada a ver com produção. Mede sobre o resultado de npm run build.

JavaScriptSEOVue.js

Damien Flandrin Programador web desde 2010, criador da Gekkode e do Email Impact. Cada artigo é testado num projeto real antes de ser publicado. Contacto
Newsletter

Os novos testes, tutoriais e projetos, por e-mail.

Testes reproduzíveis, código versionado, resultados datados. Nunca spam.