Optimisation SEO pour Développeurs
Thème :
Junior
1 vues ·1/2 leçons

Cours Pratique : Optimisation SEO et Open Graph pour une Application Inertia.js avec VueUse/Head

Bienvenue dans ce guide complet où nous allons transformer votre blog Inertia.js en une machine à référencer. L’objectif est simple : chaque article doit avoir des balises <title> et <meta> uniques et dynamiques, ce qui améliore drastiquement son visibilité sur Google et son attractivité lors du partage sur les réseaux sociaux.

Le Défi du SEO avec Inertia.js

Votre application est une SPA (Single Page Application). Quand vous naviguez d’un article à l’autre, seule une partie de la page se met à jour, sans rechargement complet. Le problème ? Les balises <head> du document HTML, elles, ne se mettent pas à jour automatiquement. Pour Google et les réseaux sociaux, c’est comme si toutes vos pages avaient le même titre et la même description.

Notre Solution : @vueuse/head

Pour résoudre ce problème de manière élégante et moderne, nous allons utiliser le package @vueuse/head.

Pourquoi est-ce le meilleur choix ?

  • Réactivité Totale : Il met à jour les balises <head> automatiquement lorsque vos données changent, que ce soit au chargement initial (côté serveur) ou lors des navigations (côté client).
  • Intégration Parfaite : Conçu pour Vue 3 et l’API Composition, il s’intègre sans effort à votre stack Inertia.
  • Syntaxe Claire : La configuration est intuitive et facile à maintenir.
  • Exhaustif : Il gère tout : le SEO classique pour Google, les balises Open Graph (Facebook, LinkedIn) et les Twitter Cards.

Étape 1 : Installation

Rien de plus simple. Ouvrez votre terminal à la racine de votre projet Laravel et lancez :

npm install @vueuse/head

Étape 2 : Configuration Globale du Plugin

Pour que @vueuse/head soit disponible dans toute votre application, nous allons l’enregistrer comme un plugin global.

2.1. Créer le fichier du plugin

Créez un nouveau fichier pour centraliser la configuration : resources/js/plugins/head.ts

// resources/js/plugins/head.ts
import { createHead } from '@vueuse/head';

// On exporte une instance "head" pré-configurée
export const head = createHead();

2.2. Enregistrer le plugin dans l’application

Ouvrez votre fichier d’entrée principal : resources/js/app.ts. Comme vous utilisez Wayfinder, la configuration est plus simple car vous n’avez pas besoin du plugin ZiggyVue.

// resources/js/app.ts
import './bootstrap';
import '../css/app.css';

import { createInertiaApp } from '@inertiajs/vue3';
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';

// Importez le plugin head que nous venons de créer
import { head } from './plugins/head';

const appName = import.meta.env.VITE_APP_NAME || 'Laravel';

createInertiaApp({
    title: (title) => `${title} - ${appName}`,
    resolve: (name) => resolvePageComponent(`./Pages/${name}.vue`, import.meta.glob('./Pages/**/*.vue')),
    setup({ el, App, props, plugin }) {
        return (
            createApp({ render: () => h(App, props) })
                .use(plugin)
                // ✅ On ajoute simplement notre plugin head
                .use(head)
                .mount(el)
        );
    },
    progress: {
        color: '#4B5563',
    },
});

Étape 3 : Implémentation dans le Composant BlogShow.vue

C’est ici que la magie opère. Nous allons utiliser les données envoyées par votre BlogController pour remplir dynamiquement les balises.

Voici le code final et corrigé pour le bloc <script setup> de votre fichier resources/js/Pages/BlogViews/BlogShow.vue. Copiez et collez ce code pour remplacer votre bloc existant.

<script setup lang="ts">
import { Link, router, useForm, usePage, Head } from '@inertiajs/vue3';
import { useHead } from '@vueuse/head';
import { computed, onMounted, ref } from 'vue';
import * as blogRoutes from '@/routes/blog';
import * as blogCommentRoutes from '@/routes/blog/comments';
import * as blogCommentPublicRoutes from '@/routes/blog/comments/public';

// --- ÉTAPE 1 : DÉFINITION DES PROPS (TRÈS IMPORTANT) ---
// Définissez toujours vos props en premier pour qu'elles soient accessibles partout.
interface Props {
    post: {
        id: number;
        title: string;
        slug: string;
        excerpt: string;
        meta_title: string;
        meta_description: string;
        content: string;
        cover_image: string | null;
        published_at: string;
        reading_time: number;
        view_count: number;
        allow_comments: boolean;
        category: { name: string; slug: string };
        categories: { id: number; name: string; slug: string }[];
        tags: { id: number; name: string; slug: string }[];
        user: { id: number; name: string };
        comments: any[];
        relatedPosts: any[];
    };
    latestPosts: { id: number; title: string; slug: string; published_at: string }[];
    latestCategories: { id: number; name: string; slug: string }[];
    allTags: { id: number; name: string; slug: string }[];
    featuredPosts: { id: number; title: string; slug: string; cover_image: string | null }[];
    totalComments: number;
}

// ✅ Déclaration des props en premier. C'est la source de vérité pour votre composant.
const props = defineProps<Props>();

// --- ÉTAPE 2 : LOGIQUE SEO avec @vueuse/head ---

// On crée une computed dédiée pour le JSON-LD pour éviter les erreurs de sérialisation.
const jsonLd = computed(() => {
    return JSON.stringify({
        '@context': 'https://schema.org',
        '@type': 'Article',
        headline: props.post.title,
        description: props.post.meta_description || props.post.excerpt,
        image: props.post.cover_image
            ? `https://VOTRE_DOMAINE.com/${props.post.cover_image}`
            : 'https://VOTRE_DOMAINE.com/images/default-og-image.jpg',
        datePublished: props.post.published_at,
        author: { '@type': 'Person', name: props.post.user.name },
    });
});

useHead({
    // Le titre de l'onglet du navigateur, avec un fallback
    title: computed(() => props.post.meta_title || props.post.title),

    // Les balises meta pour le SEO et les réseaux sociaux
    meta: [
        { name: 'description', content: computed(() => props.post.meta_description || props.post.excerpt) },
        { property: 'og:title', content: computed(() => props.post.meta_title || props.post.title) },
        { property: 'og:description', content: computed(() => props.post.meta_description || props.post.excerpt) },
        {
            property: 'og:image',
            content: computed(() =>
                props.post.cover_image
                    ? `https://VOTRE_DOMAINE.com/${props.post.cover_image}`
                    : 'https://VOTRE_DOMAINE.com/images/default-og-image.jpg',
            ),
        },
        { property: 'og:type', content: 'article' },
        { property: 'og:url', content: computed(() => blogRoutes.show(props.post.slug).url) }, // Utilisation de Wayfinder
        { name: 'twitter:card', content: 'summary_large_image' },
        { name: 'twitter:title', content: computed(() => props.post.meta_title || props.post.title) },
        { name: 'twitter:description', content: computed(() => props.post.meta_description || props.post.excerpt) },
        {
            name: 'twitter:image',
            content: computed(() =>
                props.post.cover_image
                    ? `https://VOTRE_DOMAINE.com/${props.post.cover_image}`
                    : 'https://VOTRE_DOMAINE.com/images/default-og-image.jpg',
            ),
        },
    ],
    script: [{ type: 'application/ld+json', children: jsonLd }], // On utilise la computed que nous venons de créer
});

// --- ÉTAPE 3 : RESTE DE LA LOGIQUE DU COMPOSANT ---
// ... (le reste de votre code pour les commentaires, etc., va ici)
</script>

<template>
    <!-- 
      Le composant <Head> d'Inertia est toujours utile pour le rendu 
      initial côté serveur. Il fonctionne en parallèle de @vueuse/head.
    -->
    <Head :title="props.post.meta_title || props.post.title" />

    <!-- Le reste de votre template HTML pour l'article... -->
</template>

⚠️ Pièges Courants et Bonnes Pratiques

Le code ci-dessus est le fruit de notre débogage. Voici pourquoi il est structuré ainsi :

  1. L’Erreur Duplicate key 'post' : Elle se produit si vous essayez de créer une variable post avec computed alors que defineProps la rend déjà accessible. Solution : Utilisez toujours props.post directement.
  2. L’Erreur Converting circular structure to JSON : Elle arrive quand vous essayez de mettre un objet computed directement dans JSON.stringify pour le JSON-LD. Solution : Créez une computed dédiée (notre jsonLd) qui retourne la chaîne JSON. C’est plus propre et évite les erreurs de sérialisation.
  3. L’Ordre de Déclaration : defineProps doit être appelé au tout début de votre <script setup>. Cela garantit que la variable props est définie et utilisable par toutes les computed qui suivent.

Étape 4 : Vérification

Comment savoir si tout fonctionne ?

  1. Ouvrez votre article dans le navigateur.
  2. Faites un clic droit -> “Inspecter”.
  3. Allez dans l’onglet “Éléments” et regardez la balise <head>. Vous devriez voir toutes vos balises <title>, <meta name="description">, <meta property="og:title">, etc., avec les bonnes informations !
  4. Testez sur les réseaux sociaux :

Conclusion

Félicitations ! Vous venez de mettre en place une solution de SEO robuste, moderne et parfaitement intégrée à votre stack technique. Vos articles sont maintenant optimisés pour les moteurs de recherche et prêts à être partagés sur les réseaux sociaux avec un rendu professionnel.

Prêt à passer à la suite ?

Vous avez terminé cette leçon sur Cours Pratique : Optimisation SEO et Open Graph pour une Application Inertia.js avec VueUse/Head. Continuez votre apprentissage avec la prochaine leçon ou explorez d'autres formations.