# CLAUDE.md — SEO pour SPA Vue.js > Contexte spécialisé pour Claude Code. Coller ce fichier à la racine du projet pour guider le travail SEO sur une application Vue.js single-page (Vite, vue-router, Pinia). --- ## Quand utiliser ce contexte - ✅ SPA Vue.js (Vite + vue-router) avec des pages publiques indexables par les moteurs de recherche - ✅ Choix d'architecture CSR vs SSR/SSG : arbitrage SEO vs complexité de déploiement - ✅ Audit SEO d'une app existante avec du contenu rendu côté client uniquement - ❌ Application 100% authentifiée (dashboard, back-office) : Google ne crawle pas les pages derrière login, le SEO n'a pas d'importance - ❌ Nuxt.js ou Vite SSR déjà configuré : ce contexte couvre le cas SPA CSR sans SSR --- ## Section 1 : Audit SEO initial ### Ce que Google voit par défaut Une SPA Vue.js + Vite par défaut sert un `index.html` quasi vide : - `Vite App` (le défaut Vite) - `` (attribut lang vide) - `
` (aucun contenu) - Zéro meta description, zéro Open Graph ### Vérifier l'état actuel 1. **View Source** (pas Inspect) — c'est ce que les crawlers voient 2. **Google Search Console** → Inspection d'URL → "Tester l'URL en direct" → voir le HTML rendu 3. **curl** le site : `curl -s https://monsite.com | head -30` 4. Vérifier les pages clés : accueil, pages dynamiques (détail produit/service), pages légales ### Checklist audit - [ ] `` (ou la bonne langue) - [ ] `` pertinent (pas "Vite App") - [ ] `<meta name="description">` présent - [ ] Open Graph complet (og:title, og:description, og:image, og:url, og:type, og:locale) - [ ] Twitter Card (twitter:card, twitter:title, twitter:description) - [ ] `<link rel="canonical">` - [ ] robots.txt accessible - [ ] sitemap.xml accessible et à jour - [ ] Pas de `<meta name="robots" content="noindex">` sur les pages publiques - [ ] 404 gérée proprement (pas redirection vers login) --- ## Section 2 : index.html — la base statique ### Template minimal Le `index.html` à la racine doit contenir les meta statiques de fallback. C'est ce qui s'affiche avant que Vue.js ne s'initialise. ```html <!DOCTYPE html> <html lang="fr"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <!-- SEO de base --> <title>MonApp — Description courte
``` ### Règles - **Toujours** mettre `lang="fr"` (ou la bonne langue) sur `` - L'image OG doit faire **1200×630px** minimum - Le canonical doit pointer vers l'URL propre (pas de trailing slash en double, pas de paramètres) - Ces meta statiques sont le filet de sécurité — elles s'affichent si @unhead/vue n'a pas encore pris le relais --- ## Section 3 : @unhead/vue — meta dynamiques ### Installation ```bash npm install @unhead/vue ``` ### Setup dans main.js ```javascript import { createApp } from 'vue' import { createHead } from '@unhead/vue' import App from './App.vue' const app = createApp(App) const head = createHead() app.use(head) app.mount('#app') ``` ### Template de titre dans App.vue ```javascript import { useHead } from '@unhead/vue' useHead({ titleTemplate: '%s | MonApp', }) ``` ### useHead dans chaque vue **Page statique :** ```javascript useHead({ title: 'Accueil', meta: [ { name: 'description', content: 'Page d\'accueil de MonApp.' }, { property: 'og:title', content: 'MonApp — Accueil' }, ], }) ``` **Page dynamique (données API) — utiliser computed() :** ```javascript import { computed } from 'vue' import { useHead } from '@unhead/vue' const item = ref(null) useHead({ title: computed(() => item.value?.name ?? 'Chargement...'), meta: [ { name: 'description', content: computed(() => item.value ? `${item.value.name} — ${item.value.summary}` : 'Chargement...' ), }, ], }) ``` ### Règles - **Toujours** `computed()` pour les meta qui dépendent de données asynchrones - Ne PAS manipuler `document.title` manuellement — supprimer tout `router.afterEach` qui le fait - Un `useHead()` par vue, pas de logique partagée sauf composable dédié - Le titleTemplate évite la répétition du nom de l'app --- ## Section 4 : robots.txt et sitemap ### robots.txt Placer dans `/public/robots.txt` (copié tel quel par Vite au build) : ``` User-agent: * Allow: / Disallow: /admin/ Disallow: /profile Disallow: /mes-evaluations Disallow: /login Disallow: /register Sitemap: https://monsite.com/sitemap.xml ``` ### Règles robots.txt - **Bloquer** toutes les pages privées / auth-only - **Bloquer** login/register (pas de valeur SEO) - **Toujours** pointer vers le sitemap - Tester avec Google Search Console → robots.txt Tester ### Sitemap **Routes statiques** — sitemap.xml manuel ou généré au build : ```xml https://monsite.com/weekly1.0 https://monsite.com/servicesdaily0.9 ``` **Routes dynamiques** (milliers de pages) — générer côté backend : - Commande Symfony/Go/Node qui lit la BDD et génère le XML - Cron nocturne pour régénérer - Utiliser un sitemap index si > 50 000 URLs --- ## Section 5 : JSON-LD (Schema.org) ### Injection via useHead ```javascript useHead({ script: [ { type: 'application/ld+json', innerHTML: computed(() => JSON.stringify({ '@context': 'https://schema.org', '@type': 'Product', // ou Service, Article, LocalBusiness... 'name': item.value?.name, 'description': item.value?.description, 'aggregateRating': { '@type': 'AggregateRating', 'ratingValue': item.value?.rating, 'reviewCount': item.value?.reviewCount, 'bestRating': 5, }, })), }, ], }) ``` ### Types Schema.org courants | Type de page | Schema.org type | |-------------|----------------| | Page produit | Product | | Service | Service, GovernmentService | | Article de blog | BlogPosting, Article | | Commerce local | LocalBusiness | | Personne / CV | Person | | FAQ | FAQPage | | Événement | Event | ### Règles - Tester avec https://search.google.com/test/rich-results - Ne mettre que des données réelles (pas de placeholder) - `computed()` obligatoire si les données viennent d'une API - Un seul bloc JSON-LD principal par page --- ## Section 6 : Route 404 et erreurs ### Vue Router — catch-all ```javascript { path: '/:pathMatch(.*)*', name: 'NotFound', component: () => import('@/views/NotFoundView.vue'), } ``` ### Ce qu'il ne faut PAS faire - ❌ Rediriger les 404 vers `/login` ou `/` (mauvais signal SEO) - ❌ Laisser le catch-all sans composant (page blanche) ### NotFoundView.vue ```javascript useHead({ title: 'Page introuvable', meta: [ { name: 'robots', content: 'noindex' }, ], }) ``` - `noindex` sur les 404 pour ne pas polluer l'index Google - Proposer un lien retour vers l'accueil et une recherche --- ## Section 7 : Limitations CSR et alternatives ### Ce que le rendu client (CSR) ne couvre PAS | Crawler | Exécute JS ? | Lit les meta @unhead ? | |---------|-------------|----------------------| | Googlebot | ✅ Oui | ✅ Oui | | Bingbot | ✅ Oui (partiel) | ⚠️ Parfois | | LinkedIn preview | ❌ Non | ❌ Non (lit index.html) | | Twitter/X Card | ❌ Non | ❌ Non (lit index.html) | | Slack unfurl | ❌ Non | ❌ Non (lit index.html) | | Facebook share | ⚠️ Partiel | ⚠️ Partiel | ### Quand passer au SSR (Nuxt) - Si le trafic justifie l'investissement - Si les previews LinkedIn/Slack/Twitter sont critiques (B2B, content marketing) - Si le site est principalement du contenu (blog, e-commerce catalogue) ### Quand rester en CSR - App interne / dashboard - MVP / side project - Les meta statiques dans index.html suffisent pour la page d'accueil - Les `useHead()` avec `computed()` sont compatibles Nuxt — migration future facile --- ## Section 8 : Checklist finale ``` □ index.html : lang, title, meta description, Open Graph, Twitter Card, canonical, theme-color □ @unhead/vue installé et configuré dans main.js □ titleTemplate dans App.vue □ useHead() dans chaque vue avec title + description □ computed() pour les meta dynamiques (données API) □ Supprimé tout document.title manuel □ robots.txt : Allow publiques, Disallow privées, Sitemap □ Sitemap : statique + dynamique pour les routes paramétrées □ JSON-LD Schema.org sur les pages de contenu □ Route 404 : NotFoundView avec noindex, pas de redirection □ Canonical URL sur chaque page □ Image OG : 1200×630px minimum □ Test : Google Search Console → Inspection d'URL □ Test : https://search.google.com/test/rich-results □ Test : partager l'URL sur LinkedIn/Twitter pour vérifier le preview ``` --- *Last updated: 2025-03 — Revoir si : Vue 3.5+ (changements SSR/hydration), Googlebot mise à jour du rendering pipeline, ou Vite 6+ (SSR API stable).*